Динамическое изменение запроса LINQ на основе связанной таблицы

Я динамически создаю запрос LINQ на основе различных критериев поиска.

В качестве примера предположим, что я ищу таблицу «Автомобили», и у меня есть возможность фильтровать по рейтингу. У меня есть два элемента управления:

  1. Тип сравнения: [Минимум], [Максимум], [Меньше], [Больше] и [Равно].
  2. Значение: значение для сравнения рейтинга.

Таким образом, пользователь может, например, выбрать тип сравнения [По крайней мере] и значение 3, и мой код должен создать запрос, который ограничивает результаты оценками автомобилей, большими или равными 3.

Я нашел отличное решение от VinayC в вопросе Как для реализации функции поиска в C#/ASP.NET MVC. Его метод DynamicWhere() динамически создает часть выражения, которое создаст правильный фильтр.

Моя проблема в том, что мой основной тип запроса — «Автомобиль», но мои рейтинги находятся в отдельной таблице (Automobile.Ratings). Как я могу реализовать этот же метод и фильтровать тип, отличный от моего основного типа запроса?

Спасибо за любые советы.


person Jonathan Wood    schedule 18.04.2014    source источник
comment
Я не совсем уверен, сработает ли это, но я думаю, что лучший способ - использовать простой случай или if-else для создания лямбда-выражения, а затем просто передать его одному экземпляру Where.   -  person evanmcdonnal    schedule 19.04.2014
comment
Является ли рейтинг списком рейтингов? Если да, заинтересованы ли вы в сравнении среднего значения этих оценок?   -  person Jeremy Cook    schedule 19.04.2014
comment
@JeremyCook: рейтинги это список, поскольку он находится на многих сторонах отношений, однако должен быть только один, и меня интересует только первый. (На самом деле, я должен позаботиться о случаях, когда оценки отсутствуют.)   -  person Jonathan Wood    schedule 19.04.2014
comment
@evanmcdonnal: Да, я мог бы разветвляться и делать, как вы предлагаете, но на самом деле у меня много таких условий, и было бы намного чище, если бы я мог сделать что-то вроде метода VinayC DynamicWhere().   -  person Jonathan Wood    schedule 19.04.2014
comment
Я использовал Predicate Builder Пита Монтгомери, и это дало большой эффект. Это лучше (imo), чем albahari, потому что он обрабатывает EF без использования дополнительной библиотеки.   -  person crthompson    schedule 19.04.2014
comment
Тогда вам нужно будет показать код. Ваша модель мне не очевидна по крайней мере.   -  person Jeremy Cook    schedule 19.04.2014
comment
@JonathanWood, что идеальнее; 100 строк простого декларативного кода или 20, которые никто не может понять? Просто скажу, что каждый из них - это 1 лайнер (2, если вы включите if). Их может быть 1000, и с ними все равно будет довольно легко работать.   -  person evanmcdonnal    schedule 19.04.2014
comment
@JeremyCook: в таблице «Рейтинги» есть FK в таблице «Автомобили». Не знаю, почему так устроено. Меня интересует только один рейтинг для каждого автомобиля. Если есть более одной оценки, это неверные данные, и я просто использую первую.   -  person Jonathan Wood    schedule 19.04.2014
comment
@evanmcdonnal: 20 строк кода было бы намного проще поддерживать и они менее подвержены ошибкам.   -  person Jonathan Wood    schedule 19.04.2014
comment
Я бы сказал, что это не так. Неважно, насколько короток код, если его никто не понимает. Учитывая, что вы нигде не можете найти пример того, что вы хотите сделать, я бы сказал, что это довольно безопасная ставка на то, что никто из ваших коллег не поймет этого или не сможет с ним работать.   -  person evanmcdonnal    schedule 19.04.2014
comment
@paqogomez У меня есть реализация PredicateBuilder, как показано здесь, которая еще короче и проще, а также полностью автономна.   -  person Servy    schedule 19.04.2014
comment
Основываясь на всех комментариях, я думаю, что правильное решение — потратить время на интеграцию и работу с PredicateBuilder (например, @Servy). Это позволяет очень легко генерировать динамические запросы linq, и они могут запрашивать элементы настолько глубоко, насколько вам нужно. Почитайте об Альбахари, чтобы понять, что они делают, и не используйте его. :)   -  person crthompson    schedule 19.04.2014
comment
@paqogomez: Я ценю совет. Я надеялся, что мне не нужно начинать снова с самого начала. Но PredicateBuilder определенно заслуживает внимания.   -  person Jonathan Wood    schedule 19.04.2014


Ответы (3)


Поскольку количество операций, которые у вас есть, невелико, конечно и известно во время компиляции, вы можете просто обработать его с помощью switch:

