Working with Foreign Keys and Relationships in Entity Framework

Building Connected Data Models in C#

Relational databases are built around relationships – how tables link together through primary and foreign keys.
In C#, Entity Framework (EF) models this concept naturally using navigation properties, foreign key fields, and entity configurations.

Whether you’re working Code First, Model First, or Database First, mastering relationships is essential to build robust, real-world data models.


πŸ”Ή What Are Relationships and Foreign Keys?

A foreign key (FK) in Entity Framework is a field (or set of fields) that establishes a link between two entities β€” typically from a child table back to its parent.

This link enforces referential integrity, ensuring that related data stays valid.
For example, every Order must belong to a valid Customer.

In EF, these relationships can be expressed as:

  • Navigation Properties – Object references that point to related entities.
  • Foreign Key Properties – Primitive fields that hold the key value (like CustomerId).

Together, they let you move between entities as easily as you’d traverse objects in memory β€” no manual joins required.


πŸ”Ή Relationship Types in Entity Framework

Entity Framework supports three fundamental relationship types:

TypeDescriptionExample
One-to-ManyOne entity has multiple dependentsOne Customer β†’ Many Orders
One-to-OneOne entity relates to exactly one otherOne User β†’ One Profile
Many-to-ManyMany entities relate to many othersStudents ↔ Courses

Let’s look at how each is expressed and configured in C#.


🧱 One-to-Many Relationships

This is the most common type of relationship. A Customer may have many Orders, but each Order belongs to one Customer.

public class Customer
{
    public int CustomerId { get; set; }
    public string Name { get; set; }

    // Navigation property to related orders
    public ICollection<Order> Orders { get; set; } = new List<Order>();
}

public class Order
{
    public int OrderId { get; set; }
    public DateTime Date { get; set; }

    // Foreign key
    public int CustomerId { get; set; }

    // Navigation property back to the customer
    public Customer Customer { get; set; }
}

βœ… How EF Maps It

  • EF detects the foreign key CustomerId and maps it to the Customer navigation property.
  • A database Orders table will include a CustomerId column with a foreign key constraint to Customers.CustomerId.

🧩 One-to-One Relationships

Used when one entity corresponds directly to exactly one other, such as a User and their UserProfile.

public class User
{
    public int UserId { get; set; }
    public string Email { get; set; }
    public UserProfile Profile { get; set; }
}

public class UserProfile
{
    [Key, ForeignKey("User")]
    public int UserId { get; set; }
    public string Bio { get; set; }

    public User User { get; set; }
}

βœ… Explanation

  • The [ForeignKey("User")] attribute ensures that UserProfile.UserId is both the primary key and foreign key.
  • EF enforces a strict one-to-one mapping β€” each user can have only one profile.

πŸ” Many-to-Many Relationships

A many-to-many relationship occurs when multiple entities on both sides can be linked, such as Students enrolled in Courses.

In EF Core, this can be expressed without an explicit join entity β€” EF will automatically generate a junction table.

public class Student
{
    public int StudentId { get; set; }
    public string Name { get; set; }
    public ICollection<Course> Courses { get; set; } = new List<Course>();
}

public class Course
{
    public int CourseId { get; set; }
    public string Title { get; set; }
    public ICollection<Student> Students { get; set; } = new List<Student>();
}

βœ… Behind the Scenes

  • EF Core auto-creates a join table (e.g., CourseStudent) with CourseId and StudentId columns.
  • You can customise the join table name or columns using Fluent API.

βš™οΈ Configuring Relationships with Fluent API

While EF conventions handle most relationships automatically, you can fine-tune them via Fluent API in OnModelCreating().

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>()
        .HasOne(o => o.Customer)
        .WithMany(c => c.Orders)
        .HasForeignKey(o => o.CustomerId)
        .OnDelete(DeleteBehavior.Cascade);
}

Explanation:

  • .HasOne() – Defines the reference from Order to Customer.
  • .WithMany() – Indicates a customer can have many orders.
  • .HasForeignKey() – Specifies the FK property.
  • .OnDelete() – Defines the cascade delete rule.

Fluent API provides maximum clarity and control β€” especially useful when relationships are complex or non-standard.


πŸ” Loading Related Data

Entity Framework provides several strategies to control when related entities are loaded:

StrategyDescriptionExample
Eager LoadingLoads related data immediately using .Include()context.Customers.Include(c => c.Orders).ToList();
Lazy LoadingLoads related data when navigation property is first accessed (requires virtual)customer.Orders triggers load automatically
Explicit LoadingManually loads related entities when neededcontext.Entry(customer).Collection(c => c.Orders).Load();

βœ… Best Practice:
Use Eager Loading for predictable, performance-conscious queries. Avoid excessive Lazy Loading on large datasets.


πŸ”„ Cascade Delete and Referential Actions

When a parent entity is deleted, you can control what happens to its children:

DeleteBehaviorEffect
CascadeDependent entities are deleted automatically
RestrictPrevents deletion if dependents exist
SetNullSets FK value to null on dependents

Example:

modelBuilder.Entity<Order>()
    .HasOne(o => o.Customer)
    .WithMany()
    .OnDelete(DeleteBehavior.Restrict);

This ensures no order can exist without a valid customer, preserving data integrity.


🧠 Querying Relationships with LINQ

Once relationships are configured, LINQ makes navigation effortless:

var customerOrders = context.Customers
    .Include(c => c.Orders)
    .Where(c => c.Name == "Alice")
    .Select(c => new
    {
        c.Name,
        TotalOrders = c.Orders.Count,
        TotalSpent = c.Orders.Sum(o => o.Amount)
    })
    .ToList();

This retrieves customers, their related orders, and aggregates β€” all in a single, efficient query.


πŸ“š Summary

ConceptDescriptionExample
Foreign KeyA field linking a dependent entity to its parentpublic int CustomerId { get; set; }
Navigation PropertyObject reference to related datapublic Customer Customer { get; set; }
Relationship TypesOne-to-One, One-to-Many, Many-to-ManyWithOne(), WithMany()
Loading OptionsLazy, Eager, Explicit.Include(), virtual, .Load()
Cascade RulesDefines deletion behavior.OnDelete(DeleteBehavior.Cascade)

βœ… Best Practices

  • Always define relationships clearly β€” avoid orphaned or ambiguous data.
  • Use Fluent API for fine control.
  • Prefer eager loading for predictable performance.
  • Test cascade rules carefully before deployment.
  • Keep navigation properties consistent with FK fields to ensure EF detects relationships automatically.

πŸ§ͺ Challenge Task

Task:
Create two entities β€” Department and Employee β€” where:

  • Each department can have many employees.
  • Each employee must belong to one department.
  • When a department is deleted, its employees should also be removed.

Expected Output (Console):

HR: 3 employees
IT: 2 employees
Sales: 1 employee

Call James on 07947 405886 to learn more.