A one to many (1
In other words, a record in one table can be related to multiple 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:
- A Blog can have many Posts.
- Each Post belongs to 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; }
}
Configuring the relationship
With Naming Conventions
Entity Framework automatically infers the 1
- The “one” entity (
Blog
) has a collection of the “many” entity (ICollection<Post>
). - The “many” entity (
Post
) has:- A navigation property pointing to the “one” (
public Blog Blog
). - A foreign key that follows the convention
{ClassName}Id
(BlogId
).
- A navigation property pointing to the “one” (
With Data Annotations
If we do not follow the conventions, we can use attributes:
[ForeignKey]
on the foreign key
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
[ForeignKey("Blog")] // Indicates that BlogId is FK of Blog
public int BlogForeignKey { get; set; } // Non-conventional name
public Blog Blog { get; set; }
}
[InverseProperty]
for ambiguous relationships. Useful when there are multiple relationships between the same entities.
public class Blog
{
public int BlogId { get; set; }
[InverseProperty("Blog")] // Clarifies which property in Post corresponds
public ICollection<Post> Posts { get; set; }
}
With Fluent API
In the DbContext
, we use OnModelCreating
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts) // A Blog has many Posts
.WithOne(p => p.Blog) // A Post belongs to a Blog
.HasForeignKey(p => p.BlogId); // Foreign key in Post
}
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
Apart from basic relationships, Entity Framework allows advanced configuration of aspects of relationships between entities. This includes:
Relationship with cascade deletion
When a related entity is deleted, cascade deletion 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 modification of a relationship in an entity.
modelBuilder.Entity<Book>()
.HasOne(l => l.Author)
.WithMany(a => a.Books)
.OnDelete(DeleteBehavior.Restrict);
Optional relationships
Some relationships are optional, meaning that one 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 that navigation properties are 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 in 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();