Configuration of EF Core Entities using FluentAPI

Configuration of EF Core Entities using FluentAPI

·

8 min read

EF Core uses a set of configuration classes to define the structure of the database and the relationships between the entities. These configuration classes are used to map the .NET classes to the database tables and columns, and to specify the rules for working with the data.

In this article, we will discuss how to configure EF Core entities using the Fluent API, which provides a more expressive and flexible way to configure the entities than the traditional data annotations.

Defining the Entity

The first step in configuring an EF Core entity is to define the class that represents the entity. The class should have properties that correspond to the columns in the table, and it should be decorated with the DbSet<T> attribute, which tells EF Core that the class represents an entity.

Here is an example of a simple Student entity class:

public class Student
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

Configuring the Table and Columns

The next step is to configure the database table and columns that the entity maps to. This can be done using the Fluent API in the OnModelCreating method of the DbContext class.

Here is an example of how to configure the Student entity to map to a Students table with columns Id, FirstName, LastName, and Email:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>()
        .ToTable("Students")
        .Property(s => s.Id).IsRequired().ValueGeneratedOnAdd();
    modelBuilder.Entity<Student>()
        .Property(s => s.FirstName).HasMaxLength(50).IsRequired();
    modelBuilder.Entity<Student>()
        .Property(s => s.LastName).HasMaxLength(50).IsRequired();
    modelBuilder.Entity<Student>()
        .Property(s => s.Email).HasMaxLength(100).IsRequired();
}

In the above example, we are using the Fluent API to configure the Student entity to map to a Students table. We are also configuring the Id property as the primary key and a required field, and the other properties as required fields with maximum length of 50 and 100.

Configuring Relationships

EF Core also allows you to configure the relationships between entities. There are three types of relationships that can be configured: one-to-one, one-to-many, and many-to-many.

Here is an example of how to configure a one-to-many relationship between the Student and Course entities:

public class Student
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public ICollection<Course> Courses { get; set; }
}

public class Course
{
public int Id { get; set; }
public string Name { get; set; }
public int StudentId { get; set; }
public Student Student { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.ToTable("Students")
.Property(s => s.Id).IsRequired().ValueGeneratedOnAdd();
modelBuilder.Entity<Student>()
.Property(s => s.FirstName).HasMaxLength(50).IsRequired();
modelBuilder.Entity<Student>()
.Property(s => s.LastName).HasMaxLength(50).IsRequired();
modelBuilder.Entity<Student>()
.Property(s => s.Email).HasMaxLength(100).IsRequired();

modelBuilder.Entity<Course>()
    .ToTable("Courses")
    .Property(c => c.Id).IsRequired().ValueGeneratedOnAdd();
modelBuilder.Entity<Course>()
    .Property(c => c.Name).HasMaxLength(100).IsRequired();
modelBuilder.Entity<Course>()
    .HasOne(c => c.Student)
    .WithMany(s => s.Courses)
    .HasForeignKey(c => c.StudentId);
}

In this example, we have defined a one-to-many relationship between the Student and Course entities, where each student can have multiple courses, and each course is associated with one student. We have used the HasOne and WithMany methods of the Fluent API to define the relationship, and we have also specified the foreign key property StudentId on the Course entity.

Configuration using Separate Classes

Another way to configure EF Core entities is by using separate configuration classes. This approach allows for better organization and separation of concerns, especially when working with a large number of entities and complex configurations.

To use this approach, you need to create a separate configuration class for each entity. The configuration class should inherit from the IEntityTypeConfiguration<TEntity> interface, where TEntity is the entity it's configuring.

Here is an example of a configuration class for the Student entity:

public class StudentConfiguration : IEntityTypeConfiguration<Student>
{
    public void Configure(EntityTypeBuilder<Student> builder)
    {
        builder.ToTable("Students");
        builder.HasKey(s => s.Id);
        builder.Property(s => s.Id).ValueGeneratedOnAdd();
        builder.Property(s => s.FirstName).HasMaxLength(50).IsRequired();
        builder.Property(s => s.LastName).HasMaxLength(50).IsRequired();
        builder.Property(s => s.Email).HasMaxLength(100).IsRequired();
    }
}

As you can see, the Configure method of the configuration class is used to define the configuration for the entity. It's very similar to what we've seen before when using the Fluent API in the OnModelCreating method of the DbContext class.

To use the configuration class, you need to register it in the OnModelCreating method of the DbContext class:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new StudentConfiguration());
}

You can also create configuration classes for relationships:

