Configuring Models in EF Core
EF Core provides three approaches for configuring how your entity classes map to database tables: conventions, data annotations, and the Fluent API. Understanding when to use each approach is key to building maintainable data layers.
The Three Configuration Approaches
1. Conventions
Conventions are automatic mapping rules that EF Core applies by default based on naming patterns and types. You've already been using them throughout this course:
csharp1public class Product2{3 public int Id { get; set; } // Convention: "Id" → primary key4 public string Name { get; set; } = ""; // Convention: non-nullable → required5 public decimal Price { get; set; } // Convention: property name → column name6}
Conventions require zero configuration code. They handle standard cases like primary keys, column names, and nullability.
2. Data Annotations
Data annotations are attributes placed directly on your entity classes. They override conventions for simple scenarios:
csharp1[Table("product_catalog")]2public class Product3{4 public int Id { get; set; }56 [Required]7 [MaxLength(200)]8 public string Name { get; set; } = "";910 [Column(TypeName = "decimal(10,2)")]11 public decimal Price { get; set; }12}
Annotations are visible right next to the property they configure, which makes them easy to discover. However, they mix data access concerns into your domain model.
3. Fluent API
The Fluent API is configured in the OnModelCreating method of your DbContext and provides the most powerful configuration options:
csharp1protected override void OnModelCreating(ModelBuilder modelBuilder)2{3 modelBuilder.Entity<Product>(entity =>4 {5 entity.ToTable("product_catalog");6 entity.Property(p => p.Name)7 .IsRequired()8 .HasMaxLength(200);9 entity.Property(p => p.Price)10 .HasColumnType("decimal(10,2)");11 });12}
The Fluent API keeps configuration separate from your entity classes, and it can express things that annotations cannot.
Configuration Precedence
When multiple configuration approaches are applied to the same property, EF Core follows a strict precedence order:
1Fluent API → Overrides everything2Data Annotations → Overrides conventions3Conventions → Default behavior
The Fluent API always wins. If you configure a column as HasMaxLength(100) in the Fluent API and [MaxLength(200)] on the property, the Fluent API value of 100 is used.
When to Use Each Approach
| Approach | Best For | Limitations |
|---|---|---|
| Conventions | Standard mappings that follow naming patterns | Cannot handle custom requirements |
| Data Annotations | Simple overrides visible alongside the property | Limited configuration options; mixes concerns |
| Fluent API | Complex configuration, composite keys, indexes, query filters | Requires separate configuration code |
General Guideline
- Start with conventions for anything that follows standard patterns
- Use annotations when you need simple validations that also apply outside EF Core (e.g., ASP.NET model validation)
- Use Fluent API for database-specific configuration that should not pollute your entity classes
What Only the Fluent API Can Do
Several configuration options are only available through the Fluent API:
- Composite primary keys -
HasKey(e => new { e.Key1, e.Key2 }) - Alternate keys -
HasAlternateKey(e => e.Code) - Composite indexes -
HasIndex(e => new { e.Col1, e.Col2 }) - Global query filters -
HasQueryFilter(e => !e.IsDeleted) - Value conversions -
HasConversion<string>() - Owned types -
OwnsOne(e => e.Address) - Table splitting and entity splitting
- Precision and scale for decimals (also available via annotation in EF Core 6+)
These features are the focus of the remaining lessons in this topic.
Organizing Configuration with IEntityTypeConfiguration
As your model grows, placing all Fluent API calls in OnModelCreating becomes unwieldy. EF Core provides the IEntityTypeConfiguration<T> interface to split configuration into separate classes:
csharp1public class ProductConfiguration : IEntityTypeConfiguration<Product>2{3 public void Configure(EntityTypeBuilder<Product> builder)4 {5 builder.ToTable("product_catalog");6 builder.Property(p => p.Name).HasMaxLength(200);7 }8}
You apply these configurations from OnModelCreating:
csharp1protected override void OnModelCreating(ModelBuilder modelBuilder)2{3 modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);4}
This single line discovers and applies all IEntityTypeConfiguration<T> implementations in the assembly.
Summary
- EF Core offers three configuration layers: conventions, data annotations, and Fluent API
- Fluent API has the highest precedence and the most capabilities
- Use
IEntityTypeConfiguration<T>to organize Fluent API configuration into separate classes - The Fluent API is the only way to configure composite keys, query filters, value conversions, and owned types
In the next lessons, you'll learn each of these Fluent API capabilities in detail.