Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DbUpdateException with entities that do not expose foreign key properties

I have an entity model with User and Person entities, such that each User must be associated with exactly 1 Person, and each Person can be associated zero or 1 User.

User (0..1) <-------> (1) Person

The association is mapped fluently. Originally I only had it declared on the Person side:

private class PersonOrm : EntityTypeConfiguration<Person>
{
    internal PersonOrm()
    {
        ToTable(typeof(Person).Name, DbSchemaName.People);

        // has zero or one User
        HasOptional(p => p.User)
            .WithRequired(d => d.Person)
            .Map(d => d.MapKey("PersonId"))
            .WillCascadeOnDelete(false)
        ;

Since I encountered this error, I also added the same mapping to the User side:

private class UserOrm : EntityTypeConfiguration<User>
{
    internal UserOrm()
    {
        ToTable(typeof(User).Name, DbSchemaName.Identity);

        // has exactly 1 Person
        HasRequired(p => p.Person)
            .WithOptional(d => d.User)
            .Map(d => d.MapKey("PersonId"))
            .WillCascadeOnDelete(false);

There is a scenario in the application where a new User can be created and associated with an existing Person. This is where I am having difficulty at the moment. EF is considering User as the dependent side of the relationship, and is putting a PersonId (FK, int, not null) column on the User table. I don't believe it's possible to use a foreign key property on either entity to help EF manage the association (is it?).

Here is some failing code that tries to handle the scenario:

// find out if Person already exists
var person = context.People.SingleOrDefault(p => p.Emails.Any(
    e => e.Value.Equals(emailAddress, StringComparison.OrdinalIgnoreCase)));

// find out if User already exists
var user = context.Users.SingleOrDefault(
    u => u.Name.Equals(emailAddress, StringComparison.OrdinalIgnoreCase));

if (user == null)
{
    user = new User
    {
        Name = emailAddress,
        IsRegistered = isRegistered,
        Person = person ?? PersonFactory.Create(emailAddress),
        // ignore the PersonFactory.Create, that part works
    };

    context.Entry(user).State = EntityState.Added;
    context.SaveChanges();
}

This code works fine when person is null (does not already exist in the db). However when person is not null (already exists in db) and user is null, the code attempts to create a new User and associate it with the existing Person. When invoking SaveChanges(), I get a DbUpdateException:

An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.

The inner exception is:

A relationship from the 'User_Person' AssociationSet is in the 'Deleted' state. Given multiplicity constraints, a corresponding 'User_Person_Source' must also in the 'Deleted' state.

This doesn't make any sense to me because I'm not trying to delete anything, and checking the EntityState of both User and Person shows that User is in the Added state, whereas Person is in the Unchanged state. I have overridden SaveChanges() to demonstrate:

public override int SaveChanges()
{
    var userChanges = ChangeTracker.Entries<User>().Single();
    var personChanges = ChangeTracker.Entries<Person>().Single();

    if (userChanges.State == EntityState.Deleted)
        throw new InvalidOperationException("wtf?");

    if (personChanges.State == EntityState.Deleted)
        throw new InvalidOperationException("wtf?");

    return base.SaveChanges();
}

When this code executes, neither InvalidOperationException is thrown. Again, userChanges is in the Added state, and personChanges is in the Unchanged state.

What am I doing wrong?

like image 840
danludwig Avatar asked Apr 11 '12 20:04

danludwig


1 Answers

I feel really dumb right now. After writing up this careful question, it's now obvious.

The Person I am testing with already exists, and already has a User association with a different User.Name. This is why user is coming up null. Setting the Person.User property to a new User is causing the old User to be put in the Deleted state. Doh.

Sorry to have wasted your time. I'll leave the question up unless it's agreed it would be better to delete it.

like image 84
danludwig Avatar answered Oct 24 '22 05:10

danludwig