Параметризованный запрос из дерева выражений в Entity Framework Core - PullRequest
0 голосов
/ 03 марта 2020

Я пытаюсь реализовать динамический c фильтр в общем хранилище c (. NET Core 3.1 + EF Core 3.1) путем построения дерева выражений, но сгенерированный запрос SQL никогда не параметризуется (Я проверяю сгенерированный запрос через "Microsoft.EntityFrameworkCore.Database.Command": "Information" в appsettings. json и имею EnableSensitiveDataLogging в Startup.cs)

Код для построения дерева выражений следующий (для простоты работы только со строковыми значениями):

    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): Вынудить Entity Framework использовать SQL параметризацию для улучшения SQL pro c повторное использование кэша , ему удалось работать с методом Where, но сгенерированный SQL все тот же.

Попытался использовать System.Linq.Dynami c .Core, но у него та же проблема (генерируется SQL запрос не параметризован).

Есть ли способ заставить Entity Framework Core сгенерировать параметризованный запрос из дерева выражений?

1 Ответ

0 голосов
/ 04 марта 2020

Ссылка, которую вы указали, объясняет, что EF использует параметр SQL для значений переменных, поэтому вместо создания Expression.Constant для переданного значения, если вы создаете ссылку на переменную (которая в C# всегда является полем ссылка), тогда вы получите параметризованный запрос. Кажется, самое простое решение - скопировать то, как компилятор обрабатывает ссылку на внешнюю переменную области видимости лямбды, то есть создает объект класса для хранения значения и ссылки на него.

В отличие от Expression.Constant, это нелегко чтобы получить фактический тип параметра object, изменив его на общий тип c:

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

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

        var predicateBody = Expression.Equal(
            property,
            value
        );
        var wf = Expression.Lambda<Func<T, bool>>(predicateBody, new ParameterExpression[] { pe });


        //var v = (int)propertyValue;
        //Expression<Func<Accounts,bool>> wf = (Accounts a) => a.Actid == v;

        wf.Dump();

        var whereCallExpression = Expression.Call(
            typeof(Queryable),
            "Where",
            new[] { typeof(T) },
            query.Expression,
            wf
        );

        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, property.Type)
    var value = Expression.Convert(Expression.PropertyOrField(Expression.Constant(holdpv), "v"), property.Type);

    // p.propertyName == Convert.ChangeType(holdpv.v, property.Type)
    var predicateBody = Expression.Equal(
        property,
        value
    );
    // p => p.propertyName == Convert.ChangeType(holdpv.v, property.Type)
    var wf = Expression.Lambda<Func<T, bool>>(predicateBody, new ParameterExpression[] { pe });

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

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