public class CourseConfiguration : IEntityTypeConfiguration<Course>
{
    public void Configure(EntityTypeBuilder<Course> builder)
    {
        builder.ToTable("Courses");
        builder.HasKey(c => c.Id);
        builder.Property(c => c.Id).ValueGeneratedOnAdd();
        builder.Property(c => c.Name).HasMaxLength(100).IsRequired();
        builder.HasOne(c => c.Student)
            .WithMany(s => s.Courses)
            .HasForeignKey(c => c.StudentId);
    }
}

and register it in the same way:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new StudentConfiguration());
    modelBuilder.ApplyConfiguration(new CourseConfiguration());
}

Using separate configuration classes allows for better organization and separation of concerns. It also allows you to reuse configuration classes across multiple DbContexts.

There is another way to register the configuration classes in the OnModelCreating method of the DbContext class, using the Add method of the ModelBuilder class.

Instead of using the ApplyConfiguration method, you can use the Add method to register the configuration classes.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Add(new StudentConfiguration());
    modelBuilder.Add(new CourseConfiguration());
}

This method is a bit more verbose than the ApplyConfiguration method, but it provides the same functionality.

Another way is to use a assembly scanning and register all the configuration classes in one go.

emodelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());

This way, you don't need to register each configuration class individually. It scan the current assembly for all the classes that implement IEntityTypeConfiguration<T> and register them automatically.

So, you can choose the registration method that best fits your needs and code organization.

Seed the Database with Initial Data

EF Core provides a way to seed the database with initial data, also known as "seed data". Seed data is useful for pre-populating the database with a set of data that is needed for the application to work correctly. For example, you might want to seed the database with a list of predefined users, roles, or lookup values.

There are two ways to seed data with EF Core: using the HasData method of the Fluent API, or by creating a custom DbContext seed method.

Using the HasData Method

The HasData method of the Fluent API allows you to seed data directly in the configuration class. You can use it in the Configure method of the configuration class, right after you define the entity configuration. Here is an example of how to seed the Students table with some initial data:

public class StudentConfiguration : IEntityTypeConfiguration<Student>
{
    public void Configure(EntityTypeBuilder<Student> builder)
    {
        builder.ToTable("Students");
        builder.HasKey(s => s.Id);
        builder.Property(s => s.Id).ValueGeneratedOnAdd();
        builder.Property(s => s.FirstName).HasMaxLength(50).IsRequired();
        builder.Property(s => s.LastName).HasMaxLength(50).IsRequired();
        builder.Property(s => s.Email).HasMaxLength(100).IsRequired();
        builder.HasData(
            new Student { Id = 1, FirstName = "John", LastName = "Doe", Email = "johndoe@email.com" },
            new Student { Id = 2, FirstName = "Jane", LastName = "Doe", Email = "janedoe@email.com" }
        );
    }
}

Using Custom DbContext Seed Method

Another way to seed data is by creating a custom seed method in the DbContext class. This method can be called in the OnModelCreating method to seed the data after the model is created.

Here is an example of how to create a custom seed method in the DbContext class:

public class ApplicationDbContext : DbContext
{
    public DbSet<Student> Students { get; set; }
    //other DbSets

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
    }
    public void SeedData(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Student>().HasData(
            new Student { Id = 1, FirstName = "John", LastName = "Doe", Email = "johndoe@email.com" },
            new Student { Id = 2, FirstName = "Jane", LastName = "Doe", Email = "janedoe@email.com" }
        );
    }
}

Cascading Update and Delete

When working with entities that have relationships, it's important to consider what happens to the related entities when an entity is updated or deleted. EF Core allows you to configure cascading actions to specify how the related entities should be handled when the parent entity is updated or deleted.

There are three cascading actions that can be configured:

  • Cascade: When a parent entity is updated or deleted, the related entities are updated or deleted as well.

  • Restrict: When a parent entity is updated or deleted, an exception is thrown if there are any related entities.

  • Set Null: When a parent entity is deleted, the foreign key on the related entities is set to null. This option is only available for nullable foreign keys.

Here is an example of how to configure cascading actions using the Fluent API:

modelBuilder.Entity<Course>()
    .HasOne(c => c.Student)
    .WithMany(s => s.Courses)
    .HasForeignKey(c => c.StudentId)
    .OnDelete(DeleteBehavior.Cascade);

In this example, we have defined a one-to-many relationship between the Student and Course entities and set the cascading action to DeleteBehavior.Cascade. This means that when a student is deleted, all the courses associated with that student will also be deleted.

It's important to note that the default cascading behavior is Restrict, meaning that an exception will be thrown if there are any related entities when the parent entity is updated or deleted.

Another thing to consider when working with cascading actions is that they can have a significant impact on the data integrity of your database. For example, if you have a cascading delete action and you delete a parent entity without realizing it, all the related entities will also be deleted. This can lead to data loss and unexpected behavior in the application. It's important to be mindful of the cascading actions you are using and to test them thoroughly to ensure they work as expected.