Remigiusz ZalewskiRemigiusz Zalewski

EF Core IEntityTypeConfiguration - Clean Up Your DbContext in .NET

ef-core-ientitytypeconfiguration

ZZZ Projects' EF Core Extensions Struggling with slow EF Core operations? Boost performance like never before. Experience up to 14× faster Bulk Insert, Update, Delete, and Merge - and cut your save time by as much as 94%. Learn more →

Introduction

You open the DbContext file to tweak a max length on one property. You scroll. And scroll. And scroll some more. By line 400, you're not sure if you're still looking at the right entity.

This is the reality in many enterprise .NET codebases. The OnModelCreating method becomes a graveyard of entity configurations, growing quietly until nobody wants to touch it. It works, but it does not scale - and it makes onboarding, debugging, and code reviews unnecessarily painful.

EF Core has a built-in solution for this: IEntityTypeConfiguration<T>. One class per entity, one file per configuration, and a single line in OnModelCreating to wire it all up.


The Problem: Everything in OnModelCreating

Here is what the bloated approach looks like. All your entity configurations land in one method:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>(builder =>
    {
        builder.HasKey(c => c.Id);

        builder
            .Property(c => c.Id)
            .HasDefaultValueSql("NEWSEQUENTIALID()");

        builder
            .Property(c => c.Name)
            .IsRequired()
            .HasMaxLength(200);

        builder
            .Property(c => c.Email)
            .IsRequired()
            .HasMaxLength(300);

        // ... dozens more lines per entity
    });

    // Then Product, Order, OrderLine, Category... all stacked here
}

Key problems with this approach:

  • Finding a specific entity config requires scrolling through hundreds of lines
  • Merge conflicts are frequent since everyone edits the same method
  • No clear ownership - all entity configs are mixed together
  • Adding a new entity means touching a file that already has too much responsibility

The Solution: IEntityTypeConfiguration<T>

EF Core ships with an interface specifically designed for this: IEntityTypeConfiguration<T>. You create one class per entity, implement the Configure method, and move all configuration there.

Project Structure

Start by creating a dedicated directory for your configurations:

YourProject/
  infrastructure/
    EntityConfigurations/
      CategoryConfiguration.cs
      CustomerConfiguration.cs
      OrderConfiguration.cs
      ProductConfiguration.cs
    AppDbContext.cs

Writing a Configuration Class

Here is a real-world example using a Category entity with multiple property constraints, an enum conversion, and a self-referencing relationship:

public class CategoryConfiguration : IEntityTypeConfiguration<Category>
{
    public void Configure(EntityTypeBuilder<Category> builder)
    {
        builder
            .HasKey(c => c.Id);

        builder
            .Property(c => c.Id)
            .HasDefaultValueSql("NEWSEQUENTIALID()");

        builder
            .Property(c => c.Name)
            .IsRequired()
            .HasMaxLength(200);

        builder
            .Property(c => c.Slug)
            .IsRequired()
            .HasMaxLength(200);

        builder
            .HasIndex(c => c.Slug)
            .IsUnique();

        builder
            .Property(c => c.Description)
            .HasMaxLength(1000);

        builder
            .Property(c => c.MetaTitle)
            .HasMaxLength(200);

        builder
            .Property(c => c.MetaDescription)
            .HasMaxLength(500);

        builder
            .Property(c => c.ImageUrl)
            .HasMaxLength(500);

        builder
            .Property(c => c.IconCode)
            .HasMaxLength(50);

        builder
            .Property(c => c.ColorHex)
            .HasMaxLength(10);

        builder
            .Property(c => c.ExternalId)
            .HasMaxLength(100);

        builder
            .Property(c => c.CreatedBy)
            .HasMaxLength(100);

        builder
            .Property(c => c.UpdatedBy)
            .HasMaxLength(100);

        builder
            .Property(c => c.Status)
            .HasConversion<string>()
            .HasMaxLength(30);

        builder
            .Property(c => c.Visibility)
            .HasConversion<string>()
            .HasMaxLength(30);

        builder
            .HasOne(c => c.Parent)
            .WithMany(c => c.Children)
            .HasForeignKey(c => c.ParentId)
            .OnDelete(DeleteBehavior.Restrict);
    }
}

Key points:

  • The EntityTypeBuilder<Category> parameter is already scoped to Category - no need to call .Entity<Category>() anywhere
  • Enum properties use .HasConversion<string>() to store human-readable values in the database
  • Self-referencing relationships (Parent/Children) are configured just like any other navigation
  • Each configuration class has a single, obvious responsibility

Registering All Configurations with One Line

Now the clean part. Instead of calling each configuration manually, use ApplyConfigurationsFromAssembly. EF Core will scan the assembly, find every class implementing IEntityTypeConfiguration<T>, and apply them all:

public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Product> Products => Set<Product>();
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderLine> OrderLines => Set<OrderLine>();
    public DbSet<Category> Categories => Set<Category>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
    }
}

OnModelCreating is now one line. Any new configuration class you add to the assembly is picked up automatically - no manual registration needed.

Running Migrations

Creating a migration works the same as always. If your DbContext lives in a separate infrastructure project:

dotnet ef migrations add Init -s <API_PROJECT_PATH> -p <WHERE_DBCONTEXT_IS_LOCATED_PROJECT_PATH>

For a single-project setup:

dotnet ef migrations add Init -p <API_PROJECT_PATH>

The generated migration will contain all your constraints, primary keys, foreign keys, indexes, and max lengths - exactly as if you had written them directly in OnModelCreating.

NuGet Packages

Make sure you have these in your .csproj:

<ItemGroup>
  <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0" />
</ItemGroup>

Before vs After

AspectOnModelCreating (Before)IEntityTypeConfiguration (After)
File sizeGrows unboundedOne focused file per entity
Finding a configScroll through hundreds of linesOpen the matching file directly
Adding a new entityEdit the shared DbContext fileCreate a new file, zero other changes
Merge conflictsHigh - everyone edits the same methodLow - each entity has its own file
Code reviewHard to review large diffsSmall, focused diffs
Team onboardingConfusing single mega-fileSelf-explanatory structure

Key Takeaways

  • Move entity configurations out of OnModelCreating and into separate IEntityTypeConfiguration<T> classes - one class per entity.
  • Use modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly) to register all configurations with a single line.
  • New configuration classes are picked up automatically by assembly scanning - no manual wiring required.
  • The EntityTypeBuilder<T> in Configure is already scoped to your entity type, so your configuration code becomes cleaner and more focused.
  • This pattern makes it trivial to find, edit, and review entity configurations in a team setting.