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