【问题标题】:Entity Framework extension method ICollection / IQueryable实体框架扩展方法 ICollection / IQueryable
【发布时间】:2017-03-12 08:52:50
【问题描述】:

我需要多次检查相同的特定条件(where 子句):

return _ctx.Projects.Where(p => p.CompanyId == companyId &&
                          (p.Type == Enums.ProjectType.Open || 
                           p.Invites.Any(i => i.InviteeId == userId))).ToList()

'&&'后面的部分将导致用户无法检索受限项目。

我想将此检查抽象为一个函数。将来这些条件可能会发生变化,我不想替换所有 LINQ 查询。

我使用以下扩展方法做到了这一点:

public static IQueryable<Project> IsVisibleForResearcher(this IQueryable<Project> projects, string userId)
{
    return projects.Where(p => p.Type == Enums.ProjectType.Open || 
                               p.Invites.Any(i => i.InviteeId == userId));
}

现在我可以将 LINQ 查询更改为:

return _ctx.Projects.Where(p => p.CompanyId == companyId)
                    .IsVisibleForResearcher(userId).ToList()

这会生成相同的 SQL 查询。现在,当我想在另一个具有项目的 DbSet 上使用此扩展方法时,我的问题就开始了。

假设一家公司有项目。而且我只想检索用户至少可以看到一个项目的公司。

return _ctx.Companies
       .Where(c => c.Projects.Where(p => 
            p.Type == Enums.ProjectType.Open || 
            p.Invites.Any(i => i.InviteeId == userId))
       .Any())

这里我也喜欢用扩展方法。

return _ctx.Companies
       .Where(c => c.Projects.AsQueryable().IsVisibleForCompanyAccount(userId).Any())

这会引发以下异常:

“System.NotSupportedException”类型的异常发生在 Remotion.Linq.dll 但未在用户代码中处理

附加信息:无法解析表达式“c.Projects.AsQueryable()”:当前不支持“System.Linq.Queryable.AsQueryable”方法的这种重载。

比我创建了以下扩展方法:

public static IEnumerable<Project> IsVisibleForResearcher(this ICollection<Project> projects, string userId)
{
    return projects.Where(p => p.Type == Enums.ProjectType.Open || 
                               p.Invites.Any(i => i.InviteeId == userId));
}

但这也不起作用。

有人有想法吗? 或者朝着正确的方向迈出一步。

顺便说一句,我在 .NET Core 上使用 Entity Framework Core

更新

使用Expression&lt;Func&lt;&gt;&gt; 会导致同样的异常:

目前不支持“System.Linq.Queryable.AsQueryable”。

更新 2

感谢@ivan-stoev 提供解决方案。 我还有一个问题。我还想检索“可见”项目的数量。

我通过这样做修复了它:

var companies = _ctx.Companies
                .WhereAny(c => c.Projects, Project.IsProjectVisibleForResearcher(userId))
                .Select(c => new CompanyListDto
                {
                    Id = c.Id,
                    Name = c.Name,
                    LogoId = c.LogoId,                   
                    ProjectCount = _ctx.Projects.Where(p => p.CompanyId == c.Id)
                                       .Count(Project.IsProjectVisibleForResearcher(userId))     
                });

但我找不到只使用c.Projects 而不是ctx.Projects.Where(p =&gt; p.CompanyId == c.Id) 的方法

生成的 SQL 是正确的,但我想避免这种不必要的检查。

真诚地, 布莱希特

【问题讨论】:

    标签: c# .net entity-framework linq entity-framework-core


    【解决方案1】:

    IQueryable&lt;T&gt; 查询表达式中使用表达式/自定义方法一直存在问题,并且需要一些表达式树后处理。例如,LinqKit 为此提供了AsExpandableInvokeExpand 自定义扩展方法。

    虽然不是很笼统,但这里是针对您的示例用例的解决方案,不使用 3rd 方包。

    首先,提取方法中谓词的表达式部分。 IMO 的逻辑位置是 Project 类:

    public class Project
    {
        // ...
        public static Expression<Func<Project, bool>> IsVisibleForResearcher(string userId)
        {
            return p => p.Type == Enums.ProjectType.Open ||
                        p.Invites.Any(i => i.InviteeId == userId);
        }
    }
    

    然后,像这样创建一个自定义扩展方法:

    public static class QueryableExtensions
    {
        public static IQueryable<T> WhereAny<T, E>(this IQueryable<T> source, Expression<Func<T, IEnumerable<E>>> elements, Expression<Func<E, bool>> predicate)
        {
            var body = Expression.Call(
                typeof(Enumerable), "Any", new Type[] { typeof(E) },
                elements.Body, predicate);
            return source.Where(Expression.Lambda<Func<T, bool>>(body, elements.Parameters));
        }
    }
    

    有了这个设计,你现在的扩展方法就不需要了,因为Projects查询你可以使用:

    var projects = _ctx.Projects
        .Where(p => p.CompanyId == companyId)
        .Where(Project.IsVisibleForResearcher(userId));
    

    对于Companies

    var companies = _ctx.Companies
        .WhereAny(c => c.Projects, Project.IsVisibleForResearcher(userId)); 
    

    更新:此解决方案非常有限,因此如果您有不同的用例(尤其是在第二次更新中的 Select 表达式内),您最好求助于一些 3rd 方包.例如,这里是 LinqKit 解决方案:

    // LInqKit requires expressions to be put into variables
    var projects = Linq.Expr((Company c) => c.Projects);
    var projectFilter = Project.IsVisibleForResearcher(userId);
    var companies = db.Companies.AsExpandable()
        .Where(c => projects.Invoke(c).Any(p => projectFilter.Invoke(p)))
        .Select(c => new CompanyListDto
        {
            Id = c.Id,
            Name = c.Name,
            LogoId = c.LogoId,
            ProjectCount = projects.Invoke(c).Count(p => projectFilter.Invoke(p))
        });
    

    【讨论】:

    • 完全有效!非常感谢您的回答!祝你今天过得愉快!打算研究一下解决方案,以便下次需要它时,我可以自己制作类似的东西。
    • 还有一个小问题。我在原始问题中将其发布为“更新 2”。生成的 SQL 是正确的,但我不喜欢我必须这样做的方式。不要觉得自己有义务检查!
    • 检查没问题。问题是解决方案非常有限(正如我在开头提到的那样) - 仅适用于顶级可查询对象。一旦您需要另一种方法(嵌套WhereCount 等),尤其是在您的更新中的Select 内部,您会发现您需要越来越多的帮助者。可能您真的必须查看 AutoMapper 的投影 (ProjectTo) 和/或 LinqKit 的表达式组合。
    • 您的更新成功了!自己尝试过,但我不知道 LinqKit 需要变量中的表达式。你(再次)拯救了我的一天!
    • 请注意使用LinqKit可以直接使用c.Projects(不需要projects表达式)并将projects.Invoke(c).Any(p =&gt; projectFilter.Invoke(p))改为c.Projects.Any(projectFilter.Compile())
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-10-11
    • 2016-07-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-05
    • 1970-01-01
    相关资源
    最近更新 更多