7

I am trying to not have my Business Logic know the inner workings of my Data Layer and vica versa.

But Entity Framework is making that hard. I can insert into a collection (in my Business Layer) without a reference to the ObjectContext:

order.Containers.Add(new Container { ContainerId = containerId, Order = order });

And that saves fine when it comes time to do a SaveChanges() in the Data Layer.

But to delete an item from a collection I need a reference to the ObjectContext. (I am case #1 in this guide to deleting EF Entities.) If I just do this:

 delContainers.ForEach(container => order.Containers.Remove(container));

Then when I call SaveChanges() I get an exception telling me that I need to delete the object as well as the reference.

So, my options as I see it are:

  1. To pass a delegate to my Business Logic that will call the Entity Framework ObjectContext Delete method.
  2. Or (I am hoping) find a way to get all entities that have had their reference deleted and actually delete them. (Right before calling SaveChanges() in my data layer.)

Does anyone know a way to do that?

UPDATE:

I tried this:

// Add an event when Save Changes is called
this.ObjectContext.SavingChanges += OnSavingChanges; 

...

void OnSavingChanges(object sender, EventArgs e)
{
   var objectStateEntries = ObjectContext.ObjectStateManager
                                  .GetObjectStateEntries(EntityState.Deleted);
                                                         
   foreach (var objectStateEntry in objectStateEntries)
   {
       if (objectStateEntry.IsRelationship)
       {
            // Find some way to delete the related entity
       }
   }
}

But none even though I deleted a relationship, the set of deleted items is empty.

(I tried viewing all the items too and my relationship is not in there. Clearly there is something fundamental that I don't get about ObjectStateManager.)

Community
  • 1
  • 1
Vaccano
  • 78,325
  • 149
  • 468
  • 850
  • Related: http://stackoverflow.com/questions/20707344/entity-framework-6-detect-relationship-changes – Nathan Sep 04 '15 at 20:02

2 Answers2

5

The correct solution for EF is point 3. from the linked article. It means propagating FK to principal entity into PK for dependent entity. This will form something called identifying relation which automatically deletes dependent entity when it is removed from the parent entity.

If you don't want to change your model and still want to achieve that in persistence ignorant way you probably can but it will work only for independent associations. Some initial implementation which works at least for my simple tested solution:

public partial class YourObjectContext
{
    public override int SaveChanges(SaveOptions options)
    {
        foreach (ObjectStateEntry relationEntry in ObjectStateManager
                                             .GetObjectStateEntries(EntityState.Deleted)
                                             .Where(e => e.IsRelationship))
        {
            var entry = GetEntityEntryFromRelation(relationEntry, 0);
            // Find representation of the relation 
            IRelatedEnd relatedEnd = entry.RelationshipManager
                                          .GetAllRelatedEnds()
                                          .First(r => r.RelationshipSet == relationEntry.EntitySet);

            RelationshipType relationshipType = relatedEnd.RelationshipSet.ElementType;
            if (!SkipDeletion(relationshipType))
            {
                // Now we know that model is inconsistent and entity on many side must be deleted
                if (!(relatedEnd is EntityReference)) // related end is many side
                {
                    entry = GetEntityEntryFromRelation(relationEntry, 1);
                }

                if (entry.State != EntityState.Deleted)
                {
                    context.DeleteObject(entry.Entity);
                }
            }
        }

        return base.SaveChanges();
    }

    private ObjectStateEntry GetEntityEntryFromRelation(ObjectStateEntry relationEntry, int index)
    {
        var firstKey = (EntityKey) relationEntry.OriginalValues[index];
        ObjectStateEntry entry = ObjectStateManager.GetObjectStateEntry(firstKey);
        return entry;
    }

    private bool SkipDeletion(RelationshipType relationshipType)
    {
        return
            // Many-to-many
            relationshipType.RelationshipEndMembers.All(
                r => r.RelationshipMultiplicity == RelationshipMultiplicity.Many) ||
            // ZeroOrOne-to-many 
            relationshipType.RelationshipEndMembers.Any(
                r => r.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne);
    }
}

To make it work your entities must be enabled for dynamic change tracking (all properties must be virtual and entity must be proxied) or you must manually call DetectChanges.

In case of foreign key associations the situation will be probably much worse because you will not find any deleted relation in the state manager. You will have to track changes to collections or keys manually and compare them to find discrepancies (I'm not sure how to do it in generic way) Foreign key association IMHO requires the identifying relation. Using FK properties already means that you included additional persistence dependency into your model.

Community
  • 1
  • 1
Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • Thank you for your answer. It sounds like I am out of luck because I just use what is default when you generate from a Database. That appears to be FK associations. Thank you for your blog post clarifying the two types of associations in EF. (I did not know there was more than one!) I am going to dig a bit more and see if I can find some way to detect that the FK Relationship has been deleted. (I would rather not change my project to try to use Independent Associations, especially since they are only supported for 1 to many relationships.) – Vaccano Dec 16 '11 at 23:55
  • As a side question, how can you have a "One to Many" relationship and have an Identifying Relation? The only way I know to model "One to Many" is to have the PK of the "One" table as a column in the "Many" table and use it as a FK (back to the "One" table). Is there a way to model an Identifying Relation so that I have a one to many relationship that allows deleting a reference and automatically deleting the associated entry in the "Many" table? – Vaccano Dec 19 '11 at 15:56
  • 1
    Did you check my linked answer where I describe identifying relation. Of course you can use it with one to many. You just need to make composite PK on dependent entity which consists of its own key and FK. – Ladislav Mrnka Dec 19 '11 at 18:00
  • Ah! Thank you. I feel dumb for not thinking of that. (We have an anti-composite PK stigma at my work, so I don't consider that option very often.) – Vaccano Dec 19 '11 at 18:09
2

One way is to write a change handler in your data layer:

    private void ContainersChanged(object sender,
        CollectionChangeEventArgs e)
    {
        // Check for a related reference being removed. 
        if (e.Action == CollectionChangeAction.Remove)
        {
            Context.DeleteObject(e.Element);
        }
    }

There are many places you can wire this up -- in your object's constructor or repository get or SavingChanges or wherever:

    entity.Containers.AssociationChanged += new CollectionChangeEventHandler(ContainersChanged);

Now you can remove the association from elsewhere and it will "cascade" to the entity.

Craig Stuntz
  • 125,891
  • 12
  • 252
  • 273
  • This looks as if it would work, but I would have to wire it up to every collection. Then when someone else adds a collection they would have to remember to wire it up too. (Eventually someone will forget and release a bug.) – Vaccano Dec 16 '11 at 23:03