Configuring One-To-Many Relationships with Fluent API & Using EntityTypeConfiguration Class

In previous blog posts (part 1 & part 2), we first used default conventions to create 1-to-Many relationships between entities. Later, we used Data Annotations to configure the entities and the relationship between them (e.g., creating a foreign key property, declaring a different primary key). However, annotations are not capable of performing certain configurations. Indeed, annotations cover a subset of the functionality that comes with Fluent API. This blog post explains how to use the fluent API to configure 1-to-Many relationships.

For example, one scenario that is not possible to achieve with Annotations is to set the cascade delete action of a relationship. Not being able to configure this option may result in an error when you attempt to build the database from your model. For example, consider the following example, where Song entity has 1:M relationship with two other entities:

A Song can be composed by one Singer (ComposerId as foreign key),
A Song can belong to one Album (AlbumId as foreign key)

You may need the following Navigation properties in Singer class to set up these two relationships:

/////////////////////////////////////////
public Singer Composer { get; set; }

[ForeignKey("Composer")]
public int ComposerId { get; set; }

/////////////////////////////////////////
public Album Album { get; set; }

[ForeignKey("Album")]
public int AlbumId { get; set; }

By default, cascade delete will be set TRUE in both relationships. That is, the deletion of a Singer record will cascade to Song table, and the songs composed by the deleted Singer will be also deleted. Similarly, the deletion of an album record will result in the deletion of song records that are associated with the deleted album. This means there are multiple (two) cascading delete paths, which is not allowed by SQL Server Database. If you attempt to build the database by using Migrations, you will see the following error message (“.. may cause cycles or multiple cascade paths…”) in Package Manager Console:

1

A possible solution to this issue is disabling cascade delete effect in one of these relationships, which is not possible to do with Data Annotations, but with Fluent API.

Fluent API

Fluent API is an effective way of configuring entities and relationships in entity framework code first. There are scenarios where Data Annotations are insufficient (see the example above). However, with Fluent API you can do these specific configurations as needed. With Fluent API, you do not configure the properties of entities, you do directly configure the relationships.

One way to access the code first fluent API is to override the OnModelCreating method on the derived DbContext. With Fluent API, you use an instance (modelBuilder) of DbModelBuilder class to configure the relationship.

The diagram below illustrates the methods of Fluent API that you use commonly to set up relationships between entities:

fluent-api-diagram

Whatever entity type (i.e., A or B) is chosen for Entity<T>() method, you will start building the direction from this entity toward the other entity. If it is Entity<A> then you will start configuring the constraint that every A has many Bs by using HasMany() method. Actually this is already implied in A class declaration by List<B> Bs navigation property. All we need to do is to pass this navigation property as a parameter to HasMany() method with the help of lambda expression: HasMany(x => x.Bs). Please note that fluent api is smart to set the type of x parameter as A type so that we can access the navigation property (Bs). Now, when you press . (dot)  one direction of this relationship (from A toward B) will be completed.

The next set of methods available will have the prefix of With-, which will help define the reverse direction of this relationship (from B to A;each B is associated with only one A). In our case, we will use WithRequired() since we want to constraint that every B entity has to be associated with an A entity. This relationship is actually implied in B entity with CurrA navigation property. Now, we need pass this navigation property to WithRequired() method with the help of lambda expression: WithRequired(x => x.CurrA). Again, fluent api is smart enough to infer that the x input parameter should be type of B and automatically set x as B type.

Not included in the visual above is that you can also configure the foreign key of this relationship. We can indicate the foreign key of this relationship by using .HasForeignKey() method. The dependent entity that carries the foreign key is B entity since it is dependent on A entity. With .HasForeignKey() method we need to use a lambda expression to point to this foreign key property (defined in B entity) with this .HasForeignKey(p => p.CurrAId).

Similarly, we can configure if we want cascade delete option is active or not (if active, when a A record is deleted, the corresponding B records will be automatically deleted as well) We use .WillCascadeOnDelete()  method for this purpose with true (delete cascade is on) or false (delete cascade is off) value.

Detailed Explanation with an Example

Below, we used Fluent API to configure the 1-to-Many relationship between Player and Team entity, and disable Cascade Delete effect to prevent the “Multiple cascade paths are identified” error.

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Player>()
                .HasRequired(p => p.CurrentTeam)
                .WithMany(t => t.Players)
                .HasForeignKey(p => p.CurrentTeamId)
                .WillCascadeOnDelete(false);
}

