0
votes

I have two entities:

public class EntityA
{
    public int? Id { get; set; }
    public string Name { get; set; }
    public EntityB { get; set; }
}

public class EntityB
{
    public int? Id { get; set; }
    public string Version { get; set; }
}

I have existing records for EntityB already in the database. I want to add a new EntityA with reference to one of the EntityB records.

var entityB = _dbContext.EntityB.FirstOrDefault(e => e.Id == 1);

var entityA = new EntityA { Name = "Test", EntityB = entityB };

_dbContext.Add(entityA);
_dbContext.SaveChanges();

When the above code runs I get the following error:

System.InvalidOperationException: The property 'Id' on entity type 'EntityB' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.

This seems to me, that the save is trying to also add EntityB, not just a reference to it. I do have the relationship specified in the database as well as in Entity Framework, e.g. when querying for EntityA if I include EntityB in the select, I get the referenced entity as well (so the relationship works).

modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id);
  e.HasOne(p => p.EntityB)
    .WithOne()
    .HasForeignKey<EntityB>(p => p.Id);
}

modelBuilder.Entity<EntityB>(e =>
{
  e.HasKey(p => p.Id);
}

How can I save a new EntityA, with only a reference to the selected EntityB, rather than saving both entities?

2
Did you intend to create a 1:1 Between EntityB and EntityA? You code looks that way, but it is a less common scenario and questions regarding config for 1:1 are usually explicit in that requirement, this one could easily be interpreted either way. - Chris Schaller
You're right, it should have been 1:many. - Jerry

2 Answers

1
votes

It looks like you are trying to Extend EntityB with an optional 1:1 reference to a Row n the new table EntityA. You want both records to have the same value for Id.

This 1:1 link is sometimes referred to as Table Splitting.
Logically in your application the record from EntityB and EntityA represent the same business domain object.

If you were simply trying to create a regular 1 : many relationship, then you should remove the HasOne().WithOne() as this creates a 1:1, you would also not try to make the FK back to the Id property.

The following advice only applies to configure 1:1 relationship

you might use Table Splitting for performance reasons (usually middle tier performance) or security reasons. But it also comes up when we need to extend a legacy schema with new metadata and there is code that we cannot control that would have broken if we just added the extra fields to the existing table.

Your setup for this is mostly correct, except that EntityA.Id cannot be nullable, as the primary key it must have a value.

public class EntityA
{
    public int Id { get; set; }
    public string Name { get; set; }
    public EntityB { get; set; }
}

If you want records to exist in EntityA that DO NOT have a corresponding record in EntityB then you need to use another Id column as either the primary key for EntityA or the foreign key to EntityB

You then need to close the gap with the EntityA.Id field by disabling the auto generated behaviour so that it assumes the Id value from EntityB:

modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id).ValueGeneratedNever();
  e.HasOne(p => p.EntityB)
    .WithOne()
    .HasForeignKey<EntityB>(p => p.Id);
}

I would probably go one step further and add the Reciprocating or Inverse navigation property into EntityB this would allow us to use more fluent style assignment, instead of using _dbContext.Add() to add the record to the database:

public class EntityB
{
    public int Id { get; set; }
    public string Version { get; set; }
    public virtual EntityA { get; set; }
}

With config:

modelBuilder.Entity<EntityA>(e =>
{
    e.HasKey(p => p.Id).ValueGeneratedNever();
    e.HasOne(p => p.EntityB)
     .WithOne(p => p.EntityA)
     .HasForeignKey<EntityB>(p => p.Id);
}

This allows you to add in a more fluent style:

var entityB = _dbContext.EntityB.FirstOrDefault(e => e.Id == 1);

entityB.EntityA = new EntityA { Name = "Test" };

_dbContext.SaveChanges();
1
votes

This will trip up because you are using EntityA's PK as the FK to Entity B, which enforces a 1 to 1 direct relation. An example of this would be to have something like an Order and OrderDetails which contains additional details about a specific order. Both would use "OrderId" as their PK and OrderDetails uses it's PK to relate back to its Order.

If instead, EntityB is more like an OrderType reference, you wouldn't use a HasOne / WithOne relationship because that would require Order #1 to only be associated with OrderType #1. If you tried linking OrderType #2 to Order #1, EF would be trying to replace the PK on OrderType, which is illegal.

Typically the relationship between EntityA and EntityB would require an EntityBId column on the EntityA table to serve as the FK. This can be a property in the EntityA entity, or left as a Shadow Property (Recommended where EntityA will have an EntityB navigation property) Using the above example with Order and OrderType, an Order record would have an OrderId (PK) and an OrderTypeId (FK) to the type of order it is associated with.

The mapping for this would be: (Shadow Property)

modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id);
  e.HasOne(p => p.EntityB)
    .WithMany()
    .HasForeignKey("EntityBId");
}

An OrderType can be assigned to many Orders, but we don't have an Orders collection on OrderType. We use the .HasForeignKey("EntityBId") to set up the shadow property of "EntityBId" on our EntityA table. Alternatively, if we declare the EntityBId property on our EntityA:

modelBuilder.Entity<EntityA>(e =>
{
  e.HasKey(p => p.Id);
  e.HasOne(p => p.EntityB)
    .WithMany()
    .HasForeignKey(p => p.EntityBId);
}

On a side note, navigation properties should be declared virtual. Even if you don't want to rely on lazy loading (recommended) it helps ensure the EF proxies for change tracking will be fully supported, and lazy loading is generally a better condition to be in at runtime than throwing NullReferenceExceptions.