Post

Owned Entity Types: Ensuring Non-Nullable Navigation in EF Core

In this article I will explore the possibility of a subtle bug you may face in your application, if you’re using owned entity types and you have incorrect EF configuration. Let’s start with an example directly. We have the following models and the corresponding EF configuration. To demonstrate the issue more clearly I disabled the NRT (nullable reference types) in my project. Later, we’ll discuss how EF Core can automatically infer our model more accurately if we use NRT.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

public class Customer
{
    public int Id { get; private set; }
    public string Name { get; set; }
    public Address Address { get; private set; }

    private Customer()
    {
        // Required by EF
    }
    public Customer(Address address)
    {
        if (address == null) throw new ArgumentNullException(nameof(address));
        Address = address;
    }
}
1
2
3
4
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
	modelBuilder.Entity<Customer>().OwnsOne(x => x.Address);
}

In this example, we have a Customer entity which contains an owned type Address. Based on the requirements we have received, the Customer.Name, Address.Street and Address.City properties may contain null values. This will correctly be reflected in the generated table, and the respective database columns will be marked as nullable. But, while designing our model, we have ensured that the Address in Customer should never be null. The state in Address may be null, but the navigation not. We may have various reasons why we would want that, but that’s out of scope here. We’re happy with the model, and we’re confident that we did a good job reflecting the requirements correctly in the code. The first hint that something is wrong is the warning message that is generated by EF while building the model. It’s generated on runtime or if you try to create a migration.

1
The entity type 'Address' is an optional dependent using table sharing without any required non shared property that could be used to identify whether the entity exists. If all nullable properties contain a null value in database then an object instance won't be created in the query. Add a required property to create instances with null values for other properties or mark the incoming navigation as required to always create an instance.

The message is clear, but yet a bit confusing. What it means is that since our owned type doesn’t have any non-nullable properties, and if all of them are null for a given record; while fetching that record the navigation itself will be null. The assumption we made that Address will never be null is not correct. We never told to EF that it’s required. The issue is easily replicated as shown in the code below. If we fetch the record from the database, the customer2 will have null value for Address.

1
2
3
4
5
6
7
8
var customer1 = new Customer(new Address { Street = "Street1", City = "City1" });
var customer2 = new Customer(new Address());

dbContext.Customers.AddRange(customer1, customer2);
await dbContext.SaveChangesAsync();

dbContext.ChangeTracker.Clear();
var customers = await dbContext.Customers.ToListAsync();

This can be easily fixed if we’re explicit in the configuration and set the navigation as required.

1
2
3
4
5
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>().OwnsOne(x => x.Address);
    modelBuilder.Entity<Customer>().Navigation(x => x.Address).IsRequired();
}

This may seem as a mundane issue, but in my experience is a very common misconfiguration. If you have NRT enabled in your projects you’ll be conditionally safe. If you have defined the property as non-nullable public Address Address { get; private set; }, EF will be able to deduce your intentions and mark the navigation as required automatically. Anyhow, I think it’s important to understand the underlying issue. Moreover, in case you have nested owned types, EF no longer will generate a warning, but an InvalidOperationException exception on runtime.

With that in mind, let’s play safe and write an extension method that configure all owned type navigations as required automatically.

1
2
3
4
5
6
7
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>().Navigation(x => x.Address).IsRequired();

    // Call the method after all other configurations
    modelBuilder.ConfigureOwnedTypeNavigationsAsRequired();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static partial class ModelBuilderExtensions
{
    public static void ConfigureOwnedTypeNavigationsAsRequired(this ModelBuilder modelBuilder)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (entityType.IsOwned())
            {
                var ownership = entityType.FindOwnership();

                if (ownership is null) return;

                if (ownership.IsUnique)
                {
                    ownership.IsRequiredDependent = true;
                }
            }
        }
    }
}

I hope you found this article useful. Happy coding!

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.