Relationships: One-to-Many & Many-to-Many
Model and query one-to-many and many-to-many relationships with navigation properties and Fluent API.
Relationships: One-to-Many & Many-to-Many is a free C# Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Relationships in EF Core
EF Core models three types of relationships between entities: one-to-one, one-to-many, and many-to-many. Each is expressed with navigation properties and optionally configured with the Fluent API.
One-to-Many: Entity Setup
A Blog has many Posts. The dependent (Post) holds the foreign key. Navigation properties on both sides allow traversal in either direction.
public class Blog
{
public int Id { get; set; }
public string Name { get; set; } = "";
public ICollection<Post> Posts { get; set; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = "";
public int BlogId { get; set; } // FK
public Blog Blog { get; set; } = null!; // nav property
}Fluent API: One-to-Many
EF Core infers the relationship by convention, but you can be explicit with the Fluent API for clarity and to set cascade delete behaviour.
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId)
.OnDelete(DeleteBehavior.Cascade);
}Querying with Include (Eager Loading)
Use Include to load related data in the same query. Use ThenInclude to go deeper into the navigation graph.
// Load blog with all its posts
var blog = await _db.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.Id == 1);
// Load posts with their comments too
var blogs = await _db.Blogs
.Include(b => b.Posts)
.ThenInclude(p => p.Comments)
.ToListAsync();Many-to-Many Without a Join Entity
EF Core 5+ supports many-to-many relationships without an explicit join entity. It creates the join table automatically using the entity type names.
public class Student
{
public int Id { get; set; }
public string Name { get; set; } = "";
public ICollection<Course> Courses { get; set; } = new List<Course>();
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; } = "";
public ICollection<Student> Students { get; set; } = new List<Student>();
}
// EF creates StudentCourse join table automaticallyMany-to-Many With a Join Entity
When the join table needs extra columns (e.g., enrollment date, grade), create an explicit join entity with composite primary key.
public class Enrollment
{
public int StudentId { get; set; }
public Student Student { get; set; } = null!;
public int CourseId { get; set; }
public Course Course { get; set; } = null!;
public DateTime EnrolledAt { get; set; }
public decimal? Grade { get; set; }
}
// Fluent API config:
mb.Entity<Enrollment>().HasKey(e => new { e.StudentId, e.CourseId });
mb.Entity<Enrollment>().HasOne(e => e.Student).WithMany(s => s.Enrollments);
mb.Entity<Enrollment>().HasOne(e => e.Course).WithMany(c => c.Enrollments);Inserting Related Data
Add related entities to a parent's collection before calling SaveChangesAsync. EF Core handles the FK values automatically.
var blog = new Blog { Name = "Dev Notes" };
blog.Posts.Add(new Post { Title = "Hello EF Core" });
blog.Posts.Add(new Post { Title = "Migrations Deep Dive" });
_db.Blogs.Add(blog);
await _db.SaveChangesAsync();
// Inserts Blog + 2 Posts with correct BlogId FKFiltering on Related Data
You can filter on navigation properties in LINQ. EF Core translates these to SQL JOINs automatically.
// Blogs that have at least one published post
var popularBlogs = await _db.Blogs
.Where(b => b.Posts.Any(p => p.IsPublished))
.Select(b => new { b.Name, PostCount = b.Posts.Count(p => p.IsPublished) })
.OrderByDescending(b => b.PostCount)
.ToListAsync();Cascade Delete
With DeleteBehavior.Cascade, deleting the principal (Blog) automatically deletes all dependents (Posts). Other options: SetNull, Restrict, NoAction.
// Delete a blog and all its posts
var blog = await _db.Blogs.FindAsync(1);
_db.Blogs.Remove(blog!);
await _db.SaveChangesAsync();
// Cascade: all Posts with BlogId=1 are also deletedOwned Entities for Value Objects
Owned entity types are great for value objects (e.g., Address). They are stored in the same table as the owner and have no independent identity.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Address ShippingAddress { get; set; } = new();
}
public class Address
{
public string Street { get; set; } = "";
public string City { get; set; } = "";
public string Country { get; set; } = "";
}
// Fluent API:
mb.Entity<Customer>().OwnsOne(c => c.ShippingAddress);Real-World: Order with Line Items
A complete e-commerce pattern: Order (principal) with many OrderLines (dependents), each referencing a Product.
var order = await _db.Orders
.Include(o => o.Lines)
.ThenInclude(l => l.Product)
.FirstOrDefaultAsync(o => o.Id == orderId);
if (order is null) return Results.NotFound();
var summary = new
{
OrderId = order.Id,
Total = order.Lines.Sum(l => l.Quantity * l.Product.Price),
Items = order.Lines.Select(l => new
{
l.Product.Name,
l.Quantity,
LineTotal = l.Quantity * l.Product.Price
})
};
return Results.Ok(summary);Quick Check
When does EF Core create a join table automatically without an explicit join entity?
Recap: Relationships in EF Core
Key takeaways:
- One-to-many: FK on the dependent; navigation on both sides; use Include to load
- Many-to-many (simple): two ICollection navigations — EF Core auto-creates join table
- Many-to-many (with payload): explicit join entity with composite PK
- Cascade delete controls what happens to dependents when principal is removed
- Owned entities model value objects in the same table as the owner
Frequently asked questions
Is the “Relationships: One-to-Many & Many-to-Many” lesson free?
Yes — the full text of “Relationships: One-to-Many & Many-to-Many” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.
What will I learn in “Relationships: One-to-Many & Many-to-Many”?
Model and query one-to-many and many-to-many relationships with navigation properties and Fluent API. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Relationships: One-to-Many & Many-to-Many” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- DbContext & DbSet Basics
- Migrations & Schema Management
- CRUD Operations with EF Core
- Relationships: One-to-Many & Many-to-Many