Динамично модифициране на LINQ заявка въз основа на свързана таблица

Създавам динамично LINQ заявка въз основа на различни критерии за търсене.

Като пример, да речем, че търся таблица с автомобили и имам опция за филтриране по оценки. Имам два контрола:

  1. Тип сравнение: [Поне], [Най-много], [По-малко от], [По-голямо от] и [Равно].
  2. Стойност: Стойността, с която да сравните оценката.

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

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

Проблемът ми е, че основният ми тип заявка е Automobile, но оценките ми са в отделна таблица (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: Да, бих могъл да се разклоня и да направя, както предлагате, но всъщност имам много такива условия и би било много по-чисто, ако мога да направя нещо по линия на метода DynamicWhere() на VinayC.   -  person Jonathan Wood    schedule 19.04.2014
comment
Използвах предикатен конструктор на Пийт Монтгомъри с голям ефект. По-добре е (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
Бих казал, че това не е така. Няма значение колко кратък е кодът, ако никой не го разбира. Като се има предвид, че не можете да намерите пример за това, което искате да направите никъде в SO, бих казал, че е доста сигурен залог, че никой от вашите колеги няма да го разбере или да може да работи с него.   -  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 условия и в действителното ми приложение имам няколко филтъра, които правят това. Всичко това се добавя. Прилагането на нещо подобно на метода DynamicWhere() на VinayC би направило моя код много по-ясен и по-сбит. - 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 и Expressions. - 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
Да, така го правехме в старите времена. :) Въпреки това, човекът, който настрои достъпа ни до DB, предотврати този тип заявка. И разбирам защо. Компилаторът не може да проверява за грешки. Ако колона се промени по някакъв начин и вие пропуснете тази заявка, вашият клиент ще я намери като грешка по време на изпълнение в полето. - person Jonathan Wood; 19.04.2014
comment
@JonathanWood Точно същото може да се каже за заявка, в която динамично изграждате изразите въз основа на низове, както предлагате да се направи. Предложената от вас заявка няма да има повече статична проверка на типа от тази заявка, а вашата ще изисква доста повече работа за конструиране. Ако сте използвали LINQ, за да напишете заявка, която действително може да бъде валидирана по време на компилиране, тогава това добавя стойност. - person Servy; 19.04.2014
comment
Мисля, че може би сме били по-мъдри в старите времена. Бихте могли да направите колона и op enums ... ОК, може би не op :-) Но случай на превключване на enum като 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

Ето метод за изграждане на условия за вложени колекции или типове за връзка към обекти. Преструктуриран за вашите нужди:

        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)

и пренапишете израза с помощта на expressvisitor.

person user3411327    schedule 19.04.2014