EF 核心导航 属性 未加载
EF core navigation property not loading
我正在修改我的应用程序,以便能够指定要加载到存储库中的导航属性。
模型:Team 和 TeamTunerUser 可以在 domain entites.
中找到
存储库:
namespace Sppd.TeamTuner.Infrastructure.DataAccess.EF.Repositories
{
internal class Repository<TEntity> : IRepository<TEntity>
where TEntity : BaseEntity
{
/// <summary>
/// Gets the entity set.
/// </summary>
protected DbSet<TEntity> Set => Context.Set<TEntity>();
/// <summary>
/// Gets the DB context.
/// </summary>
protected TeamTunerContext Context { get; }
public Repository(TeamTunerContext context)
{
Context = context;
}
public async Task<TEntity> GetAsync(Guid entityId, IEnumerable<string> includeProperties = null)
{
TEntity entity;
try
{
entity = await GetQueryableWithIncludes(includeProperties).SingleAsync(e => e.Id == entityId);
}
catch (InvalidOperationException)
{
throw new EntityNotFoundException(typeof(TEntity), entityId.ToString());
}
return entity;
}
protected IQueryable<TEntity> GetQueryableWithIncludes(IEnumerable<string> includeProperties = null)
{
var queryable = Set;
if (includeProperties == null)
{
return queryable;
}
foreach (var propertyName in includeProperties)
{
queryable.Include(propertyName);
}
return queryable;
}
}
}
测试:
[Fact]
public async Task TestNavigationPropertyLoading()
{
// Arrange
var teamId = Guid.Parse(TestingConstants.Team.HOLY_COW);
// Act
Team createdTeamWithoutUsers;
Team createdTeamWithUsers;
using (var scope = ServiceProvider.CreateScope())
{
var teamRepository = scope.ServiceProvider.GetService<IRepository<Team>>();
createdTeamWithoutUsers = await teamRepository.GetAsync(teamId);
createdTeamWithUsers = await teamRepository.GetAsync(teamId, new[] {nameof(Team.Users)});
}
// Assert
Assert.Null(createdTeamWithoutUsers.Leader);
Assert.False(createdTeamWithoutUsers.Users.Any());
Assert.False(createdTeamWithUsers.CoLeaders.Any());
Assert.NotNull(createdTeamWithUsers.Leader);
Assert.True(createdTeamWithUsers.Users.Any());
Assert.True(createdTeamWithUsers.CoLeaders.Any());
}
我的问题是 Users
导航 属性 从未加载,第二个断言块失败。
团队配置在这里 (class):
private static void ConfigureTeam(EntityTypeBuilder<Team> builder)
{
ConfigureDescriptiveEntity(builder);
builder.HasMany(e => e.Users)
.WithOne(e => e.Team);
// Ignore calculated properties
builder.Ignore(e => e.Members)
.Ignore(e => e.Leader)
.Ignore(e => e.CoLeaders);
}
(调试)日志不包含任何有用的信息,除了我看到加载导航属性所需的连接没有在 SQL 级别上执行:
2019-04-11 16:02:43,896 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Opening connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:43,901 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Opened connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:43,903 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Command - Executing DbCommand [Parameters=[@__entityId_0='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [m].[Id], [m].[Avatar], [m].[CreatedById], [m].[CreatedOnUtc], [m].[DeletedById], [m].[DeletedOnUtc], [m].[Description], [m].[FederationId], [m].[IsDeleted], [m].[ModifiedById], [m].[ModifiedOnUtc], [m].[Name]
FROM [Team] AS [m]
WHERE ([m].[IsDeleted] = 0) AND ([m].[Id] = @__entityId_0)
2019-04-11 16:02:43,920 [12] INFO Microsoft.EntityFrameworkCore.Database.Command - Executed DbCommand (16ms) [Parameters=[@__entityId_0='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [m].[Id], [m].[Avatar], [m].[CreatedById], [m].[CreatedOnUtc], [m].[DeletedById], [m].[DeletedOnUtc], [m].[Description], [m].[FederationId], [m].[IsDeleted], [m].[ModifiedById], [m].[ModifiedOnUtc], [m].[Name]
FROM [Team] AS [m]
WHERE ([m].[IsDeleted] = 0) AND ([m].[Id] = @__entityId_0)
2019-04-11 16:02:43,945 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Command - A data reader was disposed.
2019-04-11 16:02:43,985 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Closing connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:43,988 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Closed connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:45,054 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Opening connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:45,057 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Opened connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:45,060 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Command - Executing DbCommand [Parameters=[@__entityId_0='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [m].[Id], [m].[Avatar], [m].[CreatedById], [m].[CreatedOnUtc], [m].[DeletedById], [m].[DeletedOnUtc], [m].[Description], [m].[FederationId], [m].[IsDeleted], [m].[ModifiedById], [m].[ModifiedOnUtc], [m].[Name]
FROM [Team] AS [m]
WHERE ([m].[IsDeleted] = 0) AND ([m].[Id] = @__entityId_0)
2019-04-11 16:02:45,067 [14] INFO Microsoft.EntityFrameworkCore.Database.Command - Executed DbCommand (7ms) [Parameters=[@__entityId_0='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [m].[Id], [m].[Avatar], [m].[CreatedById], [m].[CreatedOnUtc], [m].[DeletedById], [m].[DeletedOnUtc], [m].[Description], [m].[FederationId], [m].[IsDeleted], [m].[ModifiedById], [m].[ModifiedOnUtc], [m].[Name]
FROM [Team] AS [m]
WHERE ([m].[IsDeleted] = 0) AND ([m].[Id] = @__entityId_0)
2019-04-11 16:02:45,092 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Command - A data reader was disposed.
2019-04-11 16:02:45,143 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Closing connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:45,153 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Closed connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
我尝试过的:
不指定字符串而是指定导航属性加载的表达式:
protected IQueryable<TEntity> GetQueryableWithIncludes(IEnumerable<string> includeProperties = null)
{
var queryable = Set;
if (includeProperties == null)
{
return queryable;
}
if (typeof(TEntity) == typeof(Team))
{
// TODO: Remove this block once it works by including by string properties
foreach (var propertyName in includeProperties)
{
if (propertyName == "Users")
{
queryable.OfType<Team>().Include(entity => entity.Users);
}
}
}
else
{
foreach (var propertyName in includeProperties)
{
queryable.Include(propertyName);
}
}
return queryable;
}
也为用户实体显式配置关系:
private static void ConfigureTeamTunerUser(EntityTypeBuilder<TeamTunerUser> builder)
{
ConfigureDescriptiveEntity(builder);
builder.HasMany(e => e.CardLevels)
.WithOne(e => e.User);
builder.HasOne(e => e.Team)
.WithMany(e => e.Users);
// Indexes and unique constraint
builder.HasIndex(e => e.Name)
.IsUnique();
builder.HasIndex(e => e.SppdName)
.IsUnique();
builder.HasIndex(e => e.Email)
.IsUnique();
}
我错过了什么?
您尝试过将您的属性标记为虚拟吗?您需要它来根据文档启用延迟加载导航:
Lazy Loading
EF Core will then enable lazy loading for any navigation property that can be overridden--that is, it must be virtual and on a class that can be inherited from. For example, in the following entities, the Post.Blog and Blog.Posts navigation properties will be lazy-loaded.
我注意到了一些问题。
您的方法仅适用于加载第一级导航属性。
foreach (var propertyName in includeProperties)
{
queryable.Include(propertyName);
}
您必须使用 .ThenInclude()
来加载嵌套的导航属性。不过,这打破了 IEnumerable<string> includeProperties = null
作为构造函数的方法。
第二个问题是您的测试本身。它只检查 .Any()
,但根据测试的名称,这是错误的断言。 (我们不知道测试是否失败,因为导航 属性 从未加载 或 它加载成功,但有零个 Users
。你应该只正在检查导航 属性 是否已加载。类似于以下内容。
DbContext.Entry(createdTeamWithUsers).Navigation("Users").IsLoaded
Include
/ ThenInclude
(以及所有其他 EF Core Queryable
扩展)就像常规的 LINQ Queryable
方法(Select
、Where
, OrderBy
等)修改源IQueryable<>
和return修改后的IQueryable<>
.
在这里您只是忘记了使用生成的查询,所以
queryable.Include(propertyName);
与
效果相同
queryable.Where(e => false);
即无效果.
只需将代码更改为
queryable = queryable.Include(propertyName);
我正在修改我的应用程序,以便能够指定要加载到存储库中的导航属性。
模型:Team 和 TeamTunerUser 可以在 domain entites.
中找到存储库:
namespace Sppd.TeamTuner.Infrastructure.DataAccess.EF.Repositories
{
internal class Repository<TEntity> : IRepository<TEntity>
where TEntity : BaseEntity
{
/// <summary>
/// Gets the entity set.
/// </summary>
protected DbSet<TEntity> Set => Context.Set<TEntity>();
/// <summary>
/// Gets the DB context.
/// </summary>
protected TeamTunerContext Context { get; }
public Repository(TeamTunerContext context)
{
Context = context;
}
public async Task<TEntity> GetAsync(Guid entityId, IEnumerable<string> includeProperties = null)
{
TEntity entity;
try
{
entity = await GetQueryableWithIncludes(includeProperties).SingleAsync(e => e.Id == entityId);
}
catch (InvalidOperationException)
{
throw new EntityNotFoundException(typeof(TEntity), entityId.ToString());
}
return entity;
}
protected IQueryable<TEntity> GetQueryableWithIncludes(IEnumerable<string> includeProperties = null)
{
var queryable = Set;
if (includeProperties == null)
{
return queryable;
}
foreach (var propertyName in includeProperties)
{
queryable.Include(propertyName);
}
return queryable;
}
}
}
测试:
[Fact]
public async Task TestNavigationPropertyLoading()
{
// Arrange
var teamId = Guid.Parse(TestingConstants.Team.HOLY_COW);
// Act
Team createdTeamWithoutUsers;
Team createdTeamWithUsers;
using (var scope = ServiceProvider.CreateScope())
{
var teamRepository = scope.ServiceProvider.GetService<IRepository<Team>>();
createdTeamWithoutUsers = await teamRepository.GetAsync(teamId);
createdTeamWithUsers = await teamRepository.GetAsync(teamId, new[] {nameof(Team.Users)});
}
// Assert
Assert.Null(createdTeamWithoutUsers.Leader);
Assert.False(createdTeamWithoutUsers.Users.Any());
Assert.False(createdTeamWithUsers.CoLeaders.Any());
Assert.NotNull(createdTeamWithUsers.Leader);
Assert.True(createdTeamWithUsers.Users.Any());
Assert.True(createdTeamWithUsers.CoLeaders.Any());
}
我的问题是 Users
导航 属性 从未加载,第二个断言块失败。
团队配置在这里 (class):
private static void ConfigureTeam(EntityTypeBuilder<Team> builder)
{
ConfigureDescriptiveEntity(builder);
builder.HasMany(e => e.Users)
.WithOne(e => e.Team);
// Ignore calculated properties
builder.Ignore(e => e.Members)
.Ignore(e => e.Leader)
.Ignore(e => e.CoLeaders);
}
(调试)日志不包含任何有用的信息,除了我看到加载导航属性所需的连接没有在 SQL 级别上执行:
2019-04-11 16:02:43,896 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Opening connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:43,901 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Opened connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:43,903 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Command - Executing DbCommand [Parameters=[@__entityId_0='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [m].[Id], [m].[Avatar], [m].[CreatedById], [m].[CreatedOnUtc], [m].[DeletedById], [m].[DeletedOnUtc], [m].[Description], [m].[FederationId], [m].[IsDeleted], [m].[ModifiedById], [m].[ModifiedOnUtc], [m].[Name]
FROM [Team] AS [m]
WHERE ([m].[IsDeleted] = 0) AND ([m].[Id] = @__entityId_0)
2019-04-11 16:02:43,920 [12] INFO Microsoft.EntityFrameworkCore.Database.Command - Executed DbCommand (16ms) [Parameters=[@__entityId_0='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [m].[Id], [m].[Avatar], [m].[CreatedById], [m].[CreatedOnUtc], [m].[DeletedById], [m].[DeletedOnUtc], [m].[Description], [m].[FederationId], [m].[IsDeleted], [m].[ModifiedById], [m].[ModifiedOnUtc], [m].[Name]
FROM [Team] AS [m]
WHERE ([m].[IsDeleted] = 0) AND ([m].[Id] = @__entityId_0)
2019-04-11 16:02:43,945 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Command - A data reader was disposed.
2019-04-11 16:02:43,985 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Closing connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:43,988 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Closed connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:45,054 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Opening connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:45,057 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Opened connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:45,060 [12] DEBUG Microsoft.EntityFrameworkCore.Database.Command - Executing DbCommand [Parameters=[@__entityId_0='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [m].[Id], [m].[Avatar], [m].[CreatedById], [m].[CreatedOnUtc], [m].[DeletedById], [m].[DeletedOnUtc], [m].[Description], [m].[FederationId], [m].[IsDeleted], [m].[ModifiedById], [m].[ModifiedOnUtc], [m].[Name]
FROM [Team] AS [m]
WHERE ([m].[IsDeleted] = 0) AND ([m].[Id] = @__entityId_0)
2019-04-11 16:02:45,067 [14] INFO Microsoft.EntityFrameworkCore.Database.Command - Executed DbCommand (7ms) [Parameters=[@__entityId_0='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [m].[Id], [m].[Avatar], [m].[CreatedById], [m].[CreatedOnUtc], [m].[DeletedById], [m].[DeletedOnUtc], [m].[Description], [m].[FederationId], [m].[IsDeleted], [m].[ModifiedById], [m].[ModifiedOnUtc], [m].[Name]
FROM [Team] AS [m]
WHERE ([m].[IsDeleted] = 0) AND ([m].[Id] = @__entityId_0)
2019-04-11 16:02:45,092 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Command - A data reader was disposed.
2019-04-11 16:02:45,143 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Closing connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
2019-04-11 16:02:45,153 [14] DEBUG Microsoft.EntityFrameworkCore.Database.Connection - Closed connection to database 'Sppd.TeamTuner-TEST' on server '.\SQLEXPRESS'.
我尝试过的:
不指定字符串而是指定导航属性加载的表达式:
protected IQueryable<TEntity> GetQueryableWithIncludes(IEnumerable<string> includeProperties = null) { var queryable = Set; if (includeProperties == null) { return queryable; } if (typeof(TEntity) == typeof(Team)) { // TODO: Remove this block once it works by including by string properties foreach (var propertyName in includeProperties) { if (propertyName == "Users") { queryable.OfType<Team>().Include(entity => entity.Users); } } } else { foreach (var propertyName in includeProperties) { queryable.Include(propertyName); } } return queryable; }
也为用户实体显式配置关系:
private static void ConfigureTeamTunerUser(EntityTypeBuilder<TeamTunerUser> builder) { ConfigureDescriptiveEntity(builder); builder.HasMany(e => e.CardLevels) .WithOne(e => e.User); builder.HasOne(e => e.Team) .WithMany(e => e.Users); // Indexes and unique constraint builder.HasIndex(e => e.Name) .IsUnique(); builder.HasIndex(e => e.SppdName) .IsUnique(); builder.HasIndex(e => e.Email) .IsUnique(); }
我错过了什么?
您尝试过将您的属性标记为虚拟吗?您需要它来根据文档启用延迟加载导航:
Lazy Loading
EF Core will then enable lazy loading for any navigation property that can be overridden--that is, it must be virtual and on a class that can be inherited from. For example, in the following entities, the Post.Blog and Blog.Posts navigation properties will be lazy-loaded.
我注意到了一些问题。
您的方法仅适用于加载第一级导航属性。
foreach (var propertyName in includeProperties)
{
queryable.Include(propertyName);
}
您必须使用 .ThenInclude()
来加载嵌套的导航属性。不过,这打破了 IEnumerable<string> includeProperties = null
作为构造函数的方法。
第二个问题是您的测试本身。它只检查 .Any()
,但根据测试的名称,这是错误的断言。 (我们不知道测试是否失败,因为导航 属性 从未加载 或 它加载成功,但有零个 Users
。你应该只正在检查导航 属性 是否已加载。类似于以下内容。
DbContext.Entry(createdTeamWithUsers).Navigation("Users").IsLoaded
Include
/ ThenInclude
(以及所有其他 EF Core Queryable
扩展)就像常规的 LINQ Queryable
方法(Select
、Where
, OrderBy
等)修改源IQueryable<>
和return修改后的IQueryable<>
.
在这里您只是忘记了使用生成的查询,所以
queryable.Include(propertyName);
与
效果相同queryable.Where(e => false);
即无效果.
只需将代码更改为
queryable = queryable.Include(propertyName);