The Fluent API is the enterprise standard for configuring Entity Framework Core. By overriding the OnModelCreating method inside your DbContext, you achieve absolute, microscopic control over how your physical SQL database is generated, far beyond the limits of visual Data Annotations.
Instead of polluting the C# Entity with attributes, we define the relational mapping rules cleanly in one centralized location.
public class ApplicationDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Target the specific Entity
modelBuilder.Entity<Product>(entity =>
{
// Table mapping
entity.ToTable("Inventory_Products");
// Primary Key definition
entity.HasKey(p => p.Id);
// Property configurations
entity.Property(p => p.Name)
.IsRequired()
.HasMaxLength(100)
.HasColumnName("Product_Name");
// Precision for Currency
entity.Property(p => p.Price)
.HasPrecision(18, 2);
});
}
}
There are specific configurations that can only be achieved using the Fluent API.
// Instead of setting a default value in C#, we instruct SQL Server to run the
// GETDATE() function natively in the database whenever a new row is inserted.
modelBuilder.Entity<Product>()
.Property(p => p.CreatedDate)
.HasDefaultValueSql("GETDATE()");
A composite key is a Primary Key made from combining two columns. Data Annotations physically cannot define this in EF Core.
// The StudentId and CourseId combined form the unique identifier
modelBuilder.Entity<Enrollment>()
.HasKey(e => new { e.StudentId, e.CourseId });
Creating an index on an Email column speeds up logins. But what if we only want to enforce Unique Emails on accounts that are NOT deleted?
modelBuilder.Entity<User>()
.HasIndex(u => u.Email)
.IsUnique()
.HasFilter("[IsDeleted] = 0"); // A highly optimized SQL Index!
If you have 100 tables, your OnModelCreating method will be 5,000 lines long. We fix this by extracting configurations into dedicated classes.
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Products");
builder.Property(p => p.Name).IsRequired();
}
}
// Inside DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Automatically scans the entire project and applies all configurations!
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
}
Q: "If a developer places a `[MaxLength(50)]` Data Annotation on a property, but the Architect writes a Fluent API rule limiting that exact same property to `HasMaxLength(100)`, which rule actually gets applied to the SQL database?"
Architect Answer: "The Fluent API will aggressively override the Data Annotation. EF Core processes model building in a strict sequence: First, it applies Default Conventions. Second, it applies Data Annotations. Finally, it executes the Fluent API in `OnModelCreating`. Because the Fluent API is processed last, its configurations are considered the absolute final authority in the framework and will overwrite any conflicting rules established by the attributes."