Alica's dev blog
Nested entities with EF Core's owned entity types

What if you want to use a nested object property inside an entity?

Problem

We have a Release entity that represents a release and has a version. We use a simplified form of semantic versioning and store only Major.Minor.Patch part of the version (e.g. 1.0.0).

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

    public DateTime Date { get; set; }

    public int VersionMajor { get; set; }

    public int VersionMinor { get; set; }

    public int VersionPatch { get; set; }
}

The most frequent case when querying the releases is to order them by the version, which actually means ordering them by the three properties:

var orderedReleases = await dbContext.Releases
    .OrderBy(r => r.VersionMajor)
    .ThenBy(r => r.VersionMinor)
    .ThenBy(r => r.VersionPatch)
    .ToList();

There are two problems with this approach:

1. Repetition:

Every time we want to order the releases in the code, we need to write those OrderBy’s or ThenBy’s.

And what if there is another entity with version? We would need to implement this ordering logic again for this entity.

2. Design:

Those 3 properties aren’t just “classic” properties of release. They don’t make sense without each other and need to be used together. We should rather represent them as an object.

It may lead to a decision to add Version entity (and table) and access it from a navigation property. However, this isn’t ideal – Version isn’t an actual entity, it’s just a bunch of properties that relate to each other.

Solution

The solution is called owned entity types.

(I think the most difficult thing about using it was to find out that it exists. I was trying to find it by googling phrases like “nested objects in ef core”, “ef core object property” etc. until a colleague told me that there is something like “owned entities” that might be useful.)

Move Version into a separate class

So, let’s move those three version properties into a separate object. This solves the “Design” problem mentioned above.

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

    public DateTime Date { get; set; }

    public Version Version { get; set; }
}

public class Version
{
    public int Major { get; set; }

    public int Minor { get; set; }

    public int Patch { get; set; }
}

Our goal is to make Version owned by Release. There are three ways to achieve that.

The first is to configure it in OnModelCreating method in DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Release>().OwnsOne(r => r.Version);
}

The second is to annotate the Version property with OwnedAttribute:

[Owned]
public Version Version { get; set; }

And the third one is to annotate the Version type. The documentation says that all references to this type will be configured as owned entity types.

[Owned]
public class Version { ... }

And what will EF Core do? It will flatten the nested structure into one table with the following columns:

  • Id
  • Date
  • Version_Major
  • Version_Minor
  • Version_Patch

The default pattern for column names (Navigation_OwnedEntityProperty) can be changed in the configuration.

Create a reusable extension method for Release

This partly solves the “Repetition” problem mentioned above. Instead of ordering by each element of the version every time, we create an extension method for that:

public static IOrderedQueryable<Release> OrderByVersion(this IQueryable<Release> releases)
{
    return releases
        .OrderBy(r => r.Version.Major)
        .ThenBy(r => r.Version.Minor)
        .ThenBy(r => r.Version.Patch);
}

That makes the code shorter and more readable.

var orderedReleases = dbContext.Releases
	.OrderByVersion()
	.ToList();

Use Version in other entities as well

If we have another entity with Version property, we of course don’t want to implement the ordering logic twice. So, let’s make OrderByVersion generic.

The first attempt could be to add a selector delegate which gets the Version property:

public static IOrderedQueryable<T> OrderByVersion<T>(this IQueryable<T> items, Func<T, Version> selector)
{
    return items
        .OrderBy(x => selector(x).Major)
        .ThenBy(x => selector(x).Minor)
        .ThenBy(x => selector(x).Patch);
}

Example of usage:

var orderedReleases = dbContext.Releases
	.OrderByVersion(r => r.Version)
	.ToList();

Unfortunately, that won’t work. The code will compile, but you will get a runtime error where EF Core complains that it can’t translate this expression into SQL.

The second (and also working) way is to create an interface which all entities with Version property implement.

(Some languages, e.g. Scala or Rust, use the term trait to name a construct similar to interface. I am mentioning that because in our case, calling IEntityWithVersion a trait would probably sound more intuitive.)

public interface IEntityWithVersion
{
    Version Version { get; set; }
}

Then, we say that T in our ordering method must implement this interface:

public static IOrderedQueryable<T> OrderByVersion<T>(this IQueryable<T> items) where T : IEntityWithVersion
{
    return items
        .OrderBy(x => x.Version.Major)
        .ThenBy(x => x.Version.Minor)
        .ThenBy(x => x.Version.Patch);
}

And successfully use it:

var orderedReleases = dbContext.Releases
	.OrderByVersion()
	.ToList();

Happy querying!


Last modified on 2021-01-15