Entity configuration maps C# classes to database tables with full control over table names, column names, data types, constraints, and indexes. ASP.NET Core EF Core supports two configuration styles: Data Annotations (attributes on entity classes) for simple cases, and the Fluent API (code in OnModelCreating or IEntityTypeConfiguration) for complex mappings. The Fluent API is more powerful and keeps entity classes clean — annotations mix persistence concerns into the domain model.
Fluent API Entity Configuration
// ── Post entity ───────────────────────────────────────────────────────────
public class Post
{
public int Id { get; private set; }
public string Title { get; private set; } = string.Empty;
public string Slug { get; private set; } = string.Empty;
public string Body { get; private set; } = string.Empty;
public string? Excerpt { get; private set; }
public bool IsPublished { get; private set; }
public DateTime? PublishedAt { get; private set; }
public DateTime CreatedAt { get; private set; }
public string AuthorId { get; private set; } = string.Empty;
// Navigation properties
public User? Author { get; private set; }
public ICollection<Tag> Tags { get; private set; } = [];
public ICollection<Comment> Comments { get; private set; } = [];
private Post() { } // required by EF Core for materialisation
public static Post Create(string title, string slug, string body, string authorId) => new()
{
Title = title, Slug = slug, Body = body, AuthorId = authorId,
CreatedAt = DateTime.UtcNow,
};
}
// ── IEntityTypeConfiguration — separate config class per entity ───────────
public class PostConfiguration : IEntityTypeConfiguration<Post>
{
public void Configure(EntityTypeBuilder<Post> entity)
{
entity.ToTable("Posts");
entity.HasKey(p => p.Id);
entity.Property(p => p.Title)
.IsRequired()
.HasMaxLength(200);
entity.Property(p => p.Slug)
.IsRequired()
.HasMaxLength(100);
entity.Property(p => p.Body)
.IsRequired()
.HasColumnType("nvarchar(max)");
entity.Property(p => p.Excerpt)
.HasMaxLength(500);
entity.Property(p => p.CreatedAt)
.HasDefaultValueSql("GETUTCDATE()");
// Unique index on Slug
entity.HasIndex(p => p.Slug)
.IsUnique()
.HasDatabaseName("IX_Posts_Slug");
// Composite index for common query pattern
entity.HasIndex(p => new { p.IsPublished, p.PublishedAt })
.HasDatabaseName("IX_Posts_Published_PublishedAt");
// Enum conversion — store PostStatus as string in database
entity.Property(p => p.Status)
.HasConversion<string>()
.HasMaxLength(20);
}
}
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly) in OnModelCreating automatically discovers and applies all IEntityTypeConfiguration<T> classes in the assembly. This means when you add a new entity and its configuration class, the configuration is picked up without any manual registration. This pattern scales well in large applications — each entity has its own configuration file rather than a single massive OnModelCreating method.Post.Create()) instead of public constructors. EF Core needs to materialise entities from database rows and uses the parameterless constructor for this. Without it, EF Core throws a runtime exception when loading entities. Mark the constructor private (not public) to prevent accidental direct instantiation in application code.Post entity with [MaxLength(200)] on Title mixes EF Core semantics into a class that should be pure domain logic. Use the Fluent API in IEntityTypeConfiguration to keep entity classes clean. Domain entities should only contain domain logic; persistence mapping belongs in the infrastructure layer.Value Conversions
// ── Enum to string conversion ─────────────────────────────────────────────
public enum PostStatus { Draft, Review, Published, Archived }
entity.Property(p => p.Status)
.HasConversion<string>() // stores "Draft", "Published" etc. in DB
.HasMaxLength(20);
// ── Custom value object conversion ────────────────────────────────────────
public record Money(decimal Amount, string Currency);
entity.Property(p => p.Price)
.HasConversion(
v => $"{v.Amount}:{v.Currency}", // to DB: "9.99:USD"
v => new Money(decimal.Parse(v.Split(':')[0]), v.Split(':')[1])); // from DB
// ── Owned entity type (value object stored in same table) ─────────────────
public class Address { public string Street { get; set; } = ""; public string City { get; set; } = ""; }
entity.OwnsOne(u => u.Address, a =>
{
a.Property(x => x.Street).HasColumnName("Address_Street").HasMaxLength(200);
a.Property(x => x.City).HasColumnName("Address_City").HasMaxLength(100);
});
Common Mistakes
Mistake 1 — Forgetting private parameterless constructor (entity materialisation fails)
❌ Wrong — entity with only public constructor with parameters; EF Core throws at runtime.
✅ Correct — add private Post() { } to any entity with factory methods instead of public constructors.
Mistake 2 — Not adding indexes for frequently queried columns (slow queries at scale)
❌ Wrong — querying by Slug without an index; table scan on every request.
✅ Correct — add HasIndex(p => p.Slug).IsUnique() for all columns used in WHERE clauses.