Exploring Data Mapping Options in EF CORE

antonmartyniuk

Anton Martyniuk

Posted on May 11, 2024

Exploring Data Mapping Options in EF CORE

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:

public 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; } = [];
}
Enter fullscreen mode Exit fullscreen mode

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:

using 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; }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

  1. Inside DbContext in OnModelCreating method.
  2. Inside a separate Configuration class.

Configure Mapping in DbContext Class

First, let's explore how to define mapping inside a DbContext class:

public 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
    }
}
Enter fullscreen mode Exit fullscreen mode

We can put all our mappings inside a OnModelCreating method for an Author entity:

modelBuilder.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();
});
Enter fullscreen mode Exit fullscreen mode

And for a Book entity:

modelBuilder.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();
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

using 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
    }
}
Enter fullscreen mode Exit fullscreen mode

To register these configuration classes add them to the ModelBuilder class using ApplyConfiguration method:

public class ApplicationDbContext : DbContext
{
    // ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.ApplyConfiguration(new BookConfiguration());
        modelBuilder.ApplyConfiguration(new AuthorConfiguration());
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
Enter fullscreen mode Exit fullscreen mode

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!

Originally published at https://antondevtips.com.

After reading the post consider the following:

  • Subscribe to receive newsletters with the latest blog posts
  • Download the source code for this post from my github (available for my sponsors on BuyMeACoffee and Patreon)

If you like my content —  consider supporting me

Unlock exclusive access to the source code from the blog posts by joining my Patreon and Buy Me A Coffee communities!

💖 💪 🙅 🚩
antonmartyniuk
Anton Martyniuk

Posted on May 11, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related