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
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>();
}
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
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.
public class User
{
public int Id { get; set; }
public string Email { get; set; } = string.Empty;
// Navigation property
public UserProfile? Profile { get; set; }
}
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!;
}
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+)
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>();
}
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>();
}
modelBuilder.Entity<Product>()
.HasMany(p => p.Tags)
.WithMany(t => t.Products);
// EF Core automatically creates join table: ProductTag
Explicit Join Entity (For Extra Data)
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; }
}
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 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
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!;
}
[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
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.