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.Customerallows access to the customer who placed the orderCustomer.Ordersallows 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 |