Data mapping in Entity Framework Core (EF CORE) provides a rich set of tools to define how your domain classes are mapped to the database schema. In this blog post, we'll explore the different options available for configuring data mappings in EF CORE.
Today we will explore different mapping options for the Book
and Author
entities:
csharppublic class Book { public required Guid Id { get; set; } public required string Title { get; set; } public required int Year { get; set; } public required Guid AuthorId { get; set; } public required Author Author { get; set; } } public class Author { public required Guid Id { get; set; } public required string Name { get; set; } public required List<Book> Books { get; set; } = []; }
Configure Mapping Using Data Annotations
EF CORE supports configuring entity mappings using data annotations. Data annotations are special attributes put on top of class properties that specify how these properties are mapped to the database schema. These attributes provide a way to define mappings directly within your entity classes:
csharpusing System; using System.ComponentModel.DataAnnotations.Schema; public class Book { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; set; } [Required] [Column("title", TypeName = "nvarchar(100)")] public string Title { get; set; } [Required] [Column("year")] public int Year { get; set; } [Required] public Guid AuthorId { get; set; } [ForeignKey("AuthorId")] public Author Author { get; set; } }
While this attributes might seem a concise way to define mapping and have entity and mapping in one place, but this approach has its drawbacks:
- Tight Coupling: Data annotations tightly couple the entity classes with the database schema, making it challenging to switch to a different database provider without modifying the entity classes.
- Limited Flexibility: Data annotations provide limited flexibility compared to fluent API configurations. Complex mappings or scenarios may be difficult or impossible to express using data annotations alone.
- Code Clutter: Embedding data mapping logic within entity classes can clutter the codebase, especially as the application grows larger. This violates the Single Responsibility Principle and makes the code harder to maintain.
- Limited Reusability: Data annotations cannot be easily reused across multiple entities or projects. This can lead to code duplication and inconsistency in mapping conventions.
Now let's explore a better way to define mapping - using Fluent API.
Configure Mapping Using Fluent Api
The fluent API offers a flexible and powerful way to configure entity mappings in EF CORE. By chaining method calls, you can precisely define the relationships, keys, and other attributes of your entities.
There are 2 ways to create mapping using fluent API:
- Inside DbContext in
OnModelCreating
method. - Inside a separate Configuration class.
Configure Mapping in DbContext Class
First, let's explore how to define mapping inside a DbContext
class:
csharppublic class ApplicationDbContext : DbContext { public DbSet<Author> Authors { get; set; } = default!; public DbSet<Book> Books { get; set; } = default!; public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Mapping goes here } }
We can put all our mappings inside a OnModelCreating method for an Author
entity:
csharpmodelBuilder.Entity<Author>(entity => { entity.ToTable("authors"); entity.HasKey(x => x.Id); entity.HasIndex(x => x.Name); entity.Property(a => a.Id) .HasColumnName("id") .ValueGeneratedOnAdd(); entity.Property(a => a.Name) .HasColumnName("name") .IsRequired() .HasColumnType("nvarchar(50)"); entity.HasMany(a => a.Books) .WithOne(b => b.Author) .HasForeignKey(b => b.AuthorId) .IsRequired(); });
And for a Book
entity:
csharpmodelBuilder.Entity<Book>(entity => { entity.ToTable("books"); entity.HasKey(x => x.Id); entity.HasIndex(x => x.Title); entity.Property(b => b.Id) .HasColumnName("id"); entity.Property(b => b.Title) .HasColumnName("title") .IsRequired() .HasColumnType("nvarchar(100)"); entity.Property(b => b.Year) .HasColumnName("year") .IsRequired(); entity.Property(b => b.AuthorId) .HasColumnName("author_id") .IsRequired(); entity.HasOne(b => b.Author) .WithMany(a => a.Books) .HasForeignKey(b => b.AuthorId) .IsRequired(); });
In this mapping a Book
entity has a many-to-one relationship to the Author, meaning an Author can have multiple books.
The with fluent API in EF Core has the following advantages:
- Flexibility: Fluent API provides more flexibility compared to data annotations. It allows for more complex configurations and mappings that might not be achievable with data annotations alone.
- Separation of Concerns: Fluent API promotes better separation of concerns by keeping configuration logic separate from domain classes. This improves code readability and maintainability, as it separates database concerns from domain logic.
- Explicitness: Fluent API configurations are explicit and self-documenting. Developers can easily understand the database mappings by reading the fluent API code, which helps in understanding the database schema without inspecting the database directly.
- Reuse and Composition: Fluent API configurations can be reused across multiple entities or projects. Configuration classes can be composed and organized hierarchically, allowing for easier management and maintenance of complex database mappings.
Fluent API is great but what if we have 5, maybe 10 or even more entities? The DbContext
class will become polluted quickly.
We can try to extract mappings into separate methods but eventually DbContext
class will become a mess and hard to maintain with scrolling for hundreds lines of code.
There is a better way to define mapping with fluent API - using separate configuration classes. Let's explore how to define them.
Configure Mapping in Separate Configuration Classes
We can take all the fluent API mappings from the DbContext
class and extract them to a separate classes that implement the IEntityTypeConfiguration<T>
interface for each entity:
csharpusing Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace MappingOptions.DbMapping; public class BookConfiguration : IEntityTypeConfiguration<Book> { public void Configure(EntityTypeBuilder<Book> builder) { builder.ToTable("books"); builder.HasKey(x => x.Id); builder.HasIndex(x => x.Title); builder.Property(b => b.Id) .HasColumnName("id"); // The rest of the mapping code } } public class AuthorConfiguration : IEntityTypeConfiguration<Author> { public void Configure(EntityTypeBuilder<Author> builder) { builder.ToTable("authors"); builder.HasKey(a => a.Id); builder.Property(a => a.Id) .HasColumnName("id") .ValueGeneratedOnAdd(); // The rest of the mapping code } }
To register these configuration classes add them to the ModelBuilder
class using ApplyConfiguration
method:
csharppublic class ApplicationDbContext : DbContext { // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new BookConfiguration()); modelBuilder.ApplyConfiguration(new AuthorConfiguration()); } }
If you have a lot of entity mapping classes it may become tiresome to specify all of them, moreover they can be missed by a mistake.
You can use an ApplyConfigurationsFromAssembly
method and provide an assembly where all mapping classes are located:
csharpmodelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
You can call this method multiple times and add configurations from multiple assemblies.
Summary
EF Core supports the following mapping options:
- data annotations
- fluent API in DbContext
- fluent API in separate configuration classes
I always use fluent API for expressing the mapping and recommend this approach. We have explored in details why this is the preferable approach over the data annotations.
If you have 5 or fewer entities - you can put the mapping right into the DbContext. If you have more complex entities or a lot of them - prefer extracting mapping into separate configuration classes.
Hope you find this blog post useful. Happy coding!