EF Core IEntityTypeConfiguration - Clean Up Your DbContext in .NET

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 toCategory- 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
| Aspect | OnModelCreating (Before) | IEntityTypeConfiguration (After) |
|---|---|---|
| File size | Grows unbounded | One focused file per entity |
| Finding a config | Scroll through hundreds of lines | Open the matching file directly |
| Adding a new entity | Edit the shared DbContext file | Create a new file, zero other changes |
| Merge conflicts | High - everyone edits the same method | Low - each entity has its own file |
| Code review | Hard to review large diffs | Small, focused diffs |
| Team onboarding | Confusing single mega-file | Self-explanatory structure |
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.