0

i've an unexpected issue trying to remove child entity from parent single database record. After doing some tests we have replicated the problem. Our C# code use Northwind database.

NWModel context = new NWModel();
Orders order = context.Orders.Where(w => w.OrderID == 10248).FirstOrDefault();
context.Entry(order).Collection(typeof(Order_Details).Name).Load();
order.Order_Details.RemoveAt(1);
System.Data.Entity.Infrastructure.DbChangeTracker changeset = context.ChangeTracker;
var changes = changeset.Entries().Where(x => x.State == System.Data.Entity.EntityState.Modified).ToList<Object>();
var detetions = changeset.Entries().Where(x => x.State == System.Data.Entity.EntityState.Deleted).ToList<Object>();

All works fine with original Order_Details table setup.

var deletions correctly has got deleted record.

In order to reproduce the issue, we have added on Order_Details table a new PK identity int field (OrderDetailId int identity); after doing that: var deletions contain no record while var changes contain Order_Detail record.

EF set Orders property of Order_Detail to null and mark the record as Updated.

I've found a lot of articles regarding this issue, all of them suggests to mark Order_Detail Orders property to [Required].

I've tried to set [Required] attribute on FK entity as suggested in this post, (this article describe EFCore behaviour that is the same as EF6 behaviour) but is does not solve my issue.

Is this behaviour expected?

we would appreciate any comments or suggestions.

Thanks

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
Fabio Borghi
  • 23
  • 1
  • 6

2 Answers2

1

AFAIK this is the correct behavior.

Indeed, you do not delete the detail. You remove it from the order details collection, that is you cut the relation between the order and the detail. As this relation is materialized by a navigation property on the detail side, then two things occur:

  • the collection is updated,
  • the navigation property is set to null.

Logically from this point if you SaveChanges you should have an exception because a detail cannot exist without an order, and you haven't yet deleted the detail, only cut the relation. So you should ctx.Set<orderDetail>().Remove(detail). (a bit annoying)

This is why in this case, I usually use composit key for details: detailId + orderId.

So when you remove the detail, the orderId is set to null <=> the PK is considered as null => the entity is marked for deletion.

tschmit007
  • 7,559
  • 2
  • 35
  • 43
  • Hi, thank you for your quick answer. We trying to replace an older L2S model with EF6.2 model; changing `Order.Remove(detail)` approach with `ctx.Set().Remove(detail)` on our solution will do an expensive job. Do you know a quick way in order to intercept and manage on `DbContext` level this issue? Thank you – Fabio Borghi Mar 18 '19 at 12:29
  • to use a composite key, The FK should be a component of the PK so that: Remove => (FK = null <=> PK = null) => delete from the db – tschmit007 Mar 18 '19 at 12:36
  • unfortunately this is not an option for us because of we need to keep alive older L2S ORM and EF on two different solutions. In this scenario, older L2S could not correctly manage FK as a component of PK. Do you think working on `DbContext` level should be an option? – Fabio Borghi Mar 18 '19 at 14:12
  • Not sure to understand, but you caa override the `SaveChanges` to seek/update the context to remove the details with a null FK. Depending on your model size it will be scalable/maintainable or not. – tschmit007 Mar 18 '19 at 14:53
1

Following tschmit007 suggestion, finally we have implemented this workaround.

On Orders entity we have used ObservableListSource<> instead of ICollection<>; into ObservableListSource<> class, void RemoveItem override method, we could manage ctx.Set<orderDetail>().Remove(detail) method. In this way all child record deletion works as expected.

Here is implemented code:

public partial class Orders
{
public Orders()
{
    Order_Details = new ObservableListSource<Order_Details>();
}
[Key]
public int OrderID { get; set; }
……………
public virtual ObservableListSource<Order_Details> Order_Details { get; set; }
}


public class ObservableListSource<T> : ObservableCollection<T>, IListSource
        where T : class
{
    private IBindingList _bindingList;

    bool IListSource.ContainsListCollection { get { return false; } }

    IList IListSource.GetList()
    {
        return _bindingList ?? (_bindingList = this.ToBindingList());
    }
    private bool _bRemoveInProgress = false;
    protected override void RemoveItem(int index)
    {
        if (!_bRemoveInProgress && index>=0)
        {
            _bRemoveInProgress = true;
            DbContext cntx = this[index].GetDbContextFromEntity();
            Type tp = this[index].GetDynamicProxiesType();
            cntx.Set(tp).Remove(this[index]);
            base.RemoveItem(index);
        }
        _bRemoveInProgress = false;
    }
}


public static class DbContextExtender
{
    public static Type GetDynamicProxiesType(this object entity)
    {
        var thisType = entity.GetType();
        if (thisType.Namespace == "System.Data.Entity.DynamicProxies")
            return thisType.BaseType;
        return thisType;
    }
    public static DbContext GetDbContextFromEntity(this object entity)
    {
        var object_context = GetObjectContextFromEntity(entity);

        if (object_context == null)
            return null;

        return new DbContext(object_context, dbContextOwnsObjectContext: false);
        //return object_context;
    }

    private static ObjectContext GetObjectContextFromEntity(object entity)
    {
        var field = entity.GetType().GetField("_entityWrapper");

        if (field == null)
            return null;

        var wrapper = field.GetValue(entity);
        var property = wrapper.GetType().GetProperty("Context");
        var context = (ObjectContext)property.GetValue(wrapper, null);

        return context;
    }
}
Fabio Borghi
  • 23
  • 1
  • 6