Skip to content

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 order
  • Customer.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