Search code examples
c#asp.net-coreentity-framework-core

Ensuring non-nullable navigation in EF Core


I have an entity DataSuggestion which owns Data. Data itself owns Material & Coordinates.

public class DataSuggestion : Entity<Guid>
{
    public Guid DataId { get; init; }
    public Email Email { get; init; }
    public Comment Comment { get; init; }
    public Data Data { get; init; }

    public DataSuggestion(Email email, 
        Comment comment, 
        IEnumerable<Coordinate> coordinates, 
        Material material) : this()
    {
        Email = email;
        Comment = comment;

        Data = new Data(coordinates, material);
    }

    private DataSuggestion()
    { }
}

public class Data : ValueObject<Data>
{
    private readonly List<Coordinate> _coordinates;
    public IEnumerable<Coordinate> Coordinates => _coordinates.AsReadOnly();
    public Material Material { get; init; }

    public Data(IEnumerable<Coordinate> coordinates, 
        Material material) : this()
    {
        _coordinates = coordinates.ToList();
        Material = material;
    }

    /// <summary>
    /// EF Core constructor
    /// </summary>
    private Data()
    { }

    protected override IEnumerable<object> GetAttributesToIncludeInEqualityCheck()
    {
        return [_coordinates, Material];
    }
}

The configuration is as follows

builder.OwnsOne(ds => ds.Comment);
builder.OwnsOne(ds => ds.Email);
        
builder.OwnsOne(ds => ds.Data, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.OwnsOne(d => d.Material);
            ownedNavigationBuilder.OwnsMany(d => d.Coordinates);
            ownedNavigationBuilder.Navigation(d => d.Material).IsRequired();
        });

I get an exception:

Entity type 'Data' is an optional dependent using table sharing and containing other dependents without any required non shared property 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 causing nested dependent's values to be lost. 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.

As far as I understand the exception states that if none of Data's properties must not be null. However, I've already marked Material as required (EDIT: by this I mean ownedNavigationBuilder.Navigation(d => d.Material).IsRequired();), and it's not possible to do the same for collections.

Also I'm a bit confused: DataSuggestion owns Comment which contains only string property, but such an exception is not thrown.

I've tried to mark whole Data as required but to no avail.

The questions are:

  1. Why should we mark all properties as required and not just some of them?
  2. How to deal with the exception if I can't mark Coordinates as required?

Thank you!

EDIT: I've found out that the order of instructions matter and if I put

uilder.Navigation(ds => ds.Data).IsRequired();

After

builder.OwnsOne(ds => ds.Data, ownedNavigationBuilder =>
    {
        ownedNavigationBuilder.OwnsOne(d => d.Material);
        ownedNavigationBuilder.Navigation(d => d.Material).IsRequired();
        ownedNavigationBuilder.OwnsMany(d => d.Coordinates);
    });

Then it works, but still why isn't this enough?

ownedNavigationBuilder.Navigation(d => d.Material).IsRequired();

Solution

  • Material and Coordinates aren't properties but Owned Types/Collections, and if Data itself does not contain any non-null properties, then no instance of Data will be created, regardless of Material values.

    I've annotated the exception message to help explain it.

    Entity type 'Data' is an optional dependent (you didn't use .IsRequired() on Data itself) using table sharing and containing other dependents (Material and Coordinates) without any required non shared property (Data does not contain a property, owned types/collections don't count) to identify whether the entity exists. If all nullable properties (This is the case if you don't have any properties) contain a null value in database then an object instance (of Data) won't be created in the query causing nested dependent's (Material) values to be lost. 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.