In the second part of this series, we have learned how to configure non-relational properties in Entity Framework Core. So as a logical continuation, this article will be dedicated to learning about database relationships configuration with Entity Framework Core (EF Core Relationships).

We will show you how to create additional entities in the database model and how to create relationships between them. We are going to use all three ways: by Convention, Data Annotations, and Fluent API, to create those relationships.

You can download the source code for this article on our GitHub repository.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

To see all the basic instructions and complete navigation for this series, visit Entity Framework Core with ASP.NET Core Tutorial.

EF Core Relationships – Concepts and Navigational Properties

Right now, we have only one entity (model) class, the Student class, but soon enough we are going to create the rest of the database model in our application. But before we do that, it is quite important to understand some basic concepts when working with relational databases and models.

When we create a relationship between two entities, one of them becomes the Principal entity and another one is the Dependent entity. The Principal entity is the main entity in a relationship. It contains a primary key as a property that the dependent entity refers to via the foreign key. The Dependent entity, from the other side, is the entity that holds the foreign key that refers to the principal entity’s primary key.

Our entity classes will contain Navigational properties which are the properties containing a single class or a collection of classes that EF Core uses to link entity classes.

Additionally, let’s explain the Required and Optional relationships in EF Core. The required relationship is a relationship where a foreign key cannot be null. This means that the principal entity must exist. The optional relationship is a relationship where a foreign key could be null and therefore the principal entity can be missing.

Configuring One-to-One Relationship

The one-to-one relationship means that a row in one table can only relate to one row in another table in a relationship. This is not that common relationship because it is usually handled as “all the data in one table”, but sometimes (when we want to separate our entities) it is useful to divide data into two tables.

The easiest way to configure this type of relationship is to use by the Convention approach, and that is exactly what we are going to do. So let’s first create another class in the Entities project, named StudentDetails:

public class StudentDetails
{
    [Column("StudentDetailsId")]
    public Guid Id { get; set; }
    public string Address { get; set; }
    public string AdditionalInformation { get; set; }
}

Now, to establish a relationship between the Student and the StudentDetails classes we need to add a reference navigation property at both sides. So, let’s first modify the Student class:

public class Student
{
    [Column("StudentId")]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50, ErrorMessage = "Length must be less then 50 characters")]
    public string Name { get; set; }

    public int? Age { get; set; }
    public bool IsRegularStudent { get; set; }

    public StudentDetails StudentDetails { get; set; }
}

And let’s modify the StudentDetails class:

public class StudentDetails
{
    [Column("StudentDetailsId")]
    public Guid Id { get; set; }
    public string Address { get; set; }
    public string AdditionalInformation { get; set; }

    public Guid StudentId { get; set; }
    public Student Student { get; set; }
}

We can see that the Student class has a reference navigation property towards the StudentDetails class and the StudentDetails class has a foreign key and the navigation property Student.

As a result, we can create a new migration and apply it:

PM> Add-Migration OneToOneRelationshipStudent_StudentDetails
PM> Update-Database

This is the result:

One to one - EF Core Relationships

Excellent, this works great.

Additional Explanation

If we take a look at the first article of this series, we are going to see that we had to create a DbSet<T> property for the Student class in order to be created in the database. But as we can see, we haven’t done the same thing for the StudentDetails class but it is still created in db.

Why is that?

Well, as we explained in the first article, EF Core searches for all the public DbSet<T> properties in the DbContext class to create tables in the database. Then it searches for all the public properties in the T class to map the columns. But it also searches for all the public navigational properties in the T class and creates additional tables and columns related to the type of the navigation property. So, in our example, in the Student class, EF Core finds the StudentDetails navigation property and creates an additional table with its columns.

One-to-Many Relationship Configuration

In this section, we are going to learn how to create One to Many relationships with all three ways. So, before we start, let’s create an additional model class Evaluation in the Entities project:

public class Evaluation
{
    [Column("EvaluationId")]
    public Guid Id { get; set; }
    [Required]
    public int Grade { get; set; }
    public string AdditionalExplanation { get; set; }
}

Let’s continue on.

Using by Convention Approach to Create One-to-Many Relationship

Let’s take a look at the different conventions which automatically configure the one-to-many relationship between the Student and Evaluation classes.

The first approach includes the navigation property in the principal entity, the Student class:

public class Student
{
    [Column("StudentId")]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50, ErrorMessage = "Length must be less then 50 characters")]
    public string Name { get; set; }

    public int? Age { get; set; }
    public bool IsRegularStudent { get; set; }

    public StudentDetails StudentDetails { get; set; }

    public ICollection<Evaluation> Evaluations { get; set; }
}

