Entity Framework 核心中表达式树的参数化查询

Parameterized Query from an Expression Tree in Entity Framework Core

我正在尝试在通用存储库(.NET Core 3.1 + EF Core 3.1)中实现动态过滤器 通过构建表达式树,但生成的 SQL 查询从未被参数化(我正在通过 appsettings.json 中的 "Microsoft.EntityFrameworkCore.Database.Command": "Information" 验证生成的查询,并在 [=31= 中有 EnableSensitiveDataLogging ])

构建表达式树的代码如下(为简单起见,此处仅使用字符串值):

    public static IQueryable<T> WhereEquals<T>(IQueryable<T> query, string propertyName, object propertyValue)
    {
        var pe = Expression.Parameter(typeof(T));

        var property = Expression.PropertyOrField(pe, propertyName);
        var value = Expression.Constant(propertyValue);

        var predicateBody = Expression.Equal(
            property,
            value
        );

        var whereCallExpression = Expression.Call(
            typeof(Queryable),
            "Where",
            new[] { typeof(T) },
            query.Expression,
            Expression.Lambda<Func<T, bool>>(predicateBody, new ParameterExpression[] { pe })
        );

        return query.Provider.CreateQuery<T>(whereCallExpression);
    }

该方法有效,但值总是包含在生成的 SQL 查询中,我担心它会导致 SQL 注入。

这是生成的查询示例:

Microsoft.EntityFrameworkCore.Database.Command: Information: Executed DbCommand (33ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Name], [p].[FirstName], [p].[Created], [p].[CreatedBy], [p].[Updated], [p].[UpdatedBy]
FROM [Persons] AS [p]
WHERE [p].[Name] = N'smith'

从 EF 团队成员 (@divega) 那里找到了一个可能的答案: Force Entity Framework to use SQL parameterization for better SQL proc cache reuse, 设法让它与 Where 方法一起工作,但生成的 SQL 仍然相同。

尝试使用 System.Linq.Dynamic.Core, 但它有同样的问题(生成的 SQL 查询未参数化)。

有没有办法强制 Entity Framework Core 从表达式树生成参数化查询?

您提供的 link 说明 EF 使用 SQL 参数作为变量值,因此如果您创建变量引用而不是为传入的值创建 Expression.Constant (在 C# 中始终是字段引用),然后您将获得参数化查询。最简单的解决方案似乎是复制编译器如何处理 lambda 外部作用域变量引用,即创建一个 class 对象来保存值,并引用它。

Expression.Constant 不同,获取 object 参数的实际类型并不容易,因此将其更改为通用类型:

public static class IQueryableExt {
    private sealed class holdPropertyValue<T> {
        public T v;
    }

    public static IQueryable<T> WhereEquals<T, TValue>(this IQueryable<T> query, string propertyName, TValue propertyValue) {
        // p
        var pe = Expression.Parameter(typeof(T), "p");

        // p.{propertyName}
        var property = Expression.PropertyOrField(pe, propertyName);
        var holdpv = new holdPropertyValue<TValue> { v = propertyValue };
        // holdpv.v
        var value = Expression.PropertyOrField(Expression.Constant(holdpv), "v");

        // p.{propertyName} == holdpv.v
        var whereBody = Expression.Equal(property, value);
        // p => p.{propertyName} == holdpv.v
        var whereLambda = Expression.Lambda<Func<T, bool>>(whereBody, pe);

        // Queryable.Where(query, p => p.{propertyName} == holdpv.v)
        var whereCallExpression = Expression.Call(
            typeof(Queryable),
            "Where",
            new[] { typeof(T) },
            query.Expression,
            whereLambda
        );

        // query.Where(p => p.{propertyName} == holdpv.v)
        return query.Provider.CreateQuery<T>(whereCallExpression);
    }
}

如果您需要传入一个 object,将转换添加到正确的类型(不会影响生成的 SQL)比动态创建更简单holdPropertyValue 的正确类型并为其赋值,因此:

public static IQueryable<T> WhereEquals2<T>(this IQueryable<T> query, string propertyName, object propertyValue) {
    // p
    var pe = Expression.Parameter(typeof(T), "p");
    // p.{propertyName}
    var property = Expression.PropertyOrField(pe, propertyName);

    var holdpv = new holdPropertyValue<object> { v = propertyValue };
    // Convert.ChangeType(holdpv.v, p.{propertyName}.GetType())
    var value = Expression.Convert(Expression.PropertyOrField(Expression.Constant(holdpv), "v"), property.Type);

    // p.{propertyName} == Convert.ChangeType(holdpv.v, p.{propertyName}.GetType())
    var whereBody = Expression.Equal(property, value);
    // p => p.{propertyName} == Convert.ChangeType(holdpv.v, p.{propertyName}.GetType())
    var whereLambda = Expression.Lambda<Func<T, bool>>(whereBody, pe);

    // Queryable.Where(query, p => p.{propertyName} == Convert.ChangeType(holdpv.v, p.{propertyName}.GetType()))
    var whereCallExpression = Expression.Call(
        typeof(Queryable),
        "Where",
        new[] { typeof(T) },
        query.Expression,
        whereLambda
    );

    // query.Where(query, p => p.{propertyName} == Convert.ChangeType(holdpv.v, p.{propertyName}.GetType()))
    return query.Provider.CreateQuery<T>(whereCallExpression);
}