Loading of nested entities

549 Views Asked by At

I have an issue on nested entities and some strange behaviour in Entity FrameWork Core.

I'm supposed to load an entity Meeting from a database table. Meeting has multiple properties but only Location causes me troubles. Location store a - location ;-) Meeting is simplied below. Location is much simpler, it contains a name, description, GPSCoordinates and an address. See below Address holds an address, though the class have more properties it is simplified below.

When retrieving a Meeting from the Db everything are mostly fine. Except when the rare occassion occur of Name, Description and GPSCoordinates being all being null, then regardless of any value stored in Address, Location is null.

If I change Name from Null to an empty string "", then no problem, everyhing loads as expected.

All valus of Meeting, Location and Address is stored in flattend database table. The table is configured using Fluent Api. Another class Course inherits Meeting and are stored in the same table - thus the decriminator - but it should have no impact.

The code below is reduced for simplicity

public class Meeting : BaseEntity<long>
{
    public string Name { get; set; }
    public Location Location { get; set; }
}

public class Location
{
    public string Name { get; set; }
    public string Description { get; set; }
    public PostalAddress Address { get; set; }
    public Point GpsCoordinates { get; set; }
}

public class PostalAddress
{
    public string StreetAddress1 { get; set; }
    public string ZipCode { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

Meeting meet = context.meetDbSet.Include(p=>p.Location).ThenInclude(p=>p.Address).FirstOrDefault();


public void Configure(EntityTypeBuilder<Meeting> builder)
{
    builder
        .ToTable("Meetings")
        .HasKey(p => p.Id);

    builder
        .HasDiscriminator<string>("Meeting_Descriminator")
        .HasValue<Meeting>("")
        .HasValue<Course>("Course");

    builder
        .OwnsOne(p => p.Location, location =>
            {
                location
                    .Property(p => p.Name)
                    .HasColumnType("nvarchar(max)")
                    .IsRequired(false);

                location
                    .Property(p => p.Description)
                    .HasColumnType("nvarchar(max)")
                    .IsRequired(false);

                location
                    .OwnsOne(p => p.Address, postaladdress =>
                        {
                            postaladdress
                                .Property(p => p.StreetAddress1)
                                .HasColumnName("StreetAddress1")
                                .HasColumnType("nvarchar(max)")
                                .IsRequired(false);
                            postaladdress
                                .Property(p => p.ZipCode)
                                .HasColumnType("nvarchar(max)")
                                .IsRequired(false);
                            postaladdress
                                .Property(p => p.City)
                                .HasColumnName("City")
                                .HasColumnType("nvarchar(max)")
                                .IsRequired(false);
                            postaladdress
                                .Property(p => p.Country)
                                .HasColumnName("Country")
                                .HasColumnType("nvarchar(max)")
                                .IsRequired(false);
                        });

                    location
                        .Property(p => p.GpsCoordinates)
                        .HasColumnType("geography")
                        .IsRequired(false);
            });
}
2

There are 2 best solutions below

0
On BEST ANSWER

Finally I found a solution. It turns out it is an issue in EF Core, but adding a navigation property on location and making the property required did the trick.

Notice it must be configured after the owned type it self.

public void Configure(EntityTypeBuilder<Meeting> builder)
{

builder
    .ToTable("Meetings")
    .HasKey(p => p.Id);

builder
    .HasDiscriminator<string>("Meeting_Descriminator")
    .HasValue<Meeting>("")
    .HasValue<Course>("Course");

builder
    .OwnsOne(p => p.Location, location =>
        {
            location
                .Property(p => p.Name)
                .HasColumnType("nvarchar(max)")
                .IsRequired(false);

            location
                .Property(p => p.Description)
                .HasColumnType("nvarchar(max)")
                .IsRequired(false);

            location
                .OwnsOne(p => p.Address, postaladdress =>
                    {
                        postaladdress
                            .Property(p => p.StreetAddress1)
                            .HasColumnName("StreetAddress1")
                            .HasColumnType("nvarchar(max)")
                            .IsRequired(false);
                        postaladdress
                            .Property(p => p.ZipCode)
                            .HasColumnType("nvarchar(max)")
                            .IsRequired(false);
                        postaladdress
                            .Property(p => p.City)
                            .HasColumnName("City")
                            .HasColumnType("nvarchar(max)")
                            .IsRequired(false);
                        postaladdress
                            .Property(p => p.Country)
                            .HasColumnName("Country")
                            .HasColumnType("nvarchar(max)")
                            .IsRequired(false);
                    });

                location
                    .Property(p => p.GpsCoordinates)
                    .HasColumnType("geography")
                    .IsRequired(false);
        });
builder
    .Navigation(p => p.Location)
    .IsRequired(true);
}

And something more. While searching for a solution I also learned Include is redundant on owned types. :-)

1
On

I think it could be cause you are using owned types. With owned types the key of the type is defined as the combination of its properties. When all properties are null, a key cannot be defined, thus the address also won't be loaded. But when using owned types, this behaviour sounds logical. When something is owned, you assume that it does not exist when the parent property does not exist. If that's not the case, you should change the structure.