Mapping related entities to private fields in Entity Framework Core

dotnet entity framework core domain-driven design

When mapping objects to Entity Framework Core, I like to use my domain entities directly. I always make sure my domain entities have no outside dependencies. They just represent the domain logic and are never influenced by the desires of the infrastructure layer.

For example, say you have a domain entity that represents an MLB batter:

public sealed class Batter : Entity
{
    /// <summary>
    /// The player's batting stats by game
    /// </summary>
    private readonly List<PlayerBattingStatsByGame> _battingStatsByGames;

    public int Id { get; }
    public string Name { get; }
}

public sealed class PlayerBattingStatsByGame
{
    public int GameId { get; }
    public int Hits { get; }
    public DateOnly GameDate { get; }
}

In this case, I want the following of my entities:

  • You cannot edit the player’s list of batting stats by game directly, you must go through the domain logic
    • _battingStatsByGames is unchangeable from the outside
    • So that means having a method called PlayedGame(PlayerBattingStatsByGame stats) that adds the stats to the player’s collection _battingStatsByGames
  • The related entity PlayerBattingStatsByGame does not need an ID to reference Batter. This just pollutes the class and the relationship should be inherent in the fact that it is loaded along with Batter

Defining the entity’s domain logic

Say you also want some domain logic that returns the player’s games chronologically. You then can define a property on your entity that returns the collection from _battingStatsByGames.

public sealed class Batter : Entity
{
    /// <summary>
    /// The player's batting stats by game
    /// </summary>
    private readonly List<PlayerBattingStatsByGame> _battingStatsByGames;

    public int Id { get; }
    public string Name { get; }

    /// <summary>
    /// The player's batting stats by game in chronological order
    /// </summary>
    public IReadOnlyList<PlayerBattingStatsByGame> BattingStatsByGamesChronologically =>
        _battingStatsByGames.OrderBy(x => x.GameDate).ToImmutableList(); // Or cache this on every LogBattingGame() call to reduce overhead

    /// <summary>
    /// Logs a game where the player participated in batting
    /// </summary>
    /// <param name="stats">Batting stats for the game participated in</param>
    public void LogBattingGame(PlayerBattingStatsByGame stats)
    {
        _battingStatsByGames.Add(stats);
    }
}

The problem with this is when you introduce Entity Framework Core into the picture, it will try to map your related entities PlayerBattingStatsByGame to the property BattingStatsByGamesChronologically and not the field _battingStatsByGames.

Defining the relationships in the infrastructure layer

You can write your domain entity so that it is completely free of any dependencies aside from .NET itself. The only dependency that Batter needs is using System.Collections.Immutable;.

Entity Framework Core typically wants you to set up entity relationships automagically via conventions, meaning relationships between entities are created through code syntax:

public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; }
    public Blog Blog { get; set; } = null!;
}

In this case, the usage of names like Blog and Post and inferring types, EF Core can build the relationships without further configuration.

But in my case, EF Core allows you to do just a little extra configuration to be able to keep the same relationships but without the coding conventions:

  • You won’t have to add BatterId to PlayerBattingStatsByGame
  • You won’t have to make _battingStatsByGames publicly mutable

Entity configurations

For each object that will be stored in a table, you will implement IEntityTypeConfiguration<T>. For the Batter example, you would have:

public sealed class BatterEntityTypeConfiguration : IEntityTypeConfiguration<Batter>
{
    public void Configure(EntityTypeBuilder<Batter> builder)
    {
        builder.HasKey(e => e.Id);

        var columnOrder = 0;

        builder.Property(e => e.Id)
            .IsRequired()
            .HasColumnName("id")
            .HasColumnOrder(columnOrder++);

        builder.Property(e => e.Name)
            .IsRequired()
            .HasColumnName("name")
            .HasColumnOrder(columnOrder++);
    }
}

