Navigation Properties : Entity Framework Core
Overview
Navigation properties in Entity Framework Core are object references that define relationships between entities — allowing you to navigate from one entity to another using C# object references instead of manual joins or foreign key lookups.
They represent:
- One-to-many
- One-to-one
- Many-to-many relationships
They map to foreign keys behind the scenes, and EF Core uses them to load related data.
Basic Example
Models
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
// Navigational property (many-to-one)
public Customer Customer { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
// Navigational property (one-to-many)
public List<Order> Orders { get; set; }
}
Order.Customer
allows access to the customer who placed the orderCustomer.Orders
allows access to all orders placed by that customer
Advantages
- Makes querying easier and cleaner
- Removes need for manual joins
- Enables lazy, eager, or explicit loading of related data
- Works with LINQ queries naturally
Common Pitfalls / Drawbacks
- Overusing navigation properties without managing loading strategy can cause performance issues (e.g., N+1 query problem)
- Lazy loading isn’t enabled by default in EF Core — you must opt in
- Need to carefully manage serialization in APIs (to avoid circular references)
Querying with Navigation Properties
1. Eager loading using .Include()
var orders = await _context.Orders
.Include(o => o.Customer) // Load related customer
.ToListAsync();
2. Projection using navigation property
var orderSummaries = await _context.Orders
.Select(o => new
{
OrderId = o.Id,
CustomerName = o.Customer.Name
})
.ToListAsync();
EF Core will automatically join the Customer
table in SQL.
Types of Relationships and Navigation
Relationship | Foreign Key | Navigation on Entity A | Navigation on Entity B |
---|---|---|---|
One-to-Many | CustomerId |
Order.Customer |
Customer.Orders |
One-to-One | ProfileId |
User.Profile |
Profile.User |
Many-to-Many | Auto or join table | Student.Courses |
Course.Students |
For many-to-many, you define a
List<T>
on both sides, and EF Core can manage the join table automatically.
Advanced: Fluent API for Relationships
If you're not using conventions, you can define relationships in OnModelCreating
:
modelBuilder.Entity<Order>()
.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId);
Summary
Concept | Explanation |
---|---|
Navigation Property | A property that links one entity to another |
Purpose | Enables easy access to related entities in code |
Common Loading Methods | .Include() , .Select(...) , or lazy loading |
Key Benefits | Clean queries, less boilerplate, object graph navigation |
Key Caution | Watch out for over-eager loading and circular references |
Many-to-many Navigation
Scenario
You have:
- Students who can enrol in many Courses
- Courses that can have many Students
This is a classic many-to-many relationship.
Step 1: Define the Models with Navigation Properties
Student.cs
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
// Many-to-many navigation
public List<Course> Courses { get; set; } = new();
}
Course.cs
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
// Many-to-many navigation
public List<Student> Students { get; set; } = new();
}
EF Core will automatically create the join table behind the scenes — no need to define it explicitly.
Step 2: Configure DbContext
EF Core 5+ will auto-wire this based on conventions. But if you want to be explicit:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity(j => j.ToTable("StudentCourses")); // optional custom join table name
}
Step 3: Seed or Add Data
var student = new Student { Name = "Alice" };
var course1 = new Course { Title = "Maths" };
var course2 = new Course { Title = "Physics" };
student.Courses.Add(course1);
student.Courses.Add(course2);
_context.Students.Add(student);
await _context.SaveChangesAsync();
Query Example: Include Navigation Properties
1. Get all students and their courses
var students = await _context.Students
.Include(s => s.Courses)
.ToListAsync();
2. Get all courses with their students
var courses = await _context.Courses
.Include(c => c.Students)
.ToListAsync();
Query and Project
var studentCourses = await _context.Students
.Select(s => new
{
Student = s.Name,
EnrolledCourses = s.Courses.Select(c => c.Title).ToList()
})
.ToListAsync();
Many-to-many Summary
Element | Description |
---|---|
Many-to-many setup | Use List<T> on both entities |
Join table | Created automatically by EF Core 5+ |
Querying | Use .Include() or .Select() with navigation |
Explicit config | Optional via modelBuilder.Entity<>() |
Projections | Easily access related data using LINQ |
Adding extra fields to the join table
Here's how to model a many-to-many relationship with a join table that includes extra data, such as:
- Enrolment date
- Grade
- Notes
This means you'll need to define the join entity explicitly and configure it properly in Entity Framework Core.
New Scenario
Each Student
can take many Courses
, and each course enrolment has extra details:
- Grade
- Date of enrolment
Step 1: Define the Entities
Student.cs
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public List<Enrollment> Enrollments { get; set; } = new();
}
Course.cs
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
public List<Enrollment> Enrollments { get; set; } = new();
}
Enrollment.cs
(Join Entity)
public class Enrollment
{
public int StudentId { get; set; }
public Student Student { get; set; }
public int CourseId { get; set; }
public Course Course { get; set; }
public DateTime EnrolledOn { get; set; }
public string Grade { get; set; }
}
Step 2: Configure the Relationship in DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Enrollment>()
.HasKey(e => new { e.StudentId, e.CourseId }); // Composite PK
modelBuilder.Entity<Enrollment>()
.HasOne(e => e.Student)
.WithMany(s => s.Enrollments)
.HasForeignKey(e => e.StudentId);
modelBuilder.Entity<Enrollment>()
.HasOne(e => e.Course)
.WithMany(c => c.Enrollments)
.HasForeignKey(e => e.CourseId);
}
Step 3: Add Data
var student = new Student { Name = "Alice" };
var course = new Course { Title = "Biology" };
var enrollment = new Enrollment
{
Student = student,
Course = course,
EnrolledOn = DateTime.UtcNow,
Grade = "A"
};
_context.Enrollments.Add(enrollment);
await _context.SaveChangesAsync();
Query with Navigation
Get all students with their courses and grades
var studentCourseInfo = await _context.Students
.Include(s => s.Enrollments)
.ThenInclude(e => e.Course)
.Select(s => new
{
Student = s.Name,
Courses = s.Enrollments.Select(e => new
{
CourseTitle = e.Course.Title,
Grade = e.Grade,
EnrolledOn = e.EnrolledOn
}).ToList()
})
.ToListAsync();
Many-to-many with Data Summary
Aspect | Approach |
---|---|
Extra fields in join table | Define a separate Enrolment entity |
Keys | Use a composite primary key (StudentId , CourseId ) |
Relationships | Configure both .HasOne() sides in Fluent API |
Querying | Use .Include(...).ThenInclude(...) and LINQ |