Skip to main content

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

  1. Create entities with Data Annotations
  2. Convert them to Fluent API configuration
  3. Refactor into IEntityTypeConfiguration classes
  4. Create a complex entity with:
    • Composite key
    • Computed column
    • Value conversion
    • Default values

Additional Resources