EF Core navigation property returning null? Here's why
Your navigation property always returns null. The data is there. You've checked the database twice. The fix is simpler than you think.
Take these entities. The Post entity has a navigation property of Blog which relates to the Blog entity.
// Post.cs
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public bool IsPublished { get; set; }
public DateTime? PublishedOn { get; set; }
public int BlogId { get; set; }
public Blog? Blog { get; set; }
}// Blog.cs
public class Blog
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
We have created these queries that query the Post entity:
// PostRepositorycs
public class PostRepository : IPostRepository
{
private readonly EFIncludeDbContext _dbContext;
public PostRepository(EFIncludeDbContext dbContext) =>
_dbContext = dbContext;
public async Task GetByIdAsync(int id) =>
await _dbContext.Posts
.AsNoTracking()
.FirstOrDefaultAsync(post => post.Id == id);
public async Task> GetAllAsync() =>
await _dbContext.Posts
.AsNoTracking()
.ToListAsync();
}When you run these queries, BlogId is populated, but the Blog object itself comes back null, on both the single post query and the list query.
{
"id": 1,
"title": "Understanding EF Core Include",
"isPublished": true,
"publishedOn": "2026-01-10T00:00:00",
"blogId": 1,
"blog": null
}The Post entity has a Blog navigation property defined, so on paper it should be populated automatically.
Add Include() to populate the navigation property
The fix is to add Include() to the queries in PostRepository.cs:
// PostRepository.cs
public class PostRepository : IPostRepository
{
...
public async Task GetByIdAsync(int id) =>
await _dbContext.Posts
.Include(x => x.Blog)
.AsNoTracking()
.FirstOrDefaultAsync(post => post.Id == id);
public async Task> GetAllAsync() =>
await _dbContext.Posts
.Include(x => x.Blog)
.AsNoTracking()
.ToListAsync();
}Re-run the queries and the Blog navigation property is now populated.
Watch out for looping collection navigation properties
Navigation properties can go the other way too. Since a post belongs to a blog, you might want a collection of posts on the Blog type as well:
// Blog.cs
public class Blog
{
...
public ICollection<Post> Posts { get; set; } = [];
}However when you re-run the post queries, you'll get a JSON exception saying a possible object cycle was detected.
System.Text.Json.JsonException: A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 64.What's happening is that Post includes Blog, which includes a Post collection, which includes Blog again, and so on. It keeps looping until it hits the JSON serialiser's depth guard, which then throws the exception.
To resolve the circular reference, configure the JSON options to ignore cycles. If you're using controllers:
// Program.cs
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler
= ReferenceHandler.IgnoreCycles;
});And if you're using minimal APIs:
// Program.cs
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.ReferenceHandler
= ReferenceHandler.IgnoreCycles;
});With this in place, the Blog navigation property populates as expected, but the nested Post navigation property within it stays null, since that's where the cycle gets broken.
{
"id": 1,
"title": "Understanding EF Core Include",
"isPublished": true,
"publishedOn": "2026-01-10T00:00:00",
"blogId": 1,
"blog": {
"id": 1,
"name": "Tech Insights",
"metaData": null,
"posts": [
null
]
}
}It works the same way in reverse. If you're querying blogs, you can include the posts within that query, and the posts populate while the blog navigation property nested inside them stays null.
_dbContext.Blogs
.AsNoTracking()
.Include(blog => blog.Posts)
.FirstOrDefaultAsync(blog => blog.Id == id)Be careful with this one though. If you're returning a blog along with all of its posts, and that blog has a lot of posts, you're going to get a very large result.
When you need to configure joins
A lot of the time you don't need to configure joins between two entities. EF Core is clever enough to work out what they are on its own. With the Post entity, there's a BlogId property, so EF Core knows to reference Blog based on its name and its Id property.
Where you do need to configure the relationship is with a 1 to 0..1 join, since EF Core can't work out on its own how that join should be. In this example, the blog entity has a relationship with metadata, and the metadata's own Id is actually the blog's Id.
// Blog.cs
public class Blog
{
...
public BlogMetadata? MetaData { get; set; }
}// BlogMetadata.cs
public class BlogMetadata
{
public int Id { get; set; }
public string? Tagline { get; set; }
}That makes it a one to one relationship, so it needs to be configured explicitly:
// BlogConfiguration.cs
public class BlogConfiguration : IEntityTypeConfiguration<Blog>
{
public void Configure(EntityTypeBuilder<Blog> builder)
{
...
builder.HasOne(x => x.MetaData)
.WithOne()
.HasPrincipalKey<Blog>(blog => blog.Id)
.HasForeignKey<BlogMetadata>(metadata => metadata.Id);
}
}The principal key is Blog.Id, and the foreign key is BlogMetadata.Id. With that configuration in place, and Include() added to the query, the metadata navigation property is returned in the response as expected.
_dbContext.Blogs
.AsNoTracking()
.Include(blog => blog.MetaData)
.FirstOrDefaultAsync(blog => blog.Id == id);When you don't need to add Include()
You don't always need Include(). One example is when you're using Select() to project straight into a new DTO. Returning a full entity means returning all the data from the database for that entity, whether the front end needs it or not. That's worth being careful with, since it's easy to end up exposing properties that were never meant to reach the client.
A better approach is to only return the data you actually need. Here's a DTO which exposes the blog id, the name, and the number of posts belonging to that blog:
// BlogWithPostsCountDto.cs
public record public record BlogWithPostsCountDto(int Id, string Name, int PostsCount);And here is how you can use it in a query:
// BlogRepository.cs
public class BlogRepository : IBlogRepository
{
...
public Task GetBlogWithPostCountAsync(int id) =>
_dbContext.Blogs
.Where(x => x.Id == id)
.Select(x => new BlogWithPostsCountDto(
x.Id,
x.Name,
x.Posts.Count
)).FirstOrDefaultAsync();
}Because this projects straight into the DTO, there's no need to add Include() for the posts. The query that gets generated reflects this too:
SELECT TOP(1) [b].[Id], [b].[Name], (
SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [b].[Id] = [p].[BlogId])
FROM [Blogs] AS [b]
WHERE [b].[Id] = @idNot only is the API response trimmed down to what's needed, the database query itself only pulls back the data required to build the DTO. On a large table, that's a meaningful performance difference.
Multiple Includes and the cartesian explosion problem
If you need joins across multiple entities, you can add multiple Include() calls. This works fine, but expect slower queries as your data set grows. Here's an example that includes both posts and subscribers on a blog:
// BlogRepository.cs
public class BlogRepository : IBlogRepository
{
...
public async Task GetByIdWithPostsAndSubscribersAsync(int id) =>
await _dbContext.Blogs
.AsNoTracking()
.Include(blog => blog.Posts)
.Include(blog => blog.Subscribers)
.FirstOrDefaultAsync(blog => blog.Id == id);
}This is fine for a small data set, but as it grows, performance issues start to appear, since the query has to join against both posts and subscribers at once. This is the cartesian explosion problem, and it's a common cause of slow, timing-out queries once tables grow, particularly on anything that joins several related entities together in a single query.
If you must have multiple includes, you can split the query up instead by adding AsSplitQuery():
// BlogRepository.cs
public class BlogRepository : IBlogRepository
{
...
public async Task GetByIdWithPostsAndSubscribersAsync(int id) =>
await _dbContext.Blogs
.AsNoTracking()
.Include(blog => blog.Posts)
.Include(blog => blog.Subscribers)
.AsSplitQuery()
.FirstOrDefaultAsync(blog => blog.Id == id);
}This still returns the blog with its posts and subscribers, but the database query changes. Instead of one query, EF Core now runs two. One joining posts to the blog, and a second joining subscribers to the blog.
SELECT [p].[Id], [p].[BlogId], [p].[IsPublished],
[p].[PublishedOn], [p].[Title], [b0].[Id]
FROM (
SELECT TOP(1) [b].[Id]
FROM [Blogs] AS [b]
WHERE [b].[Id] = @id
ORDER BY [b].[Id]
) AS [b0]
INNER JOIN [Posts] AS [p] ON [b0].[Id] = [p].[BlogId]
ORDER BY [b0].[Id]SELECT [s].[Id], [s].[BlogId], [s].[Email], [b1].[Id]
FROM (
SELECT TOP(1) [b].[Id]
FROM [Blogs] AS [b]
WHERE [b].[Id] = @id
ORDER BY [b].[Id]
) AS [b1]
INNER JOIN [Subscribers] AS [s] ON [b1].[Id] = [s].[BlogId]
ORDER BY [b1].[Id]That solves the row multiplication problem, but it means two separate round trips to the database instead of one, so it isn't automatically the right fix for every performance problem you'll run into with multiple includes.
Watch the video
Watch the video where we show you how to fix your navigation property if it's returning null, how to configure joins and when not to use Include().
Related tutorials
Learn how to add request logging to a database in an ASP.NET Core Web API using Entity Framework Core to effectively monitor and analyse API traffic.
Learn how to build a reusable PagedResults class in .NET that works with Entity Framework Core and supports multiple entity types.