Entity Framework (EF) is a popular Object-Relational Mapper (ORM) framework for .NET applications. It simplifies database interactions by allowing developers to work with objects instead of direct SQL queries. One of the core functionalities of EF is handling relationships between entities. In this blog post, we’ll explore Relationship Mapping Techniques in Entity Framework Core (one-to-one, one-to-many, and many-to-many).

Understanding Entity Relationships

Before diving into EF, let’s clarify the different types of entity relationships:

  • One-to-One: A single instance of entity A is associated with exactly one instance of entity B, and vice versa. For example, a person might have exactly one passport.
  • One-to-Many: A single instance of entity A can be associated with multiple instances of entity B, but each instance of entity B is associated with exactly one instance of entity A. For example, A customer and their orders.
  • Many-to-Many: Multiple instances of entity A can be associated with multiple instances of entity B, and vice versa. For example, a student can enroll in multiple courses, and a course can have multiple students.

Entity Relationships mapping techniques

We will explore the following three relationship mapping techniques in the entity framework.

  1. Default Conventions
  2. Mapping Relationships with Data Annotations
  3. Mapping Relationships with Fluent API

Choosing the Right Approach

  • Default conventions: Suitable for simple models that follow naming conventions.
  • Data annotations: Useful for basic customizations and when you prefer attribute-based configuration.
  • Fluent API: Provides full control over the mapping process and is recommended for complex scenarios.

Default Conventions

EF provides default conventions for mapping relationships based on naming conventions and entity properties. While often sufficient, understanding these conventions is crucial for customizing mappings.

Here is the ApplicationDbContext file that will be used for Default Convention mappings.

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions <ApplicationDbContext> options)
       : base(options)
    {
    }

    //One to one relation of Passport and Persons
    public DbSet<Passport> Passports { get; set; }
    public DbSet<Person> Persons { get; set; }

    //One to many relation of Blog and Post
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    //Many to Many Relation of Student and Course
    public DbSet<Student> Students { get; set; }
    public DbSet<Course> Courses { get; set; }


    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {            
        // Configure other relationships if needed
        base.OnModelCreating(modelBuilder);
    }
}
C#

One-to-One Relationship

EF assumes a one-to-one relationship when two entities have properties with the same name and type, and one of the properties is marked as a navigation property.

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Passport Passport { get; set; }
}

public class Passport
{
    public int Id { get; set; }
    public string Number { get; set; }
    public int PersonId { get; set; } // Foreign key
    public Person Person { get; set; }
}
C#

EF will create two tables: People and Passports with a one-to-one relationship based on their primary keys.

SQL Schema Design

One-to-Many Relationship

Entity Framework (EF) automatically recognizes a one-to-many relationship between entities under the following conditions:

  • Collection property: One entity contains a collection (like a list or set) of instances of the other entity.
  • Foreign key property: The entity within the collection has a property that matches the primary key of the first entity.
public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public int BlogId { get; set; } // Foreign key
    public Blog Blog { get; set; }
}
C#
  • The Blog entity has a collection of Post entities (the Posts property).
  • The Post entity has a BlogId property that matches the Id property of the Blog entity.

Based on these properties, EF infers a one-to-many relationship between Blog and Post. A blog can have many posts, but a post belongs to only one blog.

SQL Schema Design

Many-to-Many Relationship

Here we have two approaches to mapp many to many relationships.

Many-to-many Relationships without a third entity.

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

public class Course
{
    public int Id { get; set; }
    public string Title { get; set; }
    public ICollection<Student> Students { get; set; }
}
C#

EF will create three tables: Students, Courses, and a join table (usually named StudentCourse) with columns for the primary keys of Student and Course.

SQL Schema Design

Many-to-many Relationships with a third entity.

Let’s consider a many-to-many relationship between Student and Course:

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<StudentCourse> StudentCourses { get; set; }
}

public class Course
{
    public int Id { get; set; }
    public string Title { get; set; }
    public ICollection<StudentCourse> StudentCourses { get; set; }
}

public class StudentCourse
{
    public int StudentId { get; set; }
    public Student Student { get; set; }
    public int CourseId { get; set; }
    public Course Course { get; set; }
}
C#

In this example:

  • StudentCourse is a joint entity.
  • StudentId and CourseId are foreign keys to the Student and Course entities, respectively.

EF can effectively manage many-to-many relationships in your database by creating this join entity and configuring the relationships appropriately.

Entity Relationships with Data Annotations in Entity Framework Core

Here is the ApplicationDbContext file that will be used for mappings with data annotations.

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
       : base(options)
    {
    }

    //One-to-One relation
    public DbSet<User> Users { get; set; }
    public DbSet<UserProfile> UserProfiles { get; set; }

    //ONe-to-Many relation
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    //Many-to-Many relation
    public DbSet<Student> Students { get; set; }
    public DbSet<Course> Courses { get; set; }
    public DbSet<StudentCourse> StudentCourses { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Configure other relationships if needed
        base.OnModelCreating(modelBuilder);
    }
}
C#

One-to-One Relationship

A one-to-one relationship exists when one entity is associated with exactly one instance of another entity.

Consider a scenario where a User has one UserProfile.

public class User
{
    public int UserId { get; set; }
    public string UserName { get; set; }
    
    // Navigation property
    public UserProfile UserProfile { get; set; }
}

public class UserProfile
{
    [Key, ForeignKey("User")]
    public int UserId { get; set; }
    public string Bio { get; set; }
    
    // Navigation property
    public User User { get; set; }
}
C#

User entity has a navigation property UserProfile.

UserProfile entity has a navigation property User and a foreign key UserId which is also its primary key. The [ForeignKey("User")] annotation establishes the relationship.

SQL Schema Design

One-to-Many Relationship

A one-to-many relationship exists when a single entity is associated with multiple instances of another entity.

Consider a scenario where a Category can have many Products.

public class Category
{
    public int CategoryId { get; set; }
    public string CategoryName { get; set; }

    // Navigation property
    public ICollection<Product> Products { get; set; }
}

public class Product
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }

    // Foreign key
    public int CategoryId { get; set; }
    
    // Navigation property
    [ForeignKey("CategoryId")]
    public Category Category { get; set; }
}
C#

Category entity has a navigation property Products which is a collection of Product.Product entity has a foreign key CategoryId and a navigation property Category with the [ForeignKey("CategoryId")] annotation.

SQL Schema Design

Many-to-Many Relationship

A many-to-many relationship exists when multiple instances of one entity are associated with various instances of another entity. In EF Core, many-to-many relationships require a join table.

Consider a scenario where students can enroll in multiple courses, and each course can have multiple students.

public class Student
{
    [Key]
    public int StudentId { get; set; }
    
    [Required]
    public string Name { get; set; }
    
    // Navigation property
    public ICollection<StudentCourse> StudentCourses { get; set; }
}
public class Course
{
    [Key]
    public int CourseId { get; set; }
    
    [Required]
    public string Title { get; set; }
    
    // Navigation property
    public ICollection<StudentCourse> StudentCourses { get; set; }
}
public class StudentCourse
{
    [Key]
    public int StudentCourseId { get; set; }
    
    [ForeignKey("Student")]
    public int StudentId { get; set; }
    
    [ForeignKey("Course")]
    public int CourseId { get; set; }
    
    public Student Student { get; set; }
    public Course Course { get; set; }
}
C#

SQL Schema Design

Student Entity:

StudentId is the primary key, annotated with [Key].

Name is required, annotated with [Required].

StudentCourses is a navigation property representing the many-to-many relationship.

Course Entity:

CourseId is the primary key, annotated with [Key].

Title is required, annotated with [Required].

StudentCourses is a navigation property representing the many-to-many relationship.

StudentCourse Entity:

StudentCourseId is the primary key, annotated with [Key].

StudentId and CourseId are foreign keys, annotated with [ForeignKey("...")].

Student and Course are navigation properties pointing to the respective entities.

Additional Annotations

[Key]
public int Id { get; set; }

[Required]
[MaxLength(100)]
public string Name { get; set; }

[Column("OrderDate", TypeName = "datetime")]
public DateTime Date { get; set; }
C#

[Key]

The [Key] annotation specifies the primary key of an entity.

[Required]

The [Required] annotation makes a property mandatory.

[MaxLength]

The [MaxLength] annotation sets the maximum length of a string property.

[Column]

The [Column] annotation customizes the column mapping in the database, allowing you to specify the column name and data type.

Mapping Relationships with Fluent API in Entity Framework Core

The fluent API offers the most flexibility for configuring relationships. It’s used within the OnModelCreating method of your DbContext class.

We will be using the same classes used Default convention. You just need to update OnModelCreating method in ApplicationDbContext as below.

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions <ApplicationDbContext> options)
       : base(options)
    {
    }

    //One to one relation of Passport and Persons
    public DbSet<Passport> Passports { get; set; }
    public DbSet<Person> Persons { get; set; }

    //One to many relation of Blog and Post
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    //Many to Many Relation of Student and Course
    public DbSet<Student> Students { get; set; }
    public DbSet<Course> Courses { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Person>()
    .HasOne(p => p.Passport)
    .WithOne(p => p.Person)
    .HasForeignKey<Passport>(p => p.PersonId); // Or use PrincipalKey/DependentKey

    modelBuilder.Entity<Blog>()
    .HasMany(b => b.Posts)
    .WithOne(a => a.Blog);

        modelBuilder.Entity<StudentCourse>()
    .HasKey(sc => new { sc.StudentId, sc.CourseId });

    modelBuilder.Entity<Student>()
        .HasMany(s => s.Courses)
        .WithMany(c => c.Students)
        .UsingEntity<StudentCourse>(
            js => js.HasOne(j => j.Course).WithMany(),
            js => js.HasOne(j => j.Student).WithMany());

    base.OnModelCreating(modelBuilder);
    }
    
}
C#

One-to-One relationship

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .HasOne(p => p.Passport)
        .WithOne(p => p.Person)
        .HasForeignKey<Passport>(p => p.PersonId); // Or use PrincipalKey/DependentKey
}
C#

One-to-Many Relationship

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
     .HasMany(b => b.Posts)
     .WithOne(a => a.Blog);
}
C#

Many-to-Many Relationship

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<StudentCourse>()
        .HasKey(sc => new { sc.StudentId, sc.CourseId });

    modelBuilder.Entity<Student>()
        .HasMany(s => s.Courses)
        .WithMany(c => c.Students)
        .UsingEntity<StudentCourse>(
            js => js.HasOne(j => j.Course).WithMany(),
            js => js.HasOne(j => j.Student).WithMany());
}
C#

For more detailed examples and a full implementation, you can refer to this GitHub repository which provides comprehensive examples of the above code snippets.

Categorized in:

Blog, C#, Code, Dotnet, SQL,