A one-to-many (1
In other words, a record in one table can be related to several records in another table, but a record in the second table can only be associated with one record in the first table.
This is the most common type of relationship in databases.
Let’s model a system where:
- One Blog can have many Posts.
- Each Post belongs to only one Blog.
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
// Navigation property (1 → N)
public ICollection<Post> Posts { get; set; } = new List<Post>();
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
// Implicit foreign key (BlogId)
public int BlogId { get; set; }
// Navigation property (N → 1)
public Blog Blog { get; set; }
}
Configure the Relationship
Generated Database Structure
When applying a migration, EF creates the tables:
CREATE TABLE Blogs (
BlogId INT PRIMARY KEY IDENTITY,
Url NVARCHAR(MAX)
);
CREATE TABLE Posts (
PostId INT PRIMARY KEY IDENTITY,
Title NVARCHAR(MAX),
Content NVARCHAR(MAX),
BlogId INT NOT NULL,
FOREIGN KEY (BlogId) REFERENCES Blogs(BlogId)
);
BlogId in Posts is a non-unique foreign key (allows multiple posts per blog).
Advanced Configurations
Besides basic relationships, Entity Framework allows advanced configuration of aspects of the relationships between entities. This includes:
Relationship with Cascade Delete
When a related entity is deleted, cascade delete ensures that all related entities are also deleted.
modelBuilder.Entity<Book>()
.HasOne(l => l.Author)
.WithMany(a => a.Books)
.OnDelete(DeleteBehavior.Cascade);
Read-Only Relationship
In some situations, you may not want to allow a relationship to be modified in an entity.
modelBuilder.Entity<Book>()
.HasOne(l => l.Author)
.WithMany(a => a.Books)
.OnDelete(DeleteBehavior.Restrict);
Optional Relationships
Some relationships are optional, meaning an entity may not have a relationship with the other.
modelBuilder.Entity<Book>()
.HasOne(l => l.Author)
.WithMany(a => a.Books)
.IsRequired(false); // Optional relationship
Practical Examples
Explicit Loading
using (var context = new AppDbContext())
{
var blog = context.Blogs.Find(1); // Query 1: Retrieve Blog
var posts = context.Posts.Where(p => p.BlogId == blog.BlogId).ToList(); // Query 2: Retrieve Posts
}
Eager Loading
var blogWithPosts = context.Blogs
.Include(b => b.Posts) // Automatic JOIN
.FirstOrDefault(b => b.BlogId == 1);
Lazy Loading
Requires navigation properties to be virtual.
public class Blog
{
public virtual ICollection<Post> Posts { get; set; }
}
// When accessing blog.Posts, EF automatically loads the posts
var blog = context.Blogs.Find(1);
var posts = blog.Posts; // Load on demand
Insert Blog with Posts
using (var context = new AppDbContext())
{
var blog = new Blog { Url = "https://example.com" };
blog.Posts.Add(new Post { Title = "First Post", Content = "Content..." });
blog.Posts.Add(new Post { Title = "Second Post", Content = "More content..." });
context.Blogs.Add(blog);
context.SaveChanges(); // Saves Blog and Posts in one transaction
}
Insert Post into an Existing Blog
var blog = context.Blogs.Find(1);
var newPost = new Post { Title = "New Post", BlogId = blog.BlogId };
context.Posts.Add(newPost);
context.SaveChanges();
