Search code examples
c#entity-framework-coreef-code-first

How to set foreign key null by updating parent entity in EF Core?


I have two entities with this structure:

public abstract class BaseEntity
{
    public BaseEntity()
    {
        //Validate();
    }

    public Guid Id { get; set; } = Guid.NewGuid();
    public DateTime CreatedAt { get; protected set; } = DateTime.Now;
    public bool IsDeleted { get; protected set; }
    public DateTime? DeletedAt { get; protected set; }
    public DateTime? ModifiedAt { get; set; }

    public void SetAsDeleted()
    {
        DeletedAt = DateTime.Now;
        IsDeleted = true;
    }

    protected abstract void Validate();
}

public class Order : BaseEntity
{
    public Person Owner { get; set; }
    public long TotalPrice { get; set; }

    protected override void Validate()
    {
    }
}

public class Person : BaseEntity
{
    protected Person()
    {
        Orders = new List<Order>();
    }

    public Person(string name, string family)
    {
        Name = name;
        Family = family;
        Orders = new List<Order>();
    }
    
    public void AddOrder(Order order)
    {
        Orders.Add(order);
    }

    public void RemoveOrder(Guid id)
    {
        var item = Orders.FirstOrDefault(x => x.Id == id);

        if (item != null)
            Orders.Remove(item);
    }
    
    protected override void Validate()
    {
    }

    public string Name { get; set; }
    public string Family { get; set; }
    //private List<Order> _orders;
    //public IReadOnlyCollection<Order> Orders => _orders.AsReadOnly();

    public List<Order> Orders { get; set; }
}

I configured the relation between these objects like this:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<Person>()
            .HasMany(x => x.Orders)
            .WithOne(o => o.Owner)
            //.OnDelete(DeleteBehavior.SetNull)
            .HasForeignKey("OwnerId")
            .Metadata.DependentToPrincipal?.SetPropertyAccessMode(PropertyAccessMode.Field);
}

This is my repository:

public class GenericRepository<C,T> : IGenericRepository<T> where T : BaseEntity where C : DbContext 
{
    protected ILogger<GenericRepository<C, T>> Logger;
    protected readonly C DbContext;
    
    public GenericRepository(C dbContext, ILogger<GenericRepository<C,T>> logger)
    {
        DbContext = dbContext;
        Logger = logger;
    }

    protected IDbConnection DataBaseConnection => DbContext.Database.GetDbConnection();

    public virtual async Task SoftDeleteAsync(Guid id)
    {
        var item = await DbContext.Set<T>().FirstOrDefaultAsync(x => x.Id == id);
        item.SetAsDeleted();
        await DbContext.SaveChangesAsync();
    }

    public virtual async Task AddAsync(T entity)
    {
        await DbContext.Set<T>().AddAsync(entity);
        await DbContext.SaveChangesAsync();
    }

    public virtual async Task UpdateAsync(T entity)
    {
        DbContext.Attach(entity);
        DbContext.Entry(entity).State = EntityState.Modified;
        await DbContext.SaveChangesAsync();
    }

    public virtual async Task<T> GetByIdAsync(Guid id, bool asNoTracking = true)
    {
        var query = DbContext.Set<T>().AsQueryable();

        if (asNoTracking)
            query = query.AsNoTracking();
        
        return await query.FirstOrDefaultAsync(x => x.Id == id);
    }

    public virtual async Task<IEnumerable<T>> QueryAsync(Expression<Func<T, bool>> predict, bool asNoTracking = true)
    {
        var query = DbContext.Set<T>().Where(predict);
        
        if (asNoTracking)
        {
            query = query.AsNoTracking();
        }

        return await query.ToListAsync();
    }
}

I am adding an object to the database by running this code:

Person person = new Person("unknown", "person"); 

person.AddOrder(new Order
                    {
                        TotalPrice = 200
                    });
person.AddOrder(new Order
                    {
                        TotalPrice = 300
                    });
person.AddOrder(new Order
                    {
                        TotalPrice = 400
                    });

await _personRepository.AddAsync(person);

return Ok(person.Id);

It adds one row in the person table and three rows in the orders table.

Now I would like to detach the first order from the person. It means I want to set OwnerId to null (not deleting the entire row from the order table).

This is my code:

var person = await _personRepository.GetByIdAsync(id);
person.RemoveOrder(orderid);

await _personRepository.UpdateAsync(person);
return Ok(person.Id);

but the code has no effect on the database and still there are three rows related to the given person.


Solution

  • You have "asNoTracking" defaulting to true. So you would have to specify "GetByIdAsync(id, false)" to get an object that can be changed.

    In my opinion, you should consider removing the default of true for asNoTracking, forcing you to include it every time. It might make your code more verbose, but you will never forget to think about if tracking should be on or not, which can be a good thing.