Entity Configuration: Fluent API vs Data Annotations
Overview
EF Core provides two ways to configure entities: Data Annotations (attributes) and Fluent API. Understanding when to use each is crucial for maintainable code.
Data Annotations
Basic Configuration
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
[Table("Products")]
public class Product
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
[StringLength(500)]
public string Description { get; set; }
[NotMapped]
public string DisplayName => $"{Name} - ${Price}";
}
Relationship Configuration
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
[ForeignKey("Blog")]
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
Pros and Cons
Pros:
- Simple and concise
- Configuration lives with entity
- Easy to read for simple scenarios
Cons:
- Limited functionality
- Couples entity to EF Core
- Can't configure some advanced features
- Makes entity classes messy
Fluent API
Basic Configuration
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.ToTable("Products");
entity.HasKey(e => e.Id);
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.Price)
.HasColumnType("decimal(18,2)");
entity.Property(e => e.Description)
.HasMaxLength(500);
entity.Ignore(e => e.DisplayName);
});
}
}
Relationship Configuration
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId)
.OnDelete(DeleteBehavior.Cascade);
}
Pros and Cons
Pros:
- Complete control over configuration
- Keeps entities clean (POCOs)
- Can configure everything
- Better separation of concerns
Cons:
- More verbose
- Configuration separated from entity
- Can become large in OnModelCreating
IEntityTypeConfiguration (Best Practice)
Organized Configuration
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Products");
builder.HasKey(p => p.Id);
builder.Property(p => p.Name)
.IsRequired()
.HasMaxLength(100);
builder.Property(p => p.Price)
.HasColumnType("decimal(18,2)")
.IsRequired();
builder.HasIndex(p => p.Name);
builder.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId);
}
}
// DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new ProductConfiguration());
// Or apply all configurations in assembly
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
Folder Structure
Data/
├── Entities/
│ ├── Product.cs
│ ├── Category.cs
│ └── Order.cs
└── Configurations/
├── ProductConfiguration.cs
├── CategoryConfiguration.cs
└── OrderConfiguration.cs
Decision Guide
Use Data Annotations When:
- Simple CRUD applications
- Basic property constraints (Required, MaxLength)
- Validation attributes already needed
- Small entities with minimal configuration
Use Fluent API When:
- Complex relationships
- Advanced features (indexes, filters, computed columns)
- Need separation of concerns
- Enterprise applications
- Multiple configurations per entity
Hybrid Approach (Common)
// Entity with basic validation
public class Product
{
public int Id { get; set; }
[Required] // For model validation
[MaxLength(100)]
public string Name { get; set; }
public decimal Price { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
// Fluent API for advanced configuration
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasIndex(p => p.Name);
builder.Property(p => p.Price)
.HasColumnType("decimal(18,2)");
builder.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId)
.OnDelete(DeleteBehavior.Restrict);
}
}
Advanced Fluent API Features
Composite Keys
modelBuilder.Entity<OrderItem>()
.HasKey(oi => new { oi.OrderId, oi.ProductId });
Computed Columns
modelBuilder.Entity<Person>()
.Property(p => p.FullName)
.HasComputedColumnSql("[FirstName] + ' ' + [LastName]");
Default Values
modelBuilder.Entity<Product>()
.Property(p => p.CreatedDate)
.HasDefaultValueSql("GETUTCDATE()");
modelBuilder.Entity<Product>()
.Property(p => p.IsActive)
.HasDefaultValue(true);
Value Conversions
modelBuilder.Entity<Product>()
.Property(p => p.Status)
.HasConversion<string>(); // Enum to string
modelBuilder.Entity<User>()
.Property(u => u.Email)
.HasConversion(
v => v.ToLowerInvariant(),
v => v);
Table Splitting
modelBuilder.Entity<Product>()
.ToTable("Products");
modelBuilder.Entity<ProductDetails>()
.ToTable("Products");
modelBuilder.Entity<Product>()
.HasOne(p => p.Details)
.WithOne(d => d.Product)
.HasForeignKey<ProductDetails>(d => d.Id);
Interview Questions
Q: What's the difference between Data Annotations and Fluent API?
A: Data Annotations are attributes on entity properties, limited but simple. Fluent API is configuration in DbContext, more powerful and keeps entities clean. Fluent API can do everything Data Annotations can, plus advanced features.
Q: Can you mix Data Annotations and Fluent API?
A: Yes, and it's common. Use Data Annotations for validation, Fluent API for database configuration. Fluent API takes precedence when both configure the same thing.
Q: What is IEntityTypeConfiguration and why use it?
A: It's an interface for organizing entity configuration into separate classes. Benefits: better organization, single responsibility, easier to maintain, reusable configurations.
Q: How do you apply all configurations at once?
A: Use modelBuilder.ApplyConfigurationsFromAssembly(assembly) to automatically discover and apply all IEntityTypeConfiguration classes.
Best Practices
✅ DO
- Use IEntityTypeConfiguration for organization
- Keep entity classes as POCOs (Plain Old CLR Objects)
- Use Fluent API for complex configurations
- Apply configurations from assembly
- Keep configuration DRY (Don't Repeat Yourself)
❌ DON'T
- Mix database and validation concerns in Data Annotations
- Put all configuration in OnModelCreating
- Over-configure simple entities
- Use magic strings (use nameof or lambdas)
Practice Exercise
- Create entities with Data Annotations
- Convert them to Fluent API configuration
- Refactor into IEntityTypeConfiguration classes
- Create a complex entity with:
- Composite key
- Computed column
- Value conversion
- Default values