8

(NOTE: This is not a duplicate of this question even though it has the same exception.)

I have a poor man's transaction in place where the strategy is:

  1. Insert a parent and child record.
  2. Perform a long-running operation.
  3. If long-running operation fails, go delete the previously-inserted parent and child records.

When I attempt step 3, I get the following message:

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.

I understand generally what this means, but I thought I was playing by the rules and no matter how hard I try to play by the rules, I'm unsure why I'm getting this message.

We use self-tracking entities and my code is effectively this:

var parent = new Parent(1,2,3);
var child = new Child(4,5,6);
parent.Children.Add(child);

MyContext.Parents.ApplyChanges(parent);
MyContext.SaveChanges(SaveOptions.AcceptAllChangesAfterSave);

// At this point, inserts were successful and entities are in an Unchanged state.
// Also at this point, I see that parent.Children.Count == 1

var shouldDeleteEntities = false;
try
{
  // This is not database-related. This process does some
  // encryption/decryption and uploads some files up to
  // Azure blob storage. It doesn't touch the DB.
  SomeLongRunningProcess();
}
catch
{
  // Oops, something bad happened. Let's delete the entities!
  shouldDeleteEntities = true;
}

// At this point, both entities are in an Unchanged state, child still
// appears in parent.Children, nothing is wrong that I can see.
parent.MarkAsDeleted();
child.MarkAsDeleted();

// I've tried MyContext.ApplyChanges here for both entities, no change.

// At this point, everything appears to be in the state that
// they're supposed to be!
try
{
  MyContext.SaveChanges(SaveOptions.AcceptAllChangesAfterSave);
}
catch
{
  // This exception was thrown and I can't figure out why!
}

What's wrong with this logic? Why am I not able to simply delete these two records? I've tried calling MyContext.ApplyChanges after I call MarkAsDeleted. I've tried all sorts of things and no matter what, no matter how hard I try to tell the Context that I want both of them deleted, it keeps throwing this exception.

Community
  • 1
  • 1
Jaxidian
  • 13,081
  • 8
  • 83
  • 125
  • Cant you do something like here: http://stackoverflow.com/questions/9432994/specify-a-cascading-delete-for-parent-child-relationships ? – Dzyann Apr 13 '13 at 22:24
  • I'm not sure but we have policies against cascading deletes. – Jaxidian Apr 13 '13 at 22:27
  • I never worked with EntityFramework, I have worked with NHibernate, and normally we would cascade. If you can not cascade then, you should Delete one of them, commit, and then delete the other one. Since you are not cascading there is no way the framework can guess that you are doing a valid delete, by deleting both. That from my experience with NHibernate is the problem. – Dzyann Apr 13 '13 at 22:30
  • @Dzyann Entity Framework is supposed to be able to figure this out if I'm doing a valid delete or not. That's the point - it's that functionality that is breaking. This isn't supposed to be happening with Entity Framework. It's acting as though it's only aware of one of my deletes and not both of my deletes. It's supposed to perform the deletes in the proper order to make it valid. – Jaxidian Apr 13 '13 at 23:05
  • What is `SomeLongRunningProcess` doing? If there are some other entities involved and you create (required) relationships to `parent` or `child` it might be possible that EF does not allow to delete parent and child because it would violate a FK constraint. (Despite the exception all entities are still attached to the context.) What happens for example if you replace `SomeLongRunningProcess` by `throw new InvalidOperationException()`? Can you delete then without exception? If yes, it's important obviously to know what exactly happens in `SomeLongRunningProcess`. – Slauma Apr 14 '13 at 11:46
  • @Slauma That's a good thought and question! However, it's unhelpful for me. The long-running process is not DB-related. It's doing some file (PDF/Excel and some others) encryption/decryption and uploading them to Azure blob storage. The Context and Entities are uninvolved other than we read a couple DB-generated IDs off of the saved entities (an `identity` field and a `newid()` field). I have actually done nearly identically to what you suggested - throwing an exception to trigger the deletes. I have to do this because it's hard to make the long-running process actually fail. – Jaxidian Apr 14 '13 at 14:05
  • 1
    I see. Is your code above using *self-tracking entities*? If yes, I suggest to emphasize it in your question or in the tags or even in the title, as self-tracking entities have become an "exotic" (and deprecated) EF approach. Maybe this is related to the problem: http://blog.alner.net/archive/2011/11/01/dont_reuse_context_with_entity_framework_self_tracking_entities.aspx – Slauma Apr 14 '13 at 14:32
  • @Slauma Indeed, they are STEs. Didn't think about that (I've actually never used non-STEs over the past 4 years I've been using EF so I guess I take that for granted). I'll update the question as well as look into that link. Thanks! – Jaxidian Apr 14 '13 at 14:42
  • @Slauma That was precisely my problem! Post it as an answer and I'll accept it. I'd encourage you to also repost the tweak to the Context.tt template that was in that blog post (as well as a link to it) as that was very key to fixing the problem! – Jaxidian Apr 14 '13 at 16:22
  • 2
    Could *you* please write the answer? I have actually no clue what the article is saying :) I just googled it accidentally and saw the correlation of "STE" and "Deletes don't work". I think it is more valuable for other readers if you post an answer with your understanding of STEs and the article. – Slauma Apr 14 '13 at 16:44

1 Answers1

4

@Slauma provided this answer in the above comments but asked me to post the answer.

The problem is that there is effectively a "bug" in the Self-Tracking Entities templates for Entity Framework (something Microsoft no longer recommends you use). A blog post specifically on this topic can be found here.

Specifically, the problem is that the Context's ObjectStateManager gets out of sync with the (attached) entities' ChangeTracker.State and you end up having objects with entity.ChangeTracker.State == ObjectState.Deleted but when the context.ObjectStateManager thinks that the state is set to EntityState.Unchanged. These two are clearly very different. So this fix effectively goes and looks for any object attached to the context as EntityState.Unchanged but digs down deeper and also checks each object's ChangeTracker.State for ObjectState.Deleted to fix things up.

An easy and very thoroughly-functional work-around for this problem (that has worked well for us) can be made in the Context's T4 template by replacing the #region Handle Initial Entity State block with the following code:

#region Handle Initial Entity State

var existingEntities = context
    .ObjectStateManager
    .GetObjectStateEntries(System.Data.EntityState.Unchanged)
    .Select(x => x.Entity as IObjectWithChangeTracker)
    .Where(x => x != null);

var deletes = entityIndex.AllEntities
                    .Where(x => x.ChangeTracker.State == ObjectState.Deleted)
                    .Union(existingEntities
                            .Where(x => x.ChangeTracker.State == ObjectState.Deleted));

var notDeleted = entityIndex.AllEntities
                    .Where(x => x.ChangeTracker.State != ObjectState.Deleted)
                    .Union(existingEntities
                            .Where(x => x.ChangeTracker.State != ObjectState.Deleted));

foreach (IObjectWithChangeTracker changedEntity in deletes)
{
    HandleDeletedEntity(context, entityIndex, allRelationships, changedEntity);
}

foreach (IObjectWithChangeTracker changedEntity in notDeleted)
{
    HandleEntity(context, entityIndex, allRelationships, changedEntity);
}

#endregion
Community
  • 1
  • 1
Jaxidian
  • 13,081
  • 8
  • 83
  • 125