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)
Navigation Properties
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:
- One-to-Many: Author → Books
- One-to-One: User → UserProfile
- Many-to-Many: Products ↔ Orders
- Self-Referencing: Employee → Manager
Configure all relationships with appropriate delete behaviors.