IQueryable<Something> query = GetQuery();

int ratingToComareWith = 1;
string operation = "Equal";

switch (operation)
{
    case ("Equal"):
        query = query.Where(item => item == ratingToComareWith);
        break;
    case ("Less Than"):
        query = query.Where(item => item < ratingToComareWith);
        break;
}
person Servy    schedule 18.04.2014
comment
Да, это простой подход, который будет работать. Однако есть 5 условий, и в моем реальном приложении у меня есть несколько фильтров, которые делают это. Все это складывается. Реализация чего-то вроде метода VinayC DynamicWhere() сделала бы мой код более ясным и лаконичным. - person Jonathan Wood; 19.04.2014
comment
Согласованный. Я бы предложил добавить случай default, который вызывает исключение недопустимой операции. - person Jeremy Cook; 19.04.2014
comment
@JonathanWood Когда вы доходите до точки, когда почти весь ваш запрос определяется динамически, и о нем ничего не известно во время компиляции, вы доходите до точки, когда LINQ действительно не является подходящим инструментом для работы. . Правильный инструмент для этой работы отстает на поколение, в котором вы фактически создаете SQL-запросы. Когда все в вашем запросе LINQ является динамическим, вы тратите гораздо больше времени на создание динамизма и не получаете никаких преимуществ статической типизации. - person Servy; 19.04.2014
comment
@JonathanWood Я согласен с этим последним комментарием. Вы также можете просто построить свой запрос в виде строки и передать его SqlCommand, если вы собираетесь быть таким динамичным. В любом случае, код для этого очень прост. - person evanmcdonnal; 19.04.2014
comment
Я ценю все советы, но а) я работаю над большим приложением, а не создаю его с нуля, поэтому мне нужно работать в рамках существующего приложения, и б) я не новичок в программировании. Я занимаюсь этим больше лет, чем хочу признать. У меня есть хорошее представление о том, как я хотел бы, чтобы это работало. Думаю, я все еще учусь, когда дело доходит до LINQ и выражений. - person Jonathan Wood; 19.04.2014
comment
@JonathanWood Если вы точно знаете, как вы хотите, чтобы это работало, и не открыты для альтернатив, а также являетесь опытным программистом, просто реализуйте свое решение. На самом деле не так уж сложно построить это выражение динамически (учитывая, что у вас уже есть решение, выполняющее 95% работы), это просто не очень хорошая идея. - person Servy; 19.04.2014
comment
@Servy: я сказал, что знаю, как бы я хотел, чтобы это работало, но я не знаю, как это будет реализовано. Ответ на вопрос, на который я разместил ссылку, заключается в том, как бы я хотел, чтобы это работало. Для меня это гениально. Но я не понимаю, как применить его к связанной таблице. Я уже признал, что мне, вероятно, нужно больше узнать о выражениях и LINQ. Я не уверен, что еще вы хотите от меня. - person Jonathan Wood; 19.04.2014

Вот дружественная к Entity Framework альтернатива построению выражений. Конечно, вы захотите проверить column и op, чтобы предотвратить внедрение SQL.

// User provided values
int value = 3;
string column = "Rating";
string op = "<";
// Dynamically built query
db.Database.SqlQuery<Automobile>(@"select distinct automobile.* from automobile
    inner join ratings on .... 
    where [" + column + "] " + op + " @p0", value);
person Jeremy Cook    schedule 18.04.2014
comment
Да, так мы делали это в старые времена. :) Однако парень, который настроил наш доступ к БД, предотвратил этот тип запроса. И я могу понять почему. Компилятор не может проверить наличие ошибок. Если столбец каким-то образом изменится и вы пропустите этот запрос, то ваш клиент обнаружит его как ошибку времени выполнения в поле. - person Jonathan Wood; 19.04.2014
comment
@JonathanWood То же самое можно сказать и о запросе, в котором вы динамически строите выражения на основе строк, как вы предлагаете. В предложенном вами запросе не будет больше проверки статического типа, чем в этом запросе, и вам потребуется гораздо больше работы для построения. Если вы использовали LINQ для написания запроса, который действительно можно проверить во время компиляции, тогда это добавляет ценности. - person Servy; 19.04.2014
comment
Я думаю, что мы, возможно, были мудрее в старые времена. Вы можете сделать столбцы и открытые перечисления ... Хорошо, может быть, не op :-) Но случай переключения в перечислении, таком как ops, имеет проверку во время компиляции и является безопасной ставкой. - person Jeremy Cook; 19.04.2014
comment
@Servy: Если это правда, то только для очень небольшой части запроса. В частности, где я указываю имя столбца в виде строки. Подавляющее большинство моих запросов по-прежнему требует проверки компилятором. - person Jonathan Wood; 19.04.2014
comment
Если это так, похоже, вы пишете что-то довольно интересное @JonathanWood ... Что-то аналогичное $filters OData. - person Jeremy Cook; 19.04.2014
comment
@JeremyCook: Ничего особенного. У меня есть такие утверждения, как if (!String.IsNullOrWhiteSpace(HotelName)) query = query.Where(h => h.Description.Contains(HotelName.Trim()));. Если столбец Description был удален или переименован, компилятор это обнаружит. - person Jonathan Wood; 19.04.2014
comment
Если это так, я не уверен, что понимаю, что вы проблема. Может помочь просмотр некоторого C#-подобного псевдокода, который адекватно демонстрирует, что у вас есть в настоящее время и что вы хотели бы иметь. Похоже, вы ищете какой-то метод, который принимает некоторые параметры и выдает некоторый результат. Я не уверен, каковы параметры и каким должен быть ожидаемый результат. - person Jeremy Cook; 19.04.2014

