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 that the configuration is done through functions (programmatically).
Basic Configuration with Fluent API
Fluent API configuration is done in the file that contains the DbContext
, in the OnModelCreating
method, which is invoked by EF Core when the database context is started.
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 like we did with Data Annotations, we will see how to perform some of the most common configurations 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");
Customizing column names allows you to assign specific names to columns in your database that may differ from property names in your model.
modelBuilder.Entity<Product>()
.Property(p => p.Name)
.HasColumnName("ProductName")
.HasColumnType("varchar(100)");
This configuration allows you to exclude specific properties from the entity model so that they are not mapped to columns in the database.
It is useful for calculated or temporary properties that exist only in the application layer and do not require persistence.
modelBuilder.Entity<Product>().Ignore(p => p.TemporaryData);
Key Configuration
The 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 you to identify unique records using multiple columns in combination.
modelBuilder.Entity<OrderDetail>()
.HasKey(od => new { od.OrderId, od.ProductId });
The auto-increment configuration specifies how values for primary keys are automatically generated.
modelBuilder.Entity<Product>()
.Property(p => p.Id)
.ValueGeneratedOnAdd();
Relationship Configuration
The one-to-one relationship establishes a direct connection between two entities where each instance of one entity is related exactly to one instance of another 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
Configuring required fields is essential to ensure that 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 that columns always have a value when one is not explicitly specified during insertion. They are 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 looks when applied to a simple example of a Library, 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 “Books”), Authors, Members.
- Rules:
- Title (book) is required and a maximum of 200 characters.
- Name (author) is required.
- Email (member) is unique.
- Relationships: An author (Author) can have many books (Books).