# Configuring Entity Relationships in EF Core

> One-to-one, one-to-many, many-to-many - EF Core supports them all. Here is how to configure entity relationships properly with Fluent API and conventions.

Published: 2026-08-25. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/entity-relationships-ef-core

EF Core infers simple relationships by convention, and the Fluent API configures the rest: `HasMany().WithOne()` for one-to-many, `HasOne().WithOne().HasForeignKey<TDependent>()` for one-to-one, and `HasMany().WithMany()` for many-to-many.
Ambiguity around which side holds the foreign key or what a delete does becomes a schema decision whether you intended it or not.
This article walks through each relationship type, owned types, indexes, and delete behaviors.

## Why Fluent API Over Conventions?

An entity relationship in EF Core is a link between two entity types, made up of a foreign key on the dependent side, the navigation properties you use to traverse it, and a delete behavior.

[**EF Core**](https://milanjovanovic.tech/blog/ef-core-postgresql-getting-started) can infer relationships from conventions, but Fluent API gives you explicit control.
The `IEntityTypeConfiguration<T>` pattern keeps that configuration explicit and separated by entity:

```csharp
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.ToTable("orders");
        builder.HasKey(o => o.Id);

        // Relationships defined here
    }
}
```

Register all configurations:

```csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfigurationsFromAssembly(
        typeof(ApplicationDbContext).Assembly);
}
```

The relationships this article configures, and how they connect, look like this:

![Entity relationship diagram showing a one-to-many between Order and LineItem, a one-to-one between User and UserProfile, a many-to-many between Student and Course through an Enrollment join entity, and a self-referencing Employee manages Employee relationship](https://milanjovanovic.tech/blogs/articles/entity-relationships-ef-core/relationship-types.png)

The same self-reference can represent an arbitrary tree; [**hierarchical data in EF Core**](https://milanjovanovic.tech/blog/ef-core-hierarchical-data) covers recursive reads, deletes, and larger-tree alternatives.

## One-to-Many

The most common relationship. An order has many line items:

```csharp
public class Order
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; }
    public DateTime CreatedAt { get; set; }
    public List<LineItem> LineItems { get; set; } = [];
}

public class LineItem
{
    public Guid Id { get; set; }
    public Guid OrderId { get; set; }      // FK
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public Order Order { get; set; }        // Navigation
}
```

Configure with Fluent API:

```csharp
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.HasMany(o => o.LineItems)
            .WithOne(li => li.Order)
            .HasForeignKey(li => li.OrderId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}
```

`Cascade` means deleting an order also deletes its line items. Be intentional about this - sometimes you want `Restrict` or `SetNull` instead.

### Without Navigation on the Child

If you don't want a navigation property from `LineItem` back to `Order`:

```csharp
public class LineItem
{
    public Guid Id { get; set; }
    public Guid OrderId { get; set; }  // FK only, no nav property
    public string ProductName { get; set; }
}

// Configuration
builder.HasMany(o => o.LineItems)
    .WithOne()
    .HasForeignKey(li => li.OrderId);
```

## One-to-One

A user has one profile:

```csharp
public class User
{
    public Guid Id { get; set; }
    public string Email { get; set; }
    public UserProfile? Profile { get; set; }
}

public class UserProfile
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public string DisplayName { get; set; }
    public string? Bio { get; set; }
    public User User { get; set; }
}
```

```csharp
public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.HasOne(u => u.Profile)
            .WithOne(p => p.User)
            .HasForeignKey<UserProfile>(p => p.UserId)
            .OnDelete(DeleteBehavior.Cascade);
    }
}
```

You must specify which side holds the foreign key with `HasForeignKey<UserProfile>`.

## Many-to-Many

A student enrolls in many courses. A course has many students:

```csharp
public class Student
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public List<Course> Courses { get; set; } = [];
}

public class Course
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public List<Student> Students { get; set; } = [];
}
```

EF Core 5+ creates the join table automatically:

```csharp
builder.HasMany(s => s.Courses)
    .WithMany(c => c.Students)
    .UsingEntity(j => j.ToTable("student_courses"));
```

### Many-to-Many With Payload

When the join table needs extra columns (like enrollment date):

```csharp
public class Enrollment
{
    public Guid StudentId { get; set; }
    public Guid CourseId { get; set; }
    public DateTime EnrolledAt { get; set; }
    public Grade? Grade { get; set; }
    public Student Student { get; set; }
    public Course Course { get; set; }
}

public enum Grade
{
    A, B, C, D, F
}
```

```csharp
public class EnrollmentConfiguration
    : IEntityTypeConfiguration<Enrollment>
{
    public void Configure(EntityTypeBuilder<Enrollment> builder)
    {
        builder.ToTable("enrollments");

        builder.HasKey(e => new { e.StudentId, e.CourseId });

        builder.HasOne(e => e.Student)
            .WithMany(s => s.Enrollments)
            .HasForeignKey(e => e.StudentId);

        builder.HasOne(e => e.Course)
            .WithMany(c => c.Enrollments)
            .HasForeignKey(e => e.CourseId);
    }
}
```

Now `Student` and `Course` reference `Enrollment` instead of each other directly:

```csharp
public class Student
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public List<Enrollment> Enrollments { get; set; } = [];
}
```

## Self-Referencing Relationship

An employee has a manager (who is also an employee):

```csharp
public class Employee
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public Guid? ManagerId { get; set; }
    public Employee? Manager { get; set; }
    public List<Employee> DirectReports { get; set; } = [];
}
```

```csharp
builder.HasOne(e => e.Manager)
    .WithMany(e => e.DirectReports)
    .HasForeignKey(e => e.ManagerId)
    .OnDelete(DeleteBehavior.Restrict);
```

Use `Restrict` here - you don't want cascading deletes up the org chart.

## Owned Types (Value Objects)

For [**value objects**](https://milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals), use owned types:

```csharp
public class Order
{
    public Guid Id { get; set; }
    public Address ShippingAddress { get; set; }
    public Money TotalAmount { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
    public string Country { get; set; }
}

public sealed record Money(decimal Amount, string Currency);
```

```csharp
builder.OwnsOne(o => o.ShippingAddress, address =>
{
    address.Property(a => a.Street).HasColumnName("shipping_street");
    address.Property(a => a.City).HasColumnName("shipping_city");
    address.Property(a => a.ZipCode).HasColumnName("shipping_zip");
    address.Property(a => a.Country).HasColumnName("shipping_country");
});

builder.OwnsOne(o => o.TotalAmount, money =>
{
    money.Property(m => m.Amount).HasColumnName("total_amount");
    money.Property(m => m.Currency).HasColumnName("total_currency");
});
```

Owned types are stored in the same table as the parent entity - no joins needed.
I cover them in depth (including collections and DDD usage) in [**owned types for DDD value objects**](https://milanjovanovic.tech/blog/owned-types-ef-core-ddd).

## Loading Related Data

Configuring a relationship is half the story; the other half is how you load it.
Your options are eager loading with `Include`, explicit loading, and lazy loading - each with different query patterns and pitfalls.
I compare them in [**lazy vs eager vs explicit loading**](https://milanjovanovic.tech/blog/lazy-eager-explicit-loading-ef-core).

One tip that belongs here: when you `Include` multiple collections, watch out for cartesian explosion, and reach for [**query splitting**](https://milanjovanovic.tech/blog/how-to-improve-performance-with-ef-core-query-splitting) when result sets multiply.

## Indexes

EF Core creates an index on foreign key columns by convention, so `OrderId` is already covered.
Add indexes for the columns your queries filter and sort on:

```csharp
builder.HasIndex(o => o.CreatedAt);
builder.HasIndex(o => o.CustomerName);

// Unique index
builder.HasIndex(u => u.Email).IsUnique();

// Composite index
builder.HasIndex(e => new { e.CourseId, e.EnrolledAt });
```

## Delete Behaviors

- **`Cascade`**: deleting the parent also deletes its children
- **`Restrict`**: deleting a parent with children throws an exception
- **`SetNull`**: deleting the parent sets the FK to null on the children (requires a nullable FK)
- **`ClientSetNull`**: like SetNull, but only for children the context is currently tracking; untracked children cause a database FK violation
- **`NoAction`**: the database decides (engine-dependent)

The defaults are `Cascade` for required relationships and `ClientSetNull` for optional ones.
Be explicit about what you want - especially in PostgreSQL and SQL Server, where accidental cascades on big tables hurt.

## Summary

Use conventions for unambiguous relationships and Fluent API where the foreign key, principal side, or delete behavior needs to be explicit.
Model a many-to-many join as an entity as soon as the relationship carries data of its own.
Review the generated constraints and indexes because the relationship is not complete until the database enforces the same intent.

## Frequently asked questions

### How do I configure a one-to-many relationship in EF Core?

Use HasMany on the parent, WithOne on the child, and HasForeignKey to name the FK property. EF Core can infer simple cases by convention, but explicit configuration documents intent and lets you control delete behavior.

### Do I need a join entity for many-to-many in EF Core?

Not since EF Core 5, which creates the join table automatically from HasMany().WithMany(). You only need an explicit join entity when the relationship carries extra data, such as an enrollment date or a grade.

### What is the default delete behavior in EF Core?

Cascade for required relationships and ClientSetNull for optional ones. ClientSetNull only nulls the FK on entities the context is tracking, so be explicit about the behavior you actually want.

### Which side holds the foreign key in a one-to-one relationship?

EF Core cannot infer it, so you must specify it with the generic HasForeignKey<TDependent> call. The dependent side holds the FK column and usually a unique index on it.

### Should navigation properties go on both sides of a relationship?

Only where you actually navigate. A child-to-parent navigation you never use adds coupling for no benefit, and EF Core fully supports one-directional relationships with WithOne() or WithMany() left empty.
