EF Core Relationships Advanced
Master complex entity relationships in Entity Framework Core.
By EMEPublished: February 20, 2025
entity frameworkrelationshipsdatabaseforeign keysnavigation properties
A Simple Analogy
EF relationships are like family connections. One-to-many is like parents and children, many-to-many is like students and courses, and configuring them ensures family trees stay consistent.
One-to-Many Relationships
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
// Navigation property (one-to-many)
public List<Book> Books { get; set; } = new();
}
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public int AuthorId { get; set; } // Foreign key
// Navigation property back to Author
public Author Author { get; set; }
}
// Query
var authorWithBooks = await context.Authors
.Include(a => a.Books)
.FirstOrDefaultAsync(a => a.Id == 1);
foreach (var book in authorWithBooks.Books)
{
Console.WriteLine(book.Title);
}
Many-to-Many Relationships
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
// Many-to-many
public List<Course> Courses { get; set; } = new();
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
public List<Student> Students { get; set; } = new();
}
// DbContext configuration
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity("StudentCourse"); // Auto-generated junction table
}
// Query
var students = await context.Courses
.Where(c => c.Title == "Math")
.SelectMany(c => c.Students)
.ToListAsync();
Many-to-Many with Payload
// Explicit junction table to store extra data
public class StudentCourse
{
public int StudentId { get; set; }
public Student Student { get; set; }
public int CourseId { get; set; }
public Course Course { get; set; }
public DateTime EnrolledDate { get; set; }
public decimal Grade { get; set; }
}
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public List<StudentCourse> StudentCourses { get; set; } = new();
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
public List<StudentCourse> StudentCourses { get; set; } = new();
}
// Query with payload
var courseDetails = await context.StudentCourses
.Where(sc => sc.CourseId == courseId)
.Select(sc => new {
StudentName = sc.Student.Name,
sc.Grade,
sc.EnrolledDate
})
.ToListAsync();
Self-Referencing Relationships
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int? ManagerId { get; set; } // Points to another Employee
// Navigation to manager
public Employee Manager { get; set; }
// Navigation to subordinates
public List<Employee> Subordinates { get; set; } = new();
}
// Configuration
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employee>()
.HasOne(e => e.Manager)
.WithMany(e => e.Subordinates)
.HasForeignKey(e => e.ManagerId)
.IsRequired(false);
}
// Query organizational hierarchy
var manager = await context.Employees
.Include(e => e.Subordinates)
.FirstOrDefaultAsync(e => e.Id == managerId);
Console.WriteLine($"{manager.Name} manages:");
foreach (var subordinate in manager.Subordinates)
{
Console.WriteLine($" - {subordinate.Name}");
}
Cascade Delete
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public List<Book> Books { get; set; } = new();
}
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public int AuthorId { get; set; }
public Author Author { get; set; }
}
// Configuration
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.HasOne(b => b.Author)
.WithMany(a => a.Books)
.HasForeignKey(b => b.AuthorId)
.OnDelete(DeleteBehavior.Cascade); // Delete books when author deleted
}
// When you delete an author, books are automatically deleted
var author = await context.Authors.FindAsync(1);
context.Authors.Remove(author);
await context.SaveChangesAsync(); // Books with AuthorId=1 are deleted
Eager vs Lazy Loading
// Eager loading (specify what to include)
var author = await context.Authors
.Include(a => a.Books)
.FirstOrDefaultAsync(a => a.Id == 1);
// Lazy loading (automatic, requires virtual)
var author = await context.Authors.FindAsync(1);
var books = author.Books; // Loads automatically if virtual and proxy enabled
// Explicit loading
var author = await context.Authors.FindAsync(1);
await context.Entry(author)
.Collection(a => a.Books)
.LoadAsync();
// Avoid N+1 problem
// Bad:
var authors = context.Authors.ToList(); // 1 query
foreach (var author in authors)
{
var books = author.Books; // N more queries
}
// Good:
var authors = await context.Authors
.Include(a => a.Books)
.ToListAsync(); // 1 query with all data
Best Practices
- Use navigation properties: Makes queries easier
- Configure explicitly: Remove ambiguity
- Set appropriate delete behavior: Prevent orphans
- Eager load when needed: Avoid N+1 queries
- Use projections: Avoid loading unnecessary data
Related Concepts
- Foreign key constraints
- Change tracking
- Entity state (Added, Modified, Deleted)
- Transaction management
Summary
Master EF Core relationships to model complex data structures. Configure one-to-many, many-to-many, and self-referencing relationships to maintain data consistency and query efficiency.