Advanced Entity Framework Core Tips In Practice: DbContext separation, Fluent API and Entities configuration

During learning, you will find many tutorials on how to work with the Entity Framework Core and many basic examples which will not let you know how to use this ORM in the production codebase efficiently. The examples are in most of the cases simplified for students to be able to easily understand how the system works omitting many practical aspects. Knowing this I decided to cover this gap by writing a series of articles regarding the Entity Framework Core practical aspects. I would like every developer to know about this tool when building apps.

Fluent API

In real-world applications you will be forced to use Fluent API since it has much more functional to configure models, for example, to configure composite keys and indexes, splitting model to multiple tables, or owning the submodel. The main advantage with Fluent API is that all the rules are decoupled from the model so it can be used in different layers without mapping it to new entities. Also, decoupling provides an ability to polymorphic applying the configurations through the base type in case some common ones exist. In this case, there is no point to start configuring your models with the data annotation attributes then continue with fluent API. Every project should have conventions set, from my experience all projects where I was working were using the fluent API to configure models and relations.

DbContext separation

Often I see when people put all the DbContext configurations into one file, then after some time, it becomes giant. This happens due to the DBContext is the main class for Entity Framework Core and everything may be configured from that point. So I would recommend simply use partial classes to split your DbSet entities from the DbContext configurations and entities configurations.

// File with name MiniServiceDbContext.cs
public partial class MiniServiceDbContext
{
    public DbSet<TodoItem> TodoItems { get; set; };
    public DbSet<TodoList> TodoLists { get; set; };
}
// File with name MiniServiceDbContextConfig.cs
public sealed partial class MiniServiceDbContext: DbContext {
    public MiniServiceDbContext(DbContextOptions < MiniServiceDbContext > options): base(options) {
        ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges;
        ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;
    }
    public override Task < int > SaveChangesAsync(CancellationToken ct = new()) {
        if (Database.IsInMemory()) ct.ThrowIfCancellationRequested();
        return base.SaveChangesAsync(ct);
    }
    public override Task < int > SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken ct =
        default) {
        UpdateEntries();
        return base.SaveChangesAsync(acceptAllChangesOnSuccess, ct);
    }
    public override int SaveChanges(bool acceptAllChangesOnSuccess) => SaveChanges();
    public override int SaveChanges() =>
        throw new NotImplementedException("Use async version instead!");
    private void UpdateEntries() {
        // ...
    }
    protected override void OnModelCreating(ModelBuilder builder) {
        // ...
    }
}

Entities configuration

One more thing that should be moved out of DbContext is entities Fluent API configurations. This can be achieved by adding the builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); to the dbContext.OnModelCreating() method. This basically instructs to apply model configurations by scanning the provided assembly.

protected override void OnModelCreating(ModelBuilder builder) {
    builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
    base.OnModelCreating(builder);
}
// File TodoItemConfiguration.cs
public class TodoItemConfiguration: IEntityTypeConfiguration < TodoItem > {
    public void Configure(EntityTypeBuilder < TodoItem > b) {
        b.Property(t => t.Updated).IsConcurrencyToken();
        b.Property(t => t.Title).HasMaxLength(200).IsRequired();
    }
}
// File TodoListConfiguration.cs
public class TodoListConfiguration: IEntityTypeConfiguration < TodoList > {
    public void Configure(EntityTypeBuilder < TodoList > b) {
        b.Property(t => t.Updated).IsConcurrencyToken();
        b.OwnsOne(b => b.Colour);
        b.Property(t => t.Title).HasMaxLength(200).IsRequired();
    }
}

In real-world scenarios, we should have global configurations which can still stay in the method OnModelCreating.

protected override void OnModelCreating(ModelBuilder builder) {
    builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
    base.OnModelCreating(builder);
    // Set Decimal Precision
    var decimals = builder.Model.GetEntityTypes().SelectMany(t => t.GetProperties()).Where(p => p.ClrType == typeof(decimal));
    const string DecimalConfig = "decimal(28, 15)";
    foreach(var property in decimals)
    property.SetColumnType(DecimalConfig);
}

Please comment with your suggestions on these topics and upvote for me to continue covering advanced usage of Entity Framework Core.