Здесь представлен метод создания условий для вложенных коллекций или типов для linq-to-entities. Перестроено под ваши нужды:

        public static Expression GetCondition(Expression parameter, object value, OperatorComparer operatorComparer, params string[] properties)
{
    Expression resultExpression = null;
    Expression childParameter, navigationPropertyPredicate;
    Type childType = null;

    if (properties.Count() > 1)
    {
        //build path
        parameter = Expression.Property(parameter, properties[0]);
        var isCollection = typeof(IEnumerable).IsAssignableFrom(parameter.Type);
        //if it´s a collection we later need to use the predicate in the methodexpressioncall
        if (isCollection)
        {
            childType = parameter.Type.GetGenericArguments()[0];
            childParameter = Expression.Parameter(childType, childType.Name);
        }
        else
        {
            childParameter = parameter;
        }
        //skip current property and get navigation property expression recursivly
        var innerProperties = properties.Skip(1).ToArray();
        navigationPropertyPredicate = GetCondition(childParameter, test, innerProperties);
        if (isCollection)
        {
            //build methodexpressioncall
            var anyMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" && m.GetParameters().Length == 2);
            anyMethod = anyMethod.MakeGenericMethod(childType);
            navigationPropertyPredicate = Expression.Call(anyMethod, parameter, navigationPropertyPredicate);
            resultExpression = MakeLambda(parameter, navigationPropertyPredicate);
        }
        else
        {
            resultExpression = navigationPropertyPredicate;
        }
    }
    else
    {
       var childProperty = parameter.Type.GetProperty(properties[0]);
       var left = Expression.Property(parameter, childProperty);
       var right = Expression.Constant(value,value.GetType());
       if(!new List<OperatorComparer>    {OperatorComparer.Contains,OperatorComparer.StartsWith}.Contains(operatorComparer))
        {
            navigationPropertyPredicate = Expression.MakeBinary((ExpressionType)operatorComparer,left, right);
        }
        else
        {
            var method = GetMethod(childProperty.PropertyType, operatorComparer); //get property by enum-name from type
            navigationPropertyPredicate = Expression.Call(left, method, right);
        }
        resultExpression = MakeLambda(parameter, navigationPropertyPredicate);
    }
    return resultExpression;
}

private static MethodInfo GetMethod(Type type,OperatorComparer operatorComparer)
{
    var method = type.GetMethod(Enum.GetName(typeof(OperatorComparer),operatorComparer));
    return method;
} 

public enum OperatorComparer
{
    Equals = ExpressionType.Equal,
    Contains,
    StartsWith,
    GreaterThan = ExpressionType.GreaterThan
    ....

}

private static Expression MakeLambda(Expression parameter, Expression predicate)
{
    var resultParameterVisitor = new ParameterVisitor();
    resultParameterVisitor.Visit(parameter);
    var resultParameter = resultParameterVisitor.Parameter;
    return Expression.Lambda(predicate, (ParameterExpression)resultParameter);
}

private class ParameterVisitor : ExpressionVisitor
{
    public Expression Parameter
    {
        get;
        private set;
    }
    protected override Expression VisitParameter(ParameterExpression node)
    {
        Parameter = node;
        return node;
    }
}
    }

Вы можете заменить params string[] на params Expression(Func(T,object)), если хотите. Потребуется дополнительная работа, чтобы сделать это таким образом. Вам нужно будет определить вложенные коллекции с синтаксисом, например

item => item.nestedCollection.Select(nested => nested.Property)

и переписать выражение с помощью expressionvisitor.

person user3411327    schedule 19.04.2014