Relationships — One-to-Many, Many-to-Many and One-to-One

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()");
        });
Note: 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.
Tip: Always define foreign key properties explicitly in entity classes: 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.
Warning: Bidirectional navigation properties (Post has 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.

🧠 Test Yourself

A DeleteBehavior.Restrict is configured for Post→Author. An attempt is made to delete a User who has 3 posts. What happens?