Optional One to One Relationships in EFCore

My company is currently working on replacing our venerable WinForms application with a shinier ASP.Net MVC version and rather than just re-write everything as a slavish “lift and shift” approach, we’ve gone for the bolder approach of completely re-designing the whole thing from top to bottom. Cue buzzwords such as “Greenfield” and “Blue-sky thinking” etc etc.

As part of this, we made an early decision to move to an Entity Framework Code First approach and try to keep as close to the cutting edge of technology as possible, so whilst we started with EF Core 3.1, we’re currently on EF Core 5. As someone who has been mucking about with SQL Databases since the 90s, it’s hard to let go of the SQL and just let EF manage and control everything, but once you embrace the mindset and just let it go, the benefits have been substantial.

However, one thing that has been niggling at me is managing 1:1 relationships and especially optional ones. It wasn’t long before we designed our first 1:1 relationships and instantly hit some problems.

Let’s look at the below diagram. Here we have Requirements which have an optional 1:1 (aka zero or one) relationship with a set of Features. When a Product is created from these requirements, it will also have a set of Features, nothing complicated here, all we’re doing is putting the set of common fields in a separate table, if we happen to have them.

Sample ER Diagram

So the first thing we did here was try to add a simple navigation property to Requirements, Products and Features and hoped EFCore could work this out by magic; something like:

public virtual FeatureEntity Features { get; set; }

Your migration will generate fine but as soon as you try to apply it and code against it, you’ll see this fun error:

The dependent side could not be determined for the one-to-one relationship between ….

Which makes perfect sense if you think about it, how on earth should we expect EF to work this out? Don’t get me wrong here, EF is about as close to computer witchcraft as I’ve seen in a long time, but you’ve got to give it a helping hand in places. Unfortunately this error leads you down the path of trying to use the fluent API to try and configure the relationship by hand, but I very much disagree here, the fluent API should only be used when you can’t express what you’re trying to achieve via Data Annotations.

What we actually want to do here is configure the foreign key properties in the dependant table. So in our FeatureEntity we add our navigation properties to Requirement and Product but we additionally add RequirementId and ProductId.

Because I’m following a naming convention of {TableName}Id, EF can work everything else out itself. No need for a ForreignKey data annotation.

[Table("Features")]
public class FeatureEntity
{
    [Key]
    public Guid Id { get; set; }

    public Guid? RequirementId { get; set; }
    public Guid? ProductId { get; set; }

    public virtual RequirementEntity Requirement { get; set; }
    public virtual ProductEntity Product { get; set; }

}

After this all you have to do is add your Navigation properties to Requirement and Product (nothing extra required here) and EF will magic together the rest for you.

The final point of note here is the optionality of the relationship. As you can see above, I’ve declared that both of these FKs are optional by using ? after the type. It’s as simple as that.

If you review your migration and ContextModelSnapshot, you’ll see that the FKs will all be unique and indexed and EF has worked out the details for you using the HasOne()/WithOne()/HasForeignKey() structure for you.

No custom OnModelCreating, No Fluent API. Simples.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: