Exception The value of 'X' is unknown when attempting to save changes

Viewed 9771

There are these two entities:

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public CompanyVehicle CompanyVehicle { get; set; }
}

and

public class CompanyVehicle
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Employee Employee { get; set; }
}

Using Entity Framework Core 5.0.8 on SQL Server 2019, the configuration for CompanyVehicle is:

entityBuilder.HasOne(t => t.Employee)
    .WithOne(t => t.CompanyVehicle)
    .HasForeignKey<Employee>(t => t.Id)
    .IsRequired();

And we'll try to insert something:

public void Create(Employee employee)
{
    employee.CompanyVehicle = new CompanyVehicle();
    dbContext.Add<Employee>(employee);
    dbContext.SaveChanges();
}

The above code used to work fine in EF6. Two new records in both Employee and CompanyVehicle tables were created with the same Id. After migrating to EF Core 5.0.8, dbContext.SaveChanges() throws an exception:

System.InvalidOperationException: 'The value of 'Employee.Id' is unknown when attempting to save changes. This is because the property is also part of a foreign key for which the principal entity in the relationship is not known.'

Note that these entities are just examples and the database design should not be altered in my case.

Update
After some more investigation, I've found out my problem is:
Having X (principal) and Y (dependent) as two tables where X.Id is PK for X and Y.Id is PK for Y and also FK to X, in EF Core a record of X cannot be inserted.

3 Answers

So I finally found the problem, configuring a Property to be both PK and FK is possible and very easy. We had our old codes after migrating to EFCore from EF6 in an assembly. The project is a framework so in OnModelCreating we use modelBuilder.ApplyConfigurationsFromAssembly in our base DbContext to register configurations in the guest projects. The project will automatically find all the configurations in all of assemblies referenced by the project or DLLs in the application path.
The key point is: In EF Core explicit fluent FK configuration is in the reverse order compared to EF6. So in EF6 for Employee we used to write:

this.HasRequired(t => t.CompanyVehicle)
    .WithRequiredDependent(t => t.Employee)
    .HasForeignKey(d => d.Id);

and in EF Core we should write:

b.HasOne(t => t.CompanyVehicle)
   .WithOne(t => t.Employee)
   .HasForeignKey<Employee>(t => t.Id).IsRequired();

The parameter d used in the first part is of type CompanyVehicle. So our migrator converted the old code to:

b.HasOne(t => t.CompanyVehicle)
   .WithOne(t => t.Employee)
   .HasForeignKey<CompanyVehicle>(t => t.Id).IsRequired();

Which is incorrect. The generic parameter should be the dependent table type. We later fixed the issue in a new namespace but the ApplyConfigurationsFromAssembly method kept applying the obsolete code after our configuration too.
I used the following block of code at the end of OnModelCreating to investigate the issue:

foreach (var entity in modelBuilder.Model.GetEntityTypes()) 
    foreach(var key in entity.GetForeignKeys())
    {
        //Check what is in the key...
    }

and noticed that there are duplicated keys configured for my entities.

Entity Framework Core configures one to one relationships by being able to detect the foreign key property, and thereby identify which is the principal and which is the dependent entity in the relationship.

First look at the existing database and check what is the dependant table, assuming it is the Employee, it should have a foriegn key to CompanyVehicle table. (It could be other way around in your case.)

1. Using EF Core convestions.

If Employee is the depentant table, add that exact foriegn key property name (let's assume it's Vehicle_Id) to your Employee entity. Follow 2nd method if you don't want to add a property to the class.

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Vehicle_Id { get; set; } // <-- This right here.
    public CompanyVehicle CompanyVehicle { get; set; }
}

Without this property, as I mentioned earlier, child/dependent side could not be determined for the one-to-one relationship. (check what is yours in the db and add that property, otherwise you will get two foreign keys in the Employee table)

And using fluent API, configure the relation like this. (Notice how a and b were used to separate two navigation properties, in your implementation you have used t, for both, and when you say .HasForeignKey<Employee>(t => t.Id), you're setting the foriegn key to primary key Id of Employee table, which could be the reason behind your error).

protected override void OnModelCreating(ModelBuilder modelBuilder)
{ 
    modelBuilder.Entity<CompanyVehicle>()
        .HasOne(a => a.Employee)
        .WithOne(b => b.CompanyVehicle)
        .HasForeignKey<Employee>(b => b.Vehicle_Id);
}

2. Not using EF Core conventions.

If you do not like to add a property to the dependant table, use the exsisting foriegn key in the database (let's assume it's Vehicle_Id), fluent API config should look like this.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{ 
    modelBuilder.Entity<CompanyVehicle>()
        .HasOne(a => a.Employee)
        .WithOne(b => b.CompanyVehicle)
        .HasForeignKey<Employee>("Vehicle_Id");
}

Edit:

The Has/With pattern is used to close the loop and fully define a relationship. In this case, since the relationship to be configured is a one-to-one, the HasOne method is chained with the WithOne method. Then the dependent entity (Employee) is identified by passing it in as a type parameter to the HasForeignKey method, which takes a lambda specifying which property in the dependent type is the foreign key.

So if you want the Employee Id to act as the foriegn key to the CompanyVehicle table, ammend your Fluent API as this, again notice a and b when specifying lambdas.

modelBuilder.Entity<CompanyVehicle>()
        .HasOne(a => a.Employee)
        .WithOne(b => b.CompanyVehicle)
        .HasForeignKey<Employee>(b => b.Id);

I had the same issue that A. Morel had.

When manually inserting a custom join table for ManyToMany, and a foreign key was 0, I was getting this error.

Fixed by changing the seed value of the parent table to start at 2:

DBCC CHECKIDENT ('program_contact', RESEED, 1);

Because of this issue

DBCC CHECKIDENT Sets Identity to 0

Related