Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The relationship could not be changed because one or more of the foreign-key properties is non-nullable

(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.

like image 594
Jaxidian Avatar asked Apr 13 '13 22:04

Jaxidian


1 Answers

@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
like image 89
Jaxidian Avatar answered Nov 15 '22 00:11

Jaxidian