首页 > 解决方案 > 为什么通过泛型方法创建 Select 后的 Linq“where”表达式在本地求值?

问题描述

我正在使用泛型实现规范模式,并尝试将标准动态应用于映射实体的投影简单(未映射)版本。一般来说,它工作正常,但 Linq 会在我添加Select并应用Where后立即在本地评估表达式。

如果我将其构建为局部变量并传递给相同的Where.

这是简化的相关代码片段:

public interface ISomeable
{
    string Some { get; set; }
}

public static Expression<Func<T, bool>> GetCriteria<T>() where T : class, ISomeable
    {  return e => (e.Some == "Hello"); }


...

Expression<Func<MySimpleEntity, bool>> someCriteria = e => (e.Some == "Hello");
Expression<Func<MySimpleEntity, bool>> someCriteria2 = GetCriteria<MySimpleEntity>();

var query = db.Entities
       .Select(s => new MySimpleEntity { Id = s.Id, Some = s.Some });
// if this Select is removed and MySimpleEntity in both expressions replaced with MyFullEntity, 
// the issue disappears

// this succeeds
var filteredQueryResults = query.Where(someCriteria).ToList();

// at this point, someCriteria2 is set to the same e => (e.Some == "Hello");

// this fails: why is it evaluated locally and not in SQL? <-----
filteredQueryResults = query.Where(someCriteria2).ToList();

// results in a warning:

                /*
                 * 'Microsoft.EntityFrameworkCore.Query.QueryClientEvaluationWarning: 
                 * The LINQ expression 'where (new MySimpleEntity() {Id = [s].Id, Some = [s].Some}.Some == "Hello")' 
                 * could not be translated and will be evaluated locally.'. 
                 */

如何使它生成正确的 SQL 而不是本地评估someCriteria2

我怀疑我需要某种铸造,但不确定在哪里。两者在调试器someCriteriasomeCriteria2看起来完全一样,所以我不知道为什么 Linq 以不同的方式对待它们。

我创建了一个最小的 .Net Core Console 应用程序来重现该案例。完整的要点在这里:

https://gist.github.com/progmars/eeec32a533dbd2e1f85e551db1bc53f8

NuGet 依赖项:Microsoft.EntityFrameworkCore.SqlServer" Version="2.2.6" Microsoft.Extensions.Logging" Version="2.2.0" Microsoft.Extensions.Logging.Console" Version="2.2.0"

一些解释:

这与执行两次相同查询的事实无关。如果我注释掉第一个query.Where(someCriteria).ToList(),第二个调用someCriteria2仍然无法生成有效的 SQL。但是,如果我替换someCriteria2someCriteria第二个查询并让它运行,我会在控制台中得到两个完全有效的 SQL 查询。所以,这一切都与泛型someCriteria2Select投影有关——出于某种原因,Linq 不会将这两个变量视为相同,即使编译器(和调试器监视)认为它们是相同的确切类型。

标签: c#linqentity-framework-core

解决方案


问题类似于无法为基本属性翻译 LINQ 表达式如何在 EF Core 表达式中使用继承的属性?,但在这种情况下,DeclaringTypeReflectedTypeMemberInfo指向ISomeable接口而不是实际的类。

这再次以某种方式混淆了Select场景中的 EF Core。我检查了最新的 EF Core 3.0 预览版,它也不起作用。您可以考虑将其发布到他们的问题跟踪器。

到目前为止,我可以提供的唯一解决方法是使用自定义对表达式进行后处理并将ExpressionVisitor成员访问器绑定到实际类。像这样的东西:

public static partial class ExpressionUtils
{
    public static Expression<T> FixMemberAccess<T>(this Expression<T> source)
    {
        var body = new MemberAccessFixer().Visit(source.Body);
        if (body == source.Body) return source;
        return source.Update(body, source.Parameters);
    }

    class MemberAccessFixer : ExpressionVisitor
    {
        protected override Expression VisitMember(MemberExpression node)
        {
            if (node.Expression != null && node.Expression.Type != node.Member.DeclaringType)
            {
                var member = node.Expression.Type.GetMember(node.Member.Name).Single();
                if (member.ReflectedType != member.DeclaringType)
                    member = member.DeclaringType.GetMember(member.Name).Single();
                return Expression.MakeMemberAccess(node.Expression, member);
            }
            return base.VisitMember(node);
        }
    }
}

现在

var someCriteria2 = GetCriteria<MySimpleEntity>().FixMemberAccess();

将产生准确的表达式作为工作编译时someCriteria表达式,并且没有客户端评估。

注意:您仍然需要class约束以避免先前问题中的强制转换问题并使此解决方法有效。


推荐阅读