By using Entity<>() method, you will need to determine whether you want to first define the relationship from Player to Team or from Team to Player. In our case, we preferred to first define the relationship from Player to Team (each Player is associated with one Team) by Entity<Player>(). Then, what follows is the HasRequired, HasOptional, or HasMany method that specify the type of relationship this entity (Player) participates in. The HasRequired and HasOptional methods take a lambda expression that represents a navigation property. The HasMany method takes a lambda expression that represents a collection navigation property. In our case, we have .HasRequired(p=>p.CurrentTeam) because we require that Player entity is associated with a Team. CurrentTeam is the navigation property defined in Player class and we used lambda expression to point to this property.

Then, you need to configure the relationship from Team to Player entity: each team can have many players. You do this by using the WithRequired, WithOptional, or WithMany methods. The WithRequired and WithOptional methods take a lambda expression that represents a navigation property. The WithMany method takes a lambda expression that represents a collection navigation property. In our case, we have .WithMany(t=>t.Players) because we want that each Team can have many players. Players is the collection navigation property defined in Team class and we used lambda expression to point to this navigation property.

Then, we can indicate the foreign key of this relationship by using .HasForeignKey() method. The dependent entity that carries the foreign key is Player entity since it is dependent on Team entity. With .HasForeignKey() method we need to use a lambda expression to point to this foreign key property (defined in Team entity) with this .HasForeignKey(p => p.CurrentTeamId).

Similarly, we can configure if we want cascade delete option is active or not (if active, when a team record is deleted, the corresponding players will be automatically deleted as well) We use .WillCascadeOnDelete()  method for this purpose with true (delete cascade is on) or false (delete cascade is off) value.

The Fluent API starts from Player entity. However, we could do the same configurations by starting from Team entity:

modelBuilder.Entity<Team>()
            .HasMany(t => t.Players)
            .WithRequired(p => p.Team)
            .HasForeignKey(p => p.CurrentTeamId)
            .WillCascadeOnDelete(false);

This time, we first use HasMany() method because of the type of the relationship that Team entity participates in (i.e., each Team can have MANY players), and as the parameter we passed the Players navigation property (defined in Team class). Then, since we require that each player is associated with a team, we use WithRequired() method to indicate this relationship from Player to Team entity by passing the Team navigation property (defined in Player class).

Below are some examples that you can use to practice 1:M configuration with fluent api:

  • Each Columnist can write in one Newspaper, and each Newspaper can have many Columnists.
  • Each Teacher can serve in one School, and each School can have many Teachers.
  • Each Musician can play in one Band, and each Band can have many Musicians.
  • Each Prescription is assigned to one Patient, and each Patient can have many Prescriptions.

A Complete Example of Configuring 1:M Relationship with Fluent API

In the example Model, there are three entities: League, Team, and Player. A League contains many Teams, and a Team can have many Players. A Player can play in only one Team, and a Team can play in only one League. Below is the definition of these entities:

public class League
{
   public int LeagueId { get; set; }
 
   public string LeagueName { get; set; }
  
   public int TeamCount { get; set; }
 
   public List<Team> Teams { get; set; }
}
public class Team
{
   public int TeamId { get; set; }

   public string TeamName { get; set; }
 
   public DateTime DateEstablished { get; set; }
 
   public List<Player> Players { get; set; }

   public League League { get; set; }
 
   public int LeagueId { get; set; }
}
public class Player
{
    public int PlayerId { get; set; }

    public string FullName { get; set; }

    public DateTime BirthDate { get; set; }

    public DateTime LicenseDate { get; set; }

    public Team CurrentTeam { get; set; }

    public int CurrentTeamId { get; set; }
}

In the model definition, we have not use any Data Annotations. We will use Fluent API to configure the relationships among these entities and also to configure some constraints on the properties of entities.

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{    
    modelBuilder.Entity<Team>()
                .HasMany(t => t.Players)
                .WithRequired(p => p.Team)
                .HasForeignKey(p => p.CurrentTeamId);

    modelBuilder.Entity<League>()
                .HasMany(l => l.Teams)
                .WithRequired(t => t.League)
                .HasForeignKey(t => t.LeagueId);

    //Change the table name to FootballTeams
    modelBuilder.Entity<Team>()
                .ToTable("FootballTeams");

    //TeamName will be required when adding a new team
    modelBuilder.Entity<Team>()
                .Property(t => t.TeamName)
                .IsRequired();

    //TeamName can be maximum 50 chars length.
    modelBuilder.Entity<Team>()
                .Property(t => t.TeamName)
                .HasMaxLength(50);

    //TeamCount property will be mapped to TeamLimit column
    modelBuilder.Entity<League>()
                .Property(l => l.TeamCount)
                .HasColumnName("TeamLimit");

     //TeamName can be maximum 50 chars length.
    modelBuilder.Entity<League>()
                .Property(l => l.LeagueName)
                .IsRequired();

    //Fullname will be required when adding a new player
    modelBuilder.Entity<Player>()
                .Property(p => p.FullName)
                .IsRequired();

    //Fullname of players can be maximum 50 chars length
    modelBuilder.Entity<Player>()
                .Property(p => p.FullName)
                .HasMaxLength(50);
} 

As you may observe, only 3 entities has resulted in a long configuration code even though we skipped some configurations for many properties. What if we have 10 entities? Then, we would end up with a very long and complicated configuration code that is hard to read and revise when needed.

We can simplify these Fluent API configuration code by moving the configuration per each entity to a separate configuration file. That is, we will have a distinct configuration classes for League class, Team class, and Player class.

First let’s build the configuration class file for the League entity. For this, you need to add a class to your project and name it LeagueConfiguration. Then, please make sure LeagueConfiguration class inherits from EntityTypeConfiguration<T> class of entity framework. The T (or type) will be League:  EntityTypeConfiguration<League>. Next, you need to create the constructor for this class, which will include the configurations, as you see in the code below:

public class LeagueConfiguration : EntityTypeConfiguration<League>
{
    public LeagueConfiguration()
    {
         //1:M = a league can have many teams
         HasMany(l => l.Teams)
         .WithRequired(t => t.League)
         .HasForeignKey(t => t.LeagueId);

         //TeamCount property will be mapped to TeamLimit colum in the db
         Property(l => l.TeamCount).HasColumnName("TeamLimit");

         //LeagueName will be required when adding a new league
         Property(l => l.LeagueName).IsRequired();
    }
}

In the code above, we did not need to use modelBuilder.Entity<>() method when defining the configurations. This is because since the configuration file inherits from EntityTypeConfiguration<League> class, entity framework is able to infer that any code in this class is a configuration addressing the League entity, making modelBuilder.Entity<League>() method redundant. Therefore, any configuration that goes to this class is attached to League class automatically.

Similarly, we can create a new configuration class for Team entity, and make this class inherit from EntityTypeConfiguration<Team>.


public class TeamConfiguration: EntityTypeConfiguration<Team>
{
    public TeamConfiguration()
    {
         //1:M = A team can have many players
         HasMany(t => t.Players)
         .WithRequired(p => p.CurrentTeam)
         .HasForeignKey(p => p.CurrentTeamId);

         //Change the table name to FootballTeams
         ToTable("FootballTeams");

         //TeamName will be required when adding a new team
         Property(t => t.TeamName).IsRequired();

         //TeamName can be maximum 50 chars length.
         Property(t => t.TeamName).HasMaxLength(50);
    }
}

Last, we follow the same steps for configuring Player entity.

public class PlayerConfiguration : EntityTypeConfiguration<Player>
{
    public PlayerConfiguration()
    {
         //Fullname will be required when adding a new player
         Property(p => p.FullName)
         .IsRequired();

         //Fullname of players can be maximum 50 chars length
         Property(p => p.FullName)
         .HasMaxLength(50);
    }
}

Just creating these configuration classes does not mean that Entity Framework will read the these configurations when building the model. We will need to explicitly tell entity framework that we have created some configuration class for your use when building the model. We do this in OnModelCreating() method which is defined in your DbContext file:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
     modelBuilder.Configurations.Add(new LeagueConfiguration());
     modelBuilder.Configurations.Add(new TeamConfiguration());
     modelBuilder.Configurations.Add(new PlayerConfiguration());
}

With the code above, we created a new instance of each configuration file -e.g., new LeagueConfiguration(), and added each of these instances to the Configurations collection of modelBuilder –modelBuilder.Configurations.Add(new LeagueConfiguration()). This is actually how we register the configurations that we created in distinct classes. Otherwise, modelBuilder will not know about these configurations intended by the coder.

One thought on “Configuring One-To-Many Relationships with Fluent API & Using EntityTypeConfiguration Class

  1. Pingback: Many-to-Many Relationships with Additional Fields | Asp.Net and Entity Framework

Leave a comment