We have in the ApplicationContext class a DbSet<Student> property and as we explained, EF Core searches through the Student class to find all the navigational properties to create appropriate tables in the database.

Another way to create a One-to-Many relationship is by adding a Student property in the Evaluation class without ICollection<Evaluation> property in the Student class:

public class Evaluation
{
    [Column("EvaluationId")]
    public Guid Id { get; set; }
    [Required]
    public int Grade { get; set; }
    public string AdditionalExplanation { get; set; }

    public Student Student { get; set; }
}

For this approach to work, we have to add a DbSet<Evaluation> Evaluations property in the ApplicationContext class.

The third approach by Convention is to use a combination of the previous ones. So, we can add the ICollection<Evaluation> Evaluations navigational property to the Student class and add the Student Student navigational property in the Evaluation class. Of course, with this approach, we don’t need the DbSet<Evaluation> Evaluations property in the ApplicationContext class.

This is the result of any of these three approaches:

Optional relationship - EF Core Relationships

We can see that the relationship was properly created, but our foreign key is a nullable field. This is because both navigational properties have a default value of null. This relationship is also called an Optional Relationship (we have talked about it in the first part of this article).

If we want to create a required relationship between the Student and Evaluation entities, we have to include the foreign key into the Evaluation class:

public class Student
{
    [Column("StudentId")]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50, ErrorMessage = "Length must be less then 50 characters")]
    public string Name { get; set; }

    public int? Age { get; set; }
    public bool IsRegularStudent { get; set; }
    
    public StudentDetails StudentDetails { get; set; }
    
    public ICollection<Evaluation> Evaluations { get; set; }
}

public class Evaluation
{
    [Column("EvaluationId")]
    public Guid Id { get; set; }
    [Required]
    public int Grade { get; set; }
    public string AdditionalExplanation { get; set; }

    public Guid StudentId { get; set; }
    public Student Student { get; set; }
}

Now when we execute our migration, we are going to see the following result:

Required relationship - EF Core Relationships

It is obvious that our relationship is now required.

Data Annotations Approach

The Data Annotations approach contains only two attributes related to relationships. The [ForeignKey] and [InverseProperty] attributes.

The [ForeignKey] attribute allows us to define a foreign key for a navigational property in the model class. So, let’s modify the Evaluation class by adding this attribute:

public class Evaluation
{
    [Column("EvaluationId")]
    public Guid Id { get; set; }
    [Required]
    public int Grade { get; set; }
    public string AdditionalExplanation { get; set; }

    [ForeignKey(nameof(Student))]
    public Guid StudentId { get; set; }
    public Student Student { get; set; }
} 

We have applied the [ForeignKey] attribute on top of the StudentId property(which is a foreign key in this class) giving it a name of the navigational property Student. But it also works the other way around:

public class Evaluation
{
    [Column("EvaluationId")]
    public Guid Id { get; set; }
    [Required]
    public int Grade { get; set; }
    public string AdditionalExplanation { get; set; }
   
    public Guid StudentId { get; set; }
    [ForeignKey(nameof(StudentId))]
    public Student Student { get; set; }
}

The ForeignKey attribute takes one parameter of type string. If the foreign key is a composite key then the ForeignKey attribute should look like this:

[ForeignKey(“Property1”, “Property2”)].

Whichever way we choose, the result is going to be the same as with the “by Convention” approach. We are going to have a required relationship created between these two tables:

Required relationship - EF Core Relationships

Fluent API approach for the One-to-Many Configuration

To create a One-to-Many relationship with this approach, we need to remove the [ForeignKey] attribute from the Evaluation class and to modify the StudentConfiguration class by adding this code:

builder.HasMany(e => e.Evaluations)
    .WithOne(s => s.Student)
    .HasForeignKey(s => s.StudentId);

With a code like this, we inform EF Core that our Student entity (the builder object is of type EntityTypeBuilder<Student>) can be in a relationship with many Evaluation entities. We also state that the Evaluation is in a relationship with only one Student entity. Finally, we provide information about the foreign key in this relationship.

The result is going to be the same:

Required relationship - EF Core Relationships

We need to mention one thing here.

For the database model like we’ve defined, we don’t need to have the HasForeignKey method. That’s because the foreign key property in the Evaluation class has the same type and the same name as the primary key in the Student class. This means that by Convention this relation would still be the required one. But if we had a foreign key with a different name, StudId for example, then the HasForeignKey method would be needed because otherwise, EF core would create an optional relationship between Evaluation and Student classes.

Many-to-Many Relationship Configuration

This is the implementation for the 3.1 EF Core version.  It is valid for the EF Core version 5, but in version 5 it could be done a bit differently. We’ll explain that in the next section.

Before we start explaining how to configure this relationship, let’s create the required classes in the Entities project:

public class Subject
{
    [Column("SubjectId")]
    public Guid Id { get; set; }
    public string SubjectName { get; set; }
}

public class StudentSubject
{
    public Guid StudentId { get; set; }
    public Student Student { get; set; }

    public Guid SubjectId { get; set; }
    public Subject Subject { get; set; }
}

Now, we can modify the Student and Subject classes by providing the navigational property for each class towards the StudentSubject class:

public class Subject
{
    [Column("SubjectId")]
    public Guid Id { get; set; }
    public string SubjectName { get; set; }

    public ICollection<StudentSubject> StudentSubjects { get; set; }
}

public class Student
{
    [Column("StudentId")]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50, ErrorMessage = "Length must be less then 50 characters")]
    public string Name { get; set; }

    public int? Age { get; set; }
    public bool IsRegularStudent { get; set; }
        
    public StudentDetails StudentDetails { get; set; }
        
    public ICollection<Evaluation> Evaluations { get; set; }

    public ICollection<StudentSubject> StudentSubjects { get; set; }
}

In Entity Framework Core, we have to create a joining entity for a joining table (StudentSubject). This class contains the foreign keys and navigational properties from the Student and Subject classes. Furthermore, the Student and Subject classes both have navigational ICollection properties towards the StudentSubject class. So basically, the Many-to-Many relationship is just two One-To-Many relationships.

We have created our entities, and now we have to create the required configuration. For that, let’s create the StudentSubjectConfiguration class in the Entities/Configuration folder:

public class StudentSubjectConfiguration : IEntityTypeConfiguration<StudentSubject>
{
    public void Configure(EntityTypeBuilder<StudentSubject> builder)
    {
        builder.HasKey(s => new { s.StudentId, s.SubjectId });

        builder.HasOne(ss => ss.Student)
            .WithMany(s => s.StudentSubjects)
            .HasForeignKey(ss => ss.StudentId);

        builder.HasOne(ss => ss.Subject)
            .WithMany(s => s.StudentSubjects)
            .HasForeignKey(ss => ss.SubjectId);
    }
}

As we said, the Many-to-Many is just two One-to-Many EF Core relationships and that’s exactly what we configure in our code. We create a primary key for the StudentSubject table which is, in this case, a composite key. After the primary key configuration, we use a familiar code to create relationship configurations.

Now, we have to modify the OnModelBuilder method in the ApplicationContext class:

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

After these modifications, we can create a migration and apply it:

PM> Add-Migration ManyToManyRelationship

PM> Update-Database

This is the result:

Many to many relationship

Excellent work. Let’s press on.

.NET 5 Note

In .NET 5, we don’t need the StudentSubject table nor the StudentSubjectConfiguration class. By default, if our Student class has a navigational property to the Subject class, and the Subject class has the navigational property to the Student class, this is quite enough. No additional configuration is needed.

Basically, the Student class should have public ICollection<Subject> Subjects { get; set; } property, and the Subject class should have public ICollection<Student> Students { get; set; } property. There is no need for the third class nor the navigational properties to that class.

But, if you want to initially seed the data for both Student and Subject tables and populate the third table with both tables ids, you’ll have to use the implementation we used for the 3.1 version.

OnDelete Method

The OnDelete method configures the delete actions between relational entities. We can add this method to the end of the relationship configuration to decide how the delete actions will execute.

The values that can be used in the OnDelete method are:

  • Restrict – The delete action isn’t applied to dependent entities. This means that we can’t delete the principal entity if it has a related dependent entity.
  • SetNull – The dependent entity isn’t deleted but its foreign key property is set to null.
  • ClientSetNull – If EF Core tracks a dependent entity its foreign key is set to null and that entity is not deleted. If it doesn’t track the dependent entity, the database rules apply.
  • Cascade – The dependent entity is deleted with the principal entity.

If we look at our entities: Student and Evaluation, we are going to see that we have a required relationship between them. For this type of relationship, the Cascade deleting action is configured by default.

We can also see that from the code in our migration file:

Cascasde delete

We can change this type of behavior by modifying the configuration code in the StudentConfiguration class:

builder.HasMany(e => e.Evaluations)
     .WithOne(s => s.Student)
     .HasForeignKey(s => s.StudentId)
     .OnDelete(DeleteBehavior.Restrict);

Let’s create another migration:

PM> Add-Migration StudentEvaluationRestrictDelete

And take a look at the migration generated code:

Restrict delete

Great job.

Conclusion

Configuring EF Core Relationships in our database model is a very important part of the modeling process.

We have seen that EF Core provides us with several ways to achieve that and to make the process as easy as it can be.

Now that we know how to establish relationships in our database, we can continue to the next article where we are going to learn how to access data from the database.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!