3

Given these classes:

public class A
{
    public int Id {get; set;}
    public int? BId {get; set;}
    public B B {get; set;}
}

public class B
{
    public int Id {get; set;}
    public int? AId {get; set;}
    public A A {get; set;} 
}

Then with Fluent API

 modelBuilder.Entity<A>()
                .HasOne(a => a.B)
                .WithOne(b => b.A)
                .HasForeignKey<A>(a => a.BId);

When creating objects and add them to database, things look like following in the corresponding tables:

  • [A].BId is set
  • [B].AId = null

When I retrieve data using EF Core:

  • A.B is set, A.BId is set
  • B.A is set, but B.AId is null.

What should I do to have B.AId set as well?

ekad
  • 14,436
  • 26
  • 44
  • 46
Alex
  • 39
  • 1
  • 7
  • Why `one-to-zero` or `one-to-one` in both end? Which one is your principal entity and which one is your dependent entity? Please explain this. It would be easy to guess what you actually need. – TanvirArjel Feb 03 '19 at 05:34
  • They are independent. Each can exist without the other,. When they both exist, an A can only be associated with a B and same B can only be associated with initial A. – Alex Feb 03 '19 at 06:39
  • Okay. Got it. Then what's wrong with your current configuration? – TanvirArjel Feb 03 '19 at 06:40
  • Can you add your insert operation code here please! – TanvirArjel Feb 03 '19 at 06:49
  • ```A a = new A() { B = new B()} ``` After adding a to context and saving changes, in A table BId is set, but in B table AId is null. And also when retrieving data dbContext.As.Include(a=>a.B) I get a.B.A is set, but a.B.AId is null . – Alex Feb 03 '19 at 06:53
  • Because you have not set the AId in B object! Code is working as expected. Nothing wrong here. – TanvirArjel Feb 03 '19 at 06:56
  • OK, but at time of object creation that id is unkown, and I thought EF can take care of it. The same as for BId. However how can EF set A in a.B.A, but doesn't set AId in a.B.AId ? or in the simplier case b.A is set, but b.AId is not set? – Alex Feb 03 '19 at 06:59
  • EF taking care of BId because you assigning value to the navigation property. – TanvirArjel Feb 03 '19 at 07:01
  • OK, but what can I do to get AId set as well? – Alex Feb 03 '19 at 07:02
  • Why do you need to ser AId to B as you are already connected to A from B using BId? May are getting one-one-one relationship wrong! – TanvirArjel Feb 03 '19 at 07:24
  • When I have some B at hand I want to see its AId. I believe it is absolutely OK to navigate from both ends. Both from A to B and from B to A. And this from both object and ids perspective. – Alex Feb 03 '19 at 07:27

2 Answers2

10

These 0..1 : 0..1 relations are usually defined between entities of which none is an obvious principal entity. I like the example of cars and drivers, which is a bit more imaginable than A and B.

The model you're after looks like this:

OneToOneWithKeys

There are two mutual foreign keys, both of which have a unique index to enforce 1:1 at the database level.

The HasOne - WithOne combi can't be used here, because that always requires a HasForeignKey instruction to tell which entity is principal. This also configures only one field as foreign key. In your example, B.AId is just a regular field. If you don't give it a value, EF won't either.

The mapping of the above model is a bit more cumbersome than HasOne - WithOne:

var carEtb = modelBuilder.Entity<Car>();
var driverEtb = modelBuilder.Entity<Driver>();

carEtb.HasOne(c => c.Driver).WithMany();
carEtb.HasIndex(c => c.DriverID).IsUnique();

driverEtb.HasOne(d => d.Car).WithMany();
driverEtb.HasIndex(c => c.CarID).IsUnique();

So there are two 0..1:n association that are made unique by indexes on the foreign keys.

Which creates the following database model:

  CREATE TABLE [Drivers] (
      [ID] int NOT NULL IDENTITY,
      [Name] nvarchar(max) NULL,
      [CarID] int NULL,
      CONSTRAINT [PK_Drivers] PRIMARY KEY ([ID])
  );

  CREATE TABLE [Cars] (
      [ID] int NOT NULL IDENTITY,
      [Brand] nvarchar(max) NULL,
      [Type] nvarchar(max) NULL,
      [DriverID] int NULL,
      CONSTRAINT [PK_Cars] PRIMARY KEY ([ID]),
      CONSTRAINT [FK_Cars_Drivers_DriverID] FOREIGN KEY ([DriverID])
          REFERENCES [Drivers] ([ID]) ON DELETE NO ACTION
  );

  CREATE UNIQUE INDEX [IX_Cars_DriverID] ON [Cars] ([DriverID])
      WHERE [DriverID] IS NOT NULL;


  CREATE UNIQUE INDEX [IX_Drivers_CarID] ON [Drivers] ([CarID])
      WHERE [CarID] IS NOT NULL;

  ALTER TABLE [Drivers] ADD CONSTRAINT [FK_Drivers_Cars_CarID] FOREIGN KEY ([CarID])
      REFERENCES [Cars] ([ID]) ON DELETE NO ACTION;

It creates two nullable foreign keys both indexed by a unique filtered index. Perfect!

But...

EF doesn't see this as a bidirectional one-on-one relationship. And rightly so. The two FKs are just that, two independent FKs. However, in view of data integrity the relationship should be established by both ends: if a driver claims a car (sets driver.CarID), the car should also be attached to the driver (set car.DriverID), otherwise another driver could be connected to it.

When existing car and drivers are coupled a little helper method could be used, for example in Car:

public void SetDriver(Driver driver)
{
    Driver = driver;
    driver.Car = this;
}

However, when both a Car and Driver are created and associated in one process, this is clumsy. EF will throw an InvalidOperationException:

Unable to save changes because a circular dependency was detected in the data to be saved: 'Car [Added] <- Car { 'CarID' } Driver [Added] <- Driver { 'DriverID' } Car [Added]'.

Which means: one of the FKs can be be set at once, but the other one can only be set after saving the data. That requires two SaveChanges calls enclosed by a transaction in a pretty imperative piece of code:

using (var db = new MyContext())
{
    using (var t = db.Database.BeginTransaction())
    {
        var jag = new Car { Brand = "Jaguar", Type = "E" };
        var peter = new Driver { Name = "Peter Sellers", Car = jag };

        db.Drivers.Add(peter);

        db.SaveChanges();

        jag.Driver = peter;

        db.SaveChanges();
        t.Commit();
    }
}

Alternative: junction table

So now the reason why I go to these lengths explaining all this: in my opinion, 0..1 : 0..1 associations should be modeled by a junction table with unique foreign keys:

OntToOneJunction

By using a junction table -

  1. The association can be established in an atomic operation instead of an error-prone operation of setting two foreign keys.
  2. The entities themselves are independent: they don't have foreign keys they don't really need to fulfill their role.

This model can be implemented by this class model:

public class Car
{
    public int ID { get; set; }
    public string Brand { get; set; }
    public string Type { get; set; }
    public CarDriver CarDriver { get; set; }
}

public class Driver
{
    public Driver()
    { }
    public int ID { get; set; }
    public string Name { get; set; }
    public CarDriver CarDriver { get; set; }
}

public class CarDriver
{
    public int CarID { get; set; }
    public Car Car { get; set; }
    public int DriverID { get; set; }
    public virtual Driver Driver { get; set; }
}

And the mapping:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var carDriverEtb = modelBuilder.Entity<CarDriver>();
    carDriverEtb.HasKey(cd => new { cd.CarID, cd.DriverID });
    carDriverEtb.HasIndex(cd => cd.CarID).IsUnique();
    carDriverEtb.HasIndex(cd => cd.DriverID).IsUnique();
}

Now creating drivers and cars and their associations can easily be done in one SaveChanges call:

using (var db = new MyContext(connectionString))
{
    var ford = new Car { Brand = "Ford", Type = "Mustang" };
    var jag = new Car { Brand = "Jaguar", Type = "E" };

    var kelly = new Driver { Name = "Kelly Clarkson" };
    var peter = new Driver { Name = "Peter Sellers" };

    db.CarDrivers.Add(new CarDriver { Car = ford, Driver = kelly });
    db.CarDrivers.Add(new CarDriver { Car = jag, Driver = peter });

    db.SaveChanges();
}

The only drawback is that navigting from Car to Driver vv is a bit les convenient. Well, see for yourself which model suit you best.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
  • Isn't making any of the ends "principal" and the other end "dependent" with single optional FK (with `SetNull` cascade option) enough for this type of relationship? The navigation would be simpler compared to explicit join entity, and the order of the CUD operations is defined. – Ivan Stoev Feb 09 '19 at 13:39
  • 1
    @Ivan Ah of course, good point. I still have to get used to the new 1:1 options in EF-core. I'll see into it. My main point is that a mutually optional 1:1 relation always seem a bit weird to me business-wise, because neither of the involved entities has a clear ownership or responsibility of establishing it (otherwise there would be an obvious principal). I think in reality such associations are always a third party's responsibility, f.e. a rental contract with begin and end date etc.), i.e. as an entity in it own right that, in fact, *is* the association. – Gert Arnold Feb 09 '19 at 21:39
0

In EFCore 3.1 you can do one to zero relation like this:

public class Car
{
    public int Id { get; set; }

    public int DriverId { get; set; }
        
    public Driver Driver { get; set; }
}

public class Driver
{
    public int Id { get; set; }

    public Car Driver { get; set; }
}

in your dbContext

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Car>()
        .HasOne(x => x.Driver)
        .WithOne(x => x.Car)
        .HasForeignKey<Car>(x => x.DriverId);
}

Then you can include:

driver from car

dbContext.Driver.Inclide(x => x.Car)...

car from driver

dbContext.Car.Inclide(x => x.Driver)...