Skip to main content

Relationship Configurations

Overview

Mastering relationship configuration is essential for EF Core. Understand One-to-One, One-to-Many, Many-to-Many, and their various configurations.

One-to-Many Relationship

Required Relationship

// Entities
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }

public List<Post> Posts { get; set; } = new();
}

public class Post
{
public int Id { get; set; }
public string Title { get; set; }

public int BlogId { get; set; } // Required FK
public Blog Blog { get; set; } // Required navigation
}

// Configuration
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogId)
.OnDelete(DeleteBehavior.Cascade); // Delete posts when blog deleted

Optional Relationship

public class Post
{
public int Id { get; set; }
public string Title { get; set; }

public int? BlogId { get; set; } // Optional FK (nullable)
public Blog? Blog { get; set; } // Optional navigation
}

// Configuration
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogId)
.OnDelete(DeleteBehavior.SetNull); // Set FK to null when blog deleted

One-to-One Relationship

With Foreign Key

// Entities
public class User
{
public int Id { get; set; }
public string Username { get; set; }

public UserProfile Profile { get; set; }
}

public class UserProfile
{
public int Id { get; set; }
public string Bio { get; set; }
public DateTime DateOfBirth { get; set; }

public int UserId { get; set; } // FK
public User User { get; set; }
}

// Configuration
modelBuilder.Entity<User>()
.HasOne(u => u.Profile)
.WithOne(p => p.User)
.HasForeignKey<UserProfile>(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);

Shared Primary Key

public class UserProfile
{
public int Id { get; set; } // Same as User.Id
public string Bio { get; set; }

public User User { get; set; }
}

// Configuration
modelBuilder.Entity<UserProfile>()
.HasOne(p => p.User)
.WithOne(u => u.Profile)
.HasForeignKey<UserProfile>(p => p.Id);

Many-to-Many Relationship

With Join Entity (Explicit)

// Entities
public class Student
{
public int Id { get; set; }
public string Name { get; set; }

public List<StudentCourse> StudentCourses { get; set; }
}

public class Course
{
public int Id { get; set; }
public string Title { get; set; }

public List<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; }

// Additional properties
public DateTime EnrolledDate { get; set; }
public int Grade { get; set; }
}

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

modelBuilder.Entity<StudentCourse>()
.HasOne(sc => sc.Student)
.WithMany(s => s.StudentCourses)
.HasForeignKey(sc => sc.StudentId);

modelBuilder.Entity<StudentCourse>()
.HasOne(sc => sc.Course)
.WithMany(c => c.StudentCourses)
.HasForeignKey(sc => sc.CourseId);

Without Join Entity (Simple)

// Entities (EF Core 5+)
public class Post
{
public int Id { get; set; }
public string Title { get; set; }

public List<Tag> Tags { get; set; }
}

public class Tag
{
public int Id { get; set; }
public string Name { get; set; }

public List<Post> Posts { get; set; }
}

// Configuration (EF Core automatically creates join table)
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(t => t.Posts);

// Custom join table name
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(t => t.Posts)
.UsingEntity(j => j.ToTable("PostTags"));

Self-Referencing Relationships

Tree Structure

public class Category
{
public int Id { get; set; }
public string Name { get; set; }

public int? ParentId { get; set; }
public Category Parent { get; set; }

public List<Category> Children { get; set; }
}

// Configuration
modelBuilder.Entity<Category>()
.HasOne(c => c.Parent)
.WithMany(c => c.Children)
.HasForeignKey(c => c.ParentId)
.OnDelete(DeleteBehavior.Restrict); // Prevent cascading deletes

Delete Behaviors

// Cascade: Delete dependents when principal deleted
.OnDelete(DeleteBehavior.Cascade)

// Restrict: Prevent deletion if dependents exist (throws exception)
.OnDelete(DeleteBehavior.Restrict)

// SetNull: Set FK to null when principal deleted (FK must be nullable)
.OnDelete(DeleteBehavior.SetNull)

// NoAction: Do nothing (database must handle)
.OnDelete(DeleteBehavior.NoAction)

// ClientSetNull: Set FK to null in tracked entities only
.OnDelete(DeleteBehavior.ClientSetNull)

Collection Navigation

// List\<T\> - most common
public List<Post> Posts { get; set; } = new();

// ICollection\<T\> - interface
public ICollection<Post> Posts { get; set; }

// IEnumerable\<T\> - read-only
public IEnumerable<Post> Posts { get; set; }

Reference Navigation

// Required
public Blog Blog { get; set; } = null!;

// Optional
public Blog? Blog { get; set; }

Alternative Keys

public class Country
{
public int Id { get; set; }
public string Code { get; set; } // Alternative key
public string Name { get; set; }
}

public class City
{
public int Id { get; set; }
public string Name { get; set; }

public string CountryCode { get; set; }
public Country Country { get; set; }
}

// Configuration
modelBuilder.Entity<Country>()
.HasAlternateKey(c => c.Code);

modelBuilder.Entity<City>()
.HasOne(c => c.Country)
.WithMany()
.HasForeignKey(c => c.CountryCode)
.HasPrincipalKey(c => c.Code);

Inverse Navigation

// Bidirectional
public class Blog
{
public List<Post> Posts { get; set; }
}

public class Post
{
public Blog Blog { get; set; }
}

// Unidirectional
public class Blog
{
// No navigation to Posts
}

public class Post
{
public Blog Blog { get; set; }
}

// Configuration for unidirectional
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(); // No inverse navigation

Interview Questions

Q: What's the difference between required and optional relationships?

A: Required relationships have non-nullable foreign keys and non-nullable navigation properties. Optional relationships use nullable foreign keys (int?) and nullable navigation properties. Required relationships enforce referential integrity.

Q: When should you use explicit join entities vs automatic many-to-many?

A: Use explicit join entities when:

  • You need additional properties (enrollment date, grade, etc.)
  • You need to query the join table directly
  • You want full control over the join table

Use automatic many-to-many for simple associations without extra data.

Q: Explain cascade delete behaviors.

A:

  • Cascade: Deletes dependent entities
  • Restrict: Throws exception if dependents exist
  • SetNull: Sets FK to null (requires nullable FK)
  • NoAction: Database handles it
  • ClientSetNull: Sets FK to null only in tracked entities

Q: What happens if you don't specify a relationship configuration?

A: EF Core uses conventions to infer relationships based on:

  • Navigation properties
  • Foreign key naming patterns (BlogId matches Blog.Id)
  • Required vs optional (nullable vs non-nullable)

Best Practices

✅ DO

  • Use explicit foreign key properties
  • Initialize collections (= new())
  • Use cascade delete carefully
  • Configure relationships explicitly
  • Use meaningful navigation property names

❌ DON'T

  • Rely on conventions for complex relationships
  • Use cascade delete on many-to-many
  • Forget to configure delete behavior
  • Create circular cascade paths

Practice Exercise

Create a database schema for:

  1. One-to-Many: Author → Books
  2. One-to-One: User → UserProfile
  3. Many-to-Many: Products ↔ Orders
  4. Self-Referencing: Employee → Manager

Configure all relationships with appropriate delete behaviors.

Additional Resources