EF Core IEntityTypeConfiguration: Clean Up Your DbContext in .NET
How to replace a bloated OnModelCreating method with separate entity configuration classes using IEntityTypeConfiguration<T>
Connect with me:
🚀 Sponsored
Struggling with slow EF Core operations? With ZZZ Projects’ EF Core Extensions, you can 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%.
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.
🎬 Watch the full video here:
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.csWriting 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 toCategory- no need to call.Entity<Category>()anywhereEnum properties use
.HasConversion<string>()to store human-readable values in the databaseSelf-referencing relationships (
Parent/Children) are configured just like any other navigationEach 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:
xml
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0" />
</ItemGroup>Key Takeaways
Move entity configurations out of
OnModelCreatingand into separateIEntityTypeConfiguration<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>inConfigureis 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.
