Entity Framework Core - Working with Related Data

Entity Framework Core (EF Core) provides robust support for querying and managing relationships between entities. Understanding how to work with related data is crucial for building complex data models and applications.

Understanding Relationships

EF Core models relationships through navigation properties. These properties allow you to traverse from one entity to another. Common relationship types include:

Loading Related Data

EF Core offers several strategies for loading related data:

1. Eager Loading

Eager loading allows you to specify related data that should be loaded along with the primary entities. This is typically done using the Include() and ThenInclude() extension methods in your queries.

Example: Eager Loading a One-to-Many Relationship


using (var context = new MyDbContext())
{
    var blogs = context.Blogs
                       .Include(b => b.Posts) // Load all Posts for each Blog
                       .ToList();

    foreach (var blog in blogs)
    {
        Console.WriteLine($"Blog: {blog.Title}");
        foreach (var post in blog.Posts)
        {
            Console.WriteLine($"- Post: {post.Title}");
        }
    }
}
        

Example: Eager Loading Multiple Levels


using (var context = new MyDbContext())
{
    var blogs = context.Blogs
                       .Include(b => b.Posts)
                           .ThenInclude(p => p.Comments) // Load Comments for each Post
                       .ToList();

    foreach (var blog in blogs)
    {
        Console.WriteLine($"Blog: {blog.Title}");
        foreach (var post in blog.Posts)
        {
            Console.WriteLine($"- Post: {post.Title}");
            foreach (var comment in post.Comments)
            {
                Console.WriteLine($"  - Comment by: {comment.Author}");
            }
        }
    }
}
        

2. Lazy Loading

Lazy loading means that related entities are loaded only when you first access the navigation property. This requires the navigation properties to be virtual and the context to be configured for lazy loading.

Note: Lazy loading can lead to the "N+1 problem" where for each parent entity, a separate query is executed to load its children. Use it judiciously and consider eager loading or explicit loading when performance is critical.

To enable lazy loading, you typically need to install the Microsoft.EntityFrameworkCore.Proxies package and configure it in your DbContext:


protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseLazyLoadingProxies(); // Or .UseSqlServer("YourConnectionString")
}
        

And ensure your navigation properties are virtual:


public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
    public virtual ICollection<Post> Posts { get; set; } // Must be virtual
}
        

3. Explicit Loading

Explicit loading allows you to load related data on demand after the primary entities have already been loaded. This is useful when you don't want to eager load everything but need related data at a specific point.

Example: Explicit Loading


using (var context = new MyDbContext())
{
    var blog = context.Blogs.Find(1);

    if (blog != null)
    {
        // Load posts for this specific blog
        context.Entry(blog)
               .Collection(b => b.Posts)
               .Load();

        Console.WriteLine($"Blog: {blog.Title}");
        foreach (var post in blog.Posts)
        {
            Console.WriteLine($"- Post: {post.Title}");
        }
    }
}
        

Querying Related Data

You can filter or project data based on related entities.

Using Where() with Navigation Properties

EF Core translates queries that use navigation properties into efficient SQL JOINs.


using (var context = new MyDbContext())
{
    // Find all blogs that have at least one published post
    var blogsWithPublishedPosts = context.Blogs
                                        .Where(b => b.Posts.Any(p => p.IsPublished))
                                        .ToList();

    // Find all posts with a title containing 'EF Core'
    var efCorePosts = context.Posts
                             .Where(p => p.Title.Contains("EF Core"))
                             .ToList();
}
        

Using Select() for Projections

Create anonymous types or DTOs to shape the results, including data from related entities.


using (var context = new MyDbContext())
{
    var blogPostTitles = context.Blogs
                                .Select(b => new { BlogTitle = b.Title, PostTitles = b.Posts.Select(p => p.Title).ToList() })
                                .ToList();

    foreach (var item in blogPostTitles)
    {
        Console.WriteLine($"Blog: {item.BlogTitle}");
        foreach (var postTitle in item.PostTitles)
        {
            Console.WriteLine($"- {postTitle}");
        }
    }
}
        

Relationship Configurations

You can explicitly configure relationships in your DbContext using the Fluent API or by using attributes on your entity classes.

Fluent API Configuration


public class MyDbContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasMany(b => b.Posts) // Blog has many Posts
            .WithOne(p => p.Blog)  // Post belongs to one Blog
            .HasForeignKey(p => p.BlogId); // Specify the foreign key property

        modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts)
            .HasForeignKey(p => p.BlogId);
    }
}
        

Data Annotations

Alternatively, use attributes on your entity properties.


public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    [InverseProperty("Blog")]
    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    [ForeignKey("BlogId")]
    public virtual Blog Blog { get; set; }
    public int BlogId { get; set; }

    [InverseProperty("Post")]
    public virtual ICollection<Comment> Comments { get; set; }
}
        

Key Takeaways