public sealed class PlayerBattingStatsByGameEntityTypeConfiguration : IEntityTypeConfiguration<PlayerBattingStatsByGame>
{
    public void Configure(EntityTypeBuilder<PlayerBattingStatsByGame> builder)
    {
        builder.HasKey(["batter_id", "game_id"]);

        var columnOrder = 0;

        builder.Property("batter_id")
            .HasColumnOrder(columnOrder++);

        builder.Property(e => e.GameId)
            .IsRequired()
            .HasColumnName("game_id")
            .HasColumnOrder(columnOrder++);

        builder.Property(e => e.Hits)
            .IsRequired()
            .HasColumnName("hits")
            .HasColumnOrder(columnOrder++);

        builder.Property(e => e.GameDate)
            .IsRequired()
            .HasColumnType("date")
            .HasColumnName("game_date")
            .HasColumnOrder(columnOrder++);
    }
}

Notice that defining both batter_id as part of the key and as a property means that we don’t need a BatterId property polluting our PlayerBattingStatsByGame class. Instead, EF Core will automatically create this field on the table and only use it when it needs to load related entities.

To finish setting up the relationship between Batter and PlayerBattingStatsByGame, you also need to define the kind of relationship. This can be done in two ways:

On the parent entity

In BatterEntityTypeConfiguration.Configure, you would add:

// Tell EF Core to ignore the domain logic property so it won't try to map a table column to it
builder.Ignore(x => x.BattingStatsByGamesChronologically);

// Tell EF Core to map the related entity to the private field "_battingStatsByGames"
builder.HasMany<PlayerBattingStatsByGame>("_battingStatsByGames")
    .WithOne()
    .HasForeignKey("batter_id")
    .IsRequired();

This tells the parent entity to set up the relationship on batter_id.

You can instead define the mapping on PlayerBattingStatsByGameEntityTypeConfiguration. Define it before calling HasKey().

builder.HasOne<Batter>()
    .WithMany("_battingStatsByGames")
    .HasForeignKey("batter_id");

builder.HasKey(["batter_id", "game_id"]);

Loading related entities while querying

In your DBContext, where you have the DbSet<Batter> defined, you can add a method that loads Batters and their PlayerBattingStatsByGame:

public sealed class BattersDbContext : DbContext
{
    public BattersDbContext(DbContextOptions<BattersDbContext> options) : base(options)
    {
    }

    public DbSet<Batter> Batters { get; private init; } = null!;

    public IQueryable<Batter> BattersWithGames()
    {
        return Batters.Include("_battingStatsByGames");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.HasDefaultSchema(Constants.Schema);
        modelBuilder.ApplyConfiguration(new BatterEntityTypeConfiguration());
        modelBuilder.ApplyConfiguration(new PlayerBattingStatsByGameEntityTypeConfiguration());
    }

Now you can call DbContext.BattersWithGames() to return your Batters with the _battingStatsByGames private field fully populated.

Gotchas

Reconstructing objects when reading

EF Core requires that there be a constructor that either exposes the properties and fields that are mapped to columns, or has the constructor do the setting itself.

This means that Batter would need the following constructor:

private Batter(int id, string name) {
    Id = id;
    Name = name;
    _battingStatsByGames = new List<PlayerBattingStatsByGame>();
}

Id and Name are exposed via the constructor but _battingStatsByGames is defined manually. EF Core can populate it (I believe through reflection).

Caching the domain logic

Note that when you invoke the BattingStatsByGamesChronologically property, you are performing a sort on each call.

public IReadOnlyList<PlayerBattingStatsByGame> BattingStatsByGamesChronologically =>
    _battingStatsByGames.OrderBy(x => x.GameDate).ToImmutableList(); // Or cache this on every LogBattingGame() call to reduce overhead

You can cache this by flagging if the collection has changed and only sorting then:

public sealed class Batter : Entity
{
    private readonly bool _hasChanged = false;
    private IReadOnlyList<PlayerBattingStatsByGame> _sorted;

    ...

    private Batter(int id, string name) {
        Id = id;
        Name = name;
        _battingStatsByGames = new List<PlayerBattingStatsByGame>();
        _hasChanged = true; // Sort for the first time after loading
    }

    public void LogBattingGame(PlayerBattingStatsByGame stats)
    {
        _battingStatsByGames.Add(stats);
        _hasChanged = true;
    }

    public IReadOnlyList<PlayerBattingStatsByGame> BattingStatsByGamesChronologically
    {
        get
        {
            if (_sorted == null || hasChanged)
            {
                _sorted = _battingStatsByGames.OrderBy(x => x.GameDate).ToImmutableList();
            }

            return _sorted;
        }
    }
}