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:
| Type | Description | Example |
|---|---|---|
| One-to-Many | One entity has multiple dependents | One Customer β Many Orders |
| One-to-One | One entity relates to exactly one other | One User β One Profile |
| Many-to-Many | Many entities relate to many others | Students β 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
CustomerIdand maps it to theCustomernavigation property. - A database
Orderstable will include aCustomerIdcolumn with a foreign key constraint toCustomers.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 thatUserProfile.UserIdis 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) withCourseIdandStudentIdcolumns. - 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 fromOrdertoCustomer..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:
| Strategy | Description | Example |
|---|---|---|
| Eager Loading | Loads related data immediately using .Include() | context.Customers.Include(c => c.Orders).ToList(); |
| Lazy Loading | Loads related data when navigation property is first accessed (requires virtual) | customer.Orders triggers load automatically |
| Explicit Loading | Manually loads related entities when needed | context.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:
| DeleteBehavior | Effect |
|---|---|
Cascade | Dependent entities are deleted automatically |
Restrict | Prevents deletion if dependents exist |
SetNull | Sets 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
| Concept | Description | Example |
|---|---|---|
| Foreign Key | A field linking a dependent entity to its parent | public int CustomerId { get; set; } |
| Navigation Property | Object reference to related data | public Customer Customer { get; set; } |
| Relationship Types | One-to-One, One-to-Many, Many-to-Many | WithOne(), WithMany() |
| Loading Options | Lazy, Eager, Explicit | .Include(), virtual, .Load() |
| Cascade Rules | Defines 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 employeeCall James on 07947 405886 to learn more.