EF core导航属性未加载

4

我正在修改我的应用程序,以便能够在存储库中指定要加载的导航属性。

模型:团队和TeamTunerUser可以在域实体中找到。

存储库:

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导航属性从未被加载,因此第二个断言块失败。
团队在此处进行配置():
    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'.

我尝试过以下方法:
  • Do not specify string but an expression to specify navigation property to load:

    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;
    }
    
  • Explicitly configure the relation for the user entity as well:

    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();
    }
    

我漏掉了什么?

3个回答

8

Include / ThenInclude (以及所有其他EF Core Queryable扩展方法)类似于常规的LINQ Queryable方法(例如SelectWhereOrderBy等),这些方法修改源IQueryable<>返回修改后的IQueryable<>

在这里,您只是忘记使用结果查询,因此

queryable.Include(propertyName);

具有相同的效果

queryable.Where(e => false);

i.e.没有影响。

只需更改代码为

queryable = queryable.Include(propertyName);

现在我感觉很蠢;)现在测试失败了,因为两者都设置了用户。我想在不同的作用域中执行获取操作将会解决这个问题。谢谢! - Philippe
这在Ef Core内存提供程序中不起作用吗?因为我已经完全按照相同的方式做了,但我的导航属性为空。 - kuldeep
@kuldeep,至少在我正在测试的最新官方EFC 3.1.5中,它对我有效。 - Ivan Stoev
我注意到我使用的内部库将查询翻译为Include("Addresses")而不是Include(s => s.Addresses)..我怀疑这可能是问题所在?我有什么遗漏吗?比如启用延迟加载或其他什么东西。 - kuldeep
@kuldeep 当您使用贪婪加载 (Include) 时,惰性加载是无关紧要的。在我的测试中,Include 字符串重载也可以工作。 - Ivan Stoev
@IvanStoev 这对我来说是一个非常简单的时刻。自从三个小时以来,我一直在努力解决问题,但这节省了我的时间和精力。你真是个超级巨星!非常感谢。 - ChiragMS

1
我注意到了一些问题。
您的方法仅适用于加载第一级导航属性。
foreach (var propertyName in includeProperties)
{
    queryable.Include(propertyName);
} 

加载嵌套的导航属性时,必须使用.ThenInclude()。但这破坏了你作为构造函数的IEnumerable<string> includeProperties = null的方法。

第二个问题是关于你的测试本身。它只检查了.Any(),但根据测试名称,这是错误的断言。(我们不知道测试失败是因为导航属性从未加载或者它确实成功加载了,但没有任何Users。你应该只检查导航属性是否已加载。类似以下内容:

DbContext.Entry(createdTeamWithUsers).Navigation("Users").IsLoaded


你是对的,你可以使用链式结构 Team.Users.Whatever 等来加载。但我建议不要采用这种方法,因为它会产生 魔术字符串 的问题,最好使用表达式来获得更多的编译时安全性。 - Brad M
你说得对,我还在设计过程中。在专门的存储库中,始终存在覆盖基本方法的可能性。但是在这里,我想要一个通用的解决方案。 - Philippe

0

你尝试将属性标记为虚拟的了吗?根据文档,你需要这样做才能启用延迟加载导航:

延迟加载

EF Core 将会为任何可以被重写的导航属性启用延迟加载——也就是说,它必须是虚拟的,并且在一个可继承的类上。例如,在以下实体中,Post.Blog 和 Blog.Posts 导航属性将会被延迟加载。

来源:加载相关数据


这个问题是关于EF Core而不是EF6,所以这并不相关。 - DavidG
没有,我尝试将Team.Users声明为虚拟的,但SQL查询和结果仍然相同。我不想使用延迟加载,而是明确指定要与我正在实现的内容一起加载的导航属性。 - Philippe
@DavidG 我的答案在EF的两个版本中都是正确的。我已经更新了我的链接到EF Core,但是它并不重要,因为它无论如何都不能回答op的问题。 - Justin Lessard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接