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