EF Core: Configurations in .NET
Reading Time: 5 minutes
The Problem
Entity Framework Core makes it easy to get started fast. You define entities, add a DbContext, run migrations, and you are productive.
But as the model grows, configuration often becomes messy:
- attributes spread across entity classes
- large
OnModelCreating()methods - inconsistent naming and relationship rules
- persistence concerns leaking into domain classes
At first this feels manageable. Then you open a DbContext with 500 lines of model configuration and realize the setup no longer scales well.
Why EF Core Configurations Matter
EF Core configuration is about making your model explicit.
Good configuration helps you:
- keep entity classes clean
- centralize database mapping rules
- improve maintainability as the model grows
- avoid accidental conventions that do not match your intent
In most production applications, relying only on conventions is rarely enough.
Conventions vs Data Annotations vs Fluent API
EF Core supports three common ways to define model behavior.
1. Conventions
EF Core will infer many things automatically:
- primary keys like
Id - required vs optional relationships
- table and column shapes
Conventions are useful, but they should not be your whole strategy.
2. Data Annotations
public class Product
{
public int Id { get; set; }
[MaxLength(200)]
public string Name { get; set; } = string.Empty;
}
Data annotations are simple, but they mix persistence rules into your domain/entity types.
3. Fluent API
builder.Entity<Product>(entity =>
{
entity.Property(x => x.Name)
.HasMaxLength(200)
.IsRequired();
});
Fluent API is usually the best fit for larger systems because it is more expressive and keeps mapping logic separate.
The Recommended Pattern: IEntityTypeConfiguration
A clean approach is to move configuration into dedicated classes.
Entity
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public DateTime CreatedOnUtc { get; set; }
}
Configuration class
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Products");
builder.HasKey(x => x.Id);
builder.Property(x => x.Name)
.HasMaxLength(200)
.IsRequired();
builder.Property(x => x.Price)
.HasPrecision(18, 2);
builder.Property(x => x.CreatedOnUtc)
.IsRequired();
}
}
Apply from DbContext
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}
}
This pattern keeps DbContext small and lets each entity own a focused mapping class.
What Should Be Configured Explicitly?
In real systems, these are worth configuring intentionally.
Table names
builder.ToTable("Orders");
Keys
builder.HasKey(x => x.Id);
Required fields and lengths
builder.Property(x => x.Email)
.HasMaxLength(320)
.IsRequired();
Precision for decimals
builder.Property(x => x.Total)
.HasPrecision(18, 2);
Indexes
builder.HasIndex(x => x.Email)
.IsUnique();
Relationships
builder.HasOne(x => x.Customer)
.WithMany(x => x.Orders)
.HasForeignKey(x => x.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
Value conversions
builder.Property(x => x.Status)
.HasConversion<string>()
.HasMaxLength(50);
These rules are too important to leave vague.
Example: Order and OrderItem Configuration
Entities
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public DateTime CreatedOnUtc { get; set; }
public ICollection<OrderItem> Items { get; set; } = new List<OrderItem>();
}
public class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
public string Sku { get; set; } = string.Empty;
public int Quantity { get; set; }
public Order Order { get; set; } = null!;
}
Configuration
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(x => x.Id);
builder.Property(x => x.CreatedOnUtc)
.IsRequired();
builder.HasMany(x => x.Items)
.WithOne(x => x.Order)
.HasForeignKey(x => x.OrderId)
.OnDelete(DeleteBehavior.Cascade);
}
}
public class OrderItemConfiguration : IEntityTypeConfiguration<OrderItem>
{
public void Configure(EntityTypeBuilder<OrderItem> builder)
{
builder.ToTable("OrderItems");
builder.HasKey(x => x.Id);
builder.Property(x => x.Sku)
.HasMaxLength(100)
.IsRequired();
builder.Property(x => x.Quantity)
.IsRequired();
}
}
This structure scales much better than keeping everything in OnModelCreating().
Why This Is Better Than Attributes Everywhere
Attributes can be fine for very small apps, but configuration classes have important advantages:
- persistence rules stay out of domain classes
- more expressive mapping options
- easier to find and review mapping rules
- better separation of concerns
This matters more as your domain becomes more complex.
Organizing Configuration Files
A practical project structure:
Data/
AppDbContext.cs
Configurations/
ProductConfiguration.cs
OrderConfiguration.cs
OrderItemConfiguration.cs
This makes configuration discoverable and keeps the data layer predictable.
Common Mistakes
1. Huge OnModelCreating methods
If all mappings are in one place, the context becomes hard to maintain.
2. Relying too much on conventions
Conventions are helpful defaults, not a substitute for intentional design.
3. Mixing domain logic with persistence concerns
Entities should not become persistence-attribute containers unless the simplicity tradeoff is worth it.
4. Forgetting indexes and delete behavior
These are not small details. They materially affect data integrity and performance.
5. Not configuring decimal precision
This is a frequent production bug source for money-related fields.
Practical Rule of Thumb
For anything beyond a trivial prototype:
- use Fluent API
- prefer
IEntityTypeConfiguration<T> - auto-apply configurations from the assembly
- keep
DbContextthin
This gives you a clean default architecture for EF Core mapping.
Summary
EF Core configurations are one of the easiest ways to improve the maintainability of your data access layer.
A strong approach is:
- conventions for basic defaults
- Fluent API for explicit control
- dedicated configuration classes for scale
If your DbContext is growing or your entities are filling up with persistence attributes, moving to configuration classes is usually the right next step.
References
- EF Core model building: https://learn.microsoft.com/ef/core/modeling/
IEntityTypeConfiguration<T>API: https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.ientitytypeconfiguration-1ApplyConfigurationsFromAssemblyAPI: https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.modelbuilder.applyconfigurationsfromassembly