Section 5 of 7

Relationships

🎯 What You'll Learn

  • One-to-many relationships
  • One-to-one relationships
  • Many-to-many relationships
  • Navigation properties
  • Foreign keys
  • Configuring relationships

What are Relationships?

Relationships define how entities are related to each other, just like foreign keys in relational databases.

Types of Relationships

  • One-to-Many: One category has many products
  • One-to-One: One user has one profile
  • Many-to-Many: Products have many tags, tags have many products

One-to-Many Relationships

The most common relationship type. Example: One Category has many Products.

Entity Classes

Models/Category.cs C#
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;

    // Navigation property: Collection of products
    public ICollection<Product> Products { get; set; } = new List<Product>();
}
Models/Product.cs C#
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }

    // Foreign key
    public int CategoryId { get; set; }

    // Navigation property: Reference to category
    public Category Category { get; set; } = null!;
}

Fluent API Configuration

Configure Relationship C#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .HasOne(p => p.Category)        // Product has one Category
        .WithMany(c => c.Products)      // Category has many Products
        .HasForeignKey(p => p.CategoryId) // Foreign key
        .OnDelete(DeleteBehavior.Cascade);  // Delete products when category deleted
}

Delete Behaviors

Behavior Description
Cascade Delete dependent entities when principal deleted
Restrict Prevent deletion if dependents exist
SetNull Set foreign key to null when principal deleted
NoAction Do nothing (database handles it)

One-to-One Relationships

Example: One User has one UserProfile.

Models/User.cs C#
public class User
{
    public int Id { get; set; }
    public string Email { get; set; } = string.Empty;

    // Navigation property
    public UserProfile? Profile { get; set; }
}
Models/UserProfile.cs C#
public class UserProfile
{
    public int Id { get; set; }
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;

    // Foreign key
    public int UserId { get; set; }

    // Navigation property
    public User User { get; set; } = null!;
}
Configure One-to-One C#
modelBuilder.Entity<User>()
    .HasOne(u => u.Profile)          // User has one Profile
    .WithOne(p => p.User)            // Profile has one User
    .HasForeignKey<UserProfile>(p => p.UserId);

Many-to-Many Relationships

Example: Products have many Tags, and Tags have many Products.

Modern Approach (EF Core 5+)

Models/Product.cs C#
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;

    // Navigation property
    public ICollection<Tag> Tags { get; set; } = new List<Tag>();
}
Models/Tag.cs C#
public class Tag
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;

    // Navigation property
    public ICollection<Product> Products { get; set; } = new List<Product>();
}
Configure Many-to-Many C#
modelBuilder.Entity<Product>()
    .HasMany(p => p.Tags)
    .WithMany(t => t.Products);

// EF Core automatically creates join table: ProductTag

Explicit Join Entity (For Extra Data)

Models/ProductTag.cs C#
public class ProductTag
{
    public int ProductId { get; set; }
    public Product Product { get; set; } = null!;

    public int TagId { get; set; }
    public Tag Tag { get; set; } = null!;

    // Additional data
    public DateTime AddedAt { get; set; }
}
Configure with Join Entity C#
modelBuilder.Entity<ProductTag>()
    .HasKey(pt => new { pt.ProductId, pt.TagId });

modelBuilder.Entity<ProductTag>()
    .HasOne(pt => pt.Product)
    .WithMany()
    .HasForeignKey(pt => pt.ProductId);

modelBuilder.Entity<ProductTag>()
    .HasOne(pt => pt.Tag)
    .WithMany()
    .HasForeignKey(pt => pt.TagId);

Querying Related Data

Include Related Data C#
// Include category
var products = await _context.Products
    .Include(p => p.Category)
    .ToListAsync();

// Include multiple levels
var products = await _context.Products
    .Include(p => p.Category)
        .ThenInclude(c => c.ParentCategory)
    .ToListAsync();

// Include multiple relationships
var products = await _context.Products
    .Include(p => p.Category)
    .Include(p => p.Tags)
    .ToListAsync();

Complete InvenTrack Example

Models/Order.cs C#
public class Order
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
    public string CustomerName { get; set; } = string.Empty;

    // One-to-many: Order has many OrderItems
    public ICollection<OrderItem> Items { get; set; } = new List<OrderItem>();
}

public class OrderItem
{
    public int Id { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }

    // Foreign keys
    public int OrderId { get; set; }
    public int ProductId { get; set; }

    // Navigation properties
    public Order Order { get; set; } = null!;
    public Product Product { get; set; } = null!;
}
Controllers/OrdersController.cs C#
[HttpGet("{id}")]
public async Task<ActionResult> GetOrder(int id)
{
    var order = await _context.Orders
        .Include(o => o.Items)
            .ThenInclude(i => i.Product)
                .ThenInclude(p => p.Category)
        .FirstOrDefaultAsync(o => o.Id == id);

    if (order == null)
        return NotFound();

    return Ok(new
    {
        order.Id,
        order.OrderDate,
        order.CustomerName,
        items = order.Items.Select(i => new
        {
            i.Id,
            i.Quantity,
            i.Price,
            product = new
            {
                i.Product.Id,
                i.Product.Name,
                category = i.Product.Category.Name
            }
        }),
        total = order.Items.Sum(i => i.Quantity * i.Price)
    });
}

Best Practices

  • Navigation properties: Always define both sides
  • Foreign keys: Explicitly define for clarity
  • Delete behavior: Choose appropriate cascade behavior
  • Include wisely: Only load related data when needed
  • Avoid circular references: Use DTOs for API responses
  • Many-to-many: Use automatic join tables when possible
  • Composite keys: Use for join entities

Key Takeaways

  • One-to-Many: Most common, one parent has many children
  • One-to-One: One entity has exactly one related entity
  • Many-to-Many: Multiple entities on both sides
  • Navigation properties: Reference related entities
  • Foreign keys: Link entities together
  • Fluent API: Configure relationships in OnModelCreating
  • Include(): Load related data (eager loading)
  • Delete behaviors: Cascade, Restrict, SetNull, NoAction
🎯 Next Steps

You now understand relationships in EF Core! In the next section, we'll explore Loading Strategiesβ€”eager loading, lazy loading, and explicit loading of related data.