Fluent API in Entity Framework Core is a way to configure the data model using code.
It is an imperative approach to configuring Entity Framework, meaning the configuration is done through functions (programmatically).
Basic Configuration with Fluent API
Fluent API configuration is done in the file containing the DbContext, in the OnModelCreating method, which is invoked by EF Core when the database context starts.
public class ApplicationDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Fluent API configurations here
}
}
Main Configurations with Fluent API
Just as we did with Data Annotations, let’s see how to perform some of the most frequent configurations you’ll need with Fluent API.
Table and Column Configuration
Table name configuration allows you to customize how tables are named in the database, regardless of the class name in the model.
Using the ToTable method, you can specify a custom name for the table.
modelBuilder.Entity<Student>().ToTable("Tbl_Students");
Column name customization allows you to assign specific names to columns in your database that may differ from the property names in your model.
modelBuilder.Entity<Product>()
.Property(p => p.Name)
.HasColumnName("ProductName")
.HasColumnType("varchar(100)");
This configuration allows you to exclude specific entity model properties from being mapped to columns in the database.
It’s useful for calculated or temporary properties that exist only in the application layer and don’t need persistence.
modelBuilder.Entity<Product>().Ignore(p => p.TemporaryData);
Key Configuration
Explicit definition of primary keys is essential when Entity Framework cannot infer them automatically or when you need to customize their behavior.
modelBuilder.Entity<Product>().HasKey(p => p.ProductId);
Composite keys allow identifying unique records using multiple columns in combination.
modelBuilder.Entity<OrderDetail>()
.HasKey(od => new { od.OrderId, od.ProductId });
Autoincrement configuration specifies how values are automatically generated for primary keys.
modelBuilder.Entity<Product>()
.Property(p => p.Id)
.ValueGeneratedOnAdd();
Relationship Configuration
A one-to-one relationship establishes a direct connection between two entities where each instance of one entity relates to exactly one instance of the other entity.
modelBuilder.Entity<Employee>()
.HasOne(e => e.Profile) // An employee has a profile
.WithOne(p => p.Employee) // A profile belongs to an employee
.HasForeignKey<EmployeeProfile>(p => p.EmployeeId);
One-to-many relationships are the most common in relational databases, where one entity can be related to multiple instances of another entity.
modelBuilder.Entity<Product>()
.HasOne(p => p.Category) // A product has a category
.WithMany(c => c.Products) // A category has many products
.HasForeignKey(p => p.CategoryId); // Foreign key
Many-to-many relationships allow multiple instances of one entity to relate to multiple instances of another entity.
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses) // A student has many courses
.WithMany(c => c.Students) // A course has many students
.UsingEntity<Dictionary<string, object>>(
"StudentCourse", // Name of the join table
j => j.HasOne<Course>().WithMany().HasForeignKey("CourseId"),
j => j.HasOne<Student>().WithMany().HasForeignKey("StudentId")
);
Constraints and Validations
Required field configuration is fundamental to ensure essential data is always present. This constraint translates at the database level to NOT NULL columns, preventing incomplete records.
modelBuilder.Entity<Product>()
.Property(p => p.Name)
.IsRequired();
Controlling the length of text fields helps optimize data storage and validate inputs. This configuration prevents truncation issues by defining clear limits for text data.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.Property(e => e.Name)
.HasMaxLength(100);
modelBuilder.Entity<Student>()
.Property(e => e.Email)
.HasMaxLength(150);
}
Default values provide a way to ensure columns always have a value when one isn’t explicitly specified during insertion. They’re useful for fields like creation dates, initial states, or flags.
modelBuilder.Entity<Product>()
.Property(p => p.CreatedAt)
.HasDefaultValueSql("GETDATE()");
Indexes significantly improve query performance by optimizing data search.
modelBuilder.Entity<Product>()
.HasIndex(p => p.Name)
.IsUnique(); // Unique index
Complete Example
Let’s see how everything we’ve covered would look applied to a simple Library example, with Books, Authors, and Members.
public class LibraryContext : DbContext
{
public DbSet<Book> Books { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<Member> Members { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure Book
modelBuilder.Entity<Book>()
.ToTable("Books")
.HasKey(b => b.BookId);
modelBuilder.Entity<Book>()
.Property(b => b.Title)
.IsRequired()
.HasMaxLength(200);
modelBuilder.Entity<Book>()
.HasOne(b => b.Author)
.WithMany(a => a.Books)
.HasForeignKey(b => b.AuthorId);
// Configure Author
modelBuilder.Entity<Author>()
.Property(a => a.Name)
.IsRequired()
.HasMaxLength(100);
// Configure Member
modelBuilder.Entity<Member>()
.HasIndex(m => m.Email)
.IsUnique();
}
}
The LibraryContext class configures:
- Tables: Books (as “Libros”), Authors, Members.
- Rules:
- Title (book) is required and maximum 200 characters.
- Name (author) is required.
- Email (member) is unique.
- Relationships: One author (Author) can have many books (Books).
