← Back to all posts

EF Core: Configurations in .NET

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.

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 DbContext thin

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