Relationships between entities — one-to-many, many-to-many, one-to-one — are configured in EF Core with the Fluent API using HasMany(), HasOne(), WithMany(), and WithOne(). Getting relationships right is critical: wrong cascade delete settings can accidentally wipe entire tables, missing foreign key configuration causes EF Core to add shadow properties, and omitting navigation properties forces verbose LINQ joins. This lesson builds the complete BlogApp entity relationship model used throughout the remaining Web API chapters.
One-to-Many and Many-to-Many
// ── PostConfiguration — all relationships ────────────────────────────────
public class PostConfiguration : IEntityTypeConfiguration<Post>
{
public void Configure(EntityTypeBuilder<Post> entity)
{
// ... property configuration from previous lesson ...
// ── One-to-Many: User has many Posts ──────────────────────────────
entity.HasOne(p => p.Author) // Post has one Author
.WithMany(u => u.Posts) // User has many Posts
.HasForeignKey(p => p.AuthorId) // FK column in Posts table
.OnDelete(DeleteBehavior.Restrict); // prevent deleting user with posts
// ── One-to-Many: Post has many Comments ───────────────────────────
entity.HasMany(p => p.Comments)
.WithOne(c => c.Post)
.HasForeignKey(c => c.PostId)
.OnDelete(DeleteBehavior.Cascade); // deleting post deletes its comments
}
}
// ── Many-to-Many: Post has many Tags, Tag has many Posts ──────────────────
// Approach 1: Automatic junction table (no extra columns on join)
public class PostConfiguration : IEntityTypeConfiguration<Post>
{
public void Configure(EntityTypeBuilder<Post> entity)
{
entity.HasMany(p => p.Tags)
.WithMany(t => t.Posts)
.UsingEntity(j => j.ToTable("PostTags"));
// EF Core creates: PostTags(PostId, TagId) junction table
}
}
// Approach 2: Explicit join entity (when junction table needs extra columns)
public class PostTag // explicit join entity
{
public int PostId { get; set; }
public Post Post { get; set; } = null!;
public int TagId { get; set; }
public Tag Tag { get; set; } = null!;
public DateTime AddedAt { get; set; } // extra column!
public string AddedBy { get; set; } = string.Empty;
}
// Configuration:
entity.HasMany(p => p.Tags).WithMany(t => t.Posts)
.UsingEntity<PostTag>(
j => j.HasOne(pt => pt.Tag).WithMany().HasForeignKey(pt => pt.TagId),
j => j.HasOne(pt => pt.Post).WithMany().HasForeignKey(pt => pt.PostId),
j => {
j.HasKey(pt => new { pt.PostId, pt.TagId });
j.Property(pt => pt.AddedAt).HasDefaultValueSql("GETUTCDATE()");
});
DeleteBehavior.Cascade means deleting the principal entity (Post) automatically deletes all dependent entities (Comments). DeleteBehavior.Restrict prevents deleting the principal if any dependents exist — a database constraint violation is thrown instead. DeleteBehavior.SetNull sets the FK to null when the principal is deleted (requires nullable FK). Choose carefully: cascade delete is convenient but can be dangerous — accidentally deleting a post removes all its comments permanently. Use Restrict for important business data and handle deletion explicitly in the application layer.public string AuthorId { get; set; } alongside public User? Author { get; set; }. Without an explicit FK property, EF Core creates a “shadow property” — a FK that exists in the database but is invisible in the C# class. Shadow properties are hard to use in queries, cause confusion during debugging, and make it harder to set FK values without loading the full navigation entity. Explicit FK properties are always the better choice.ICollection<Comment> Comments, and Comment has Post Post) can cause circular reference issues during JSON serialisation. If you return a Post with Include(p => p.Comments) and each Comment’s Post navigation is loaded, the serialiser enters an infinite loop. Configure ReferenceHandler.IgnoreCycles in AddJsonOptions(), and more importantly, never return entities directly from API endpoints — always map to DTOs that explicitly control which navigation properties are included.One-to-One Relationship
// ── One-to-One: User has one Profile ─────────────────────────────────────
public class UserConfiguration : IEntityTypeConfiguration<ApplicationUser>
{
public void Configure(EntityTypeBuilder<ApplicationUser> entity)
{
entity.HasOne(u => u.Profile)
.WithOne(p => p.User)
.HasForeignKey<UserProfile>(p => p.UserId) // FK on the dependent side
.OnDelete(DeleteBehavior.Cascade);
}
}
// Profile entity — dependent side of one-to-one
public class UserProfile
{
public int Id { get; set; }
public string UserId { get; set; } = string.Empty; // explicit FK
public string? DisplayName { get; set; }
public string? Bio { get; set; }
public string? AvatarUrl { get; set; }
public ApplicationUser User { get; set; } = null!; // navigation back to user
}
Common Mistakes
Mistake 1 — Using Cascade delete for business-critical data (accidental mass delete)
❌ Wrong — User.Posts configured with Cascade; deleting a test user cascades to delete all their posts.
✅ Correct — use Restrict for important data; handle deletion explicitly with business logic.
Mistake 2 — Returning EF entities directly from API (circular reference serialisation error)
❌ Wrong — Post entity with loaded Comments navigation; JSON serialiser enters infinite cycle.
✅ Correct — map all entities to DTOs before returning from controllers; DTOs control included navigation data.