Использование Funcs в выражениях?

Задний план

У меня есть пример теста, который проходит, но возникает ошибка, и я не уверен, почему. Я хотел бы выяснить, что происходит, но я новичок в построении Expression и не хочу делать никаких предположений.

Это для механизма фильтрации поиска. Он использует реализацию ServiceStack PredicateBuilder. По сути, у меня есть список значений, которые я передаю, и я хочу, чтобы он построил дерево выражений. Раньше я делал это только с Func<T<bool>>, но понял, что мне нужно закончить с Expression<Func<T<bool>>>. облом.

Цель

Поисковые фильтры, созданные из повторно используемых типов поисковых фильтров, которые построены из Funcs и Expressions, что позволяет мне передавать имя поля из объекта вместе со значениями, которые я должен сопоставить, и получить что-то, против чего мы можем запустить оператор Where(). .

Код/выпуск

Общий фильтр "nullable bool", который я пробую, устанавливает допустимые элементы и возвращает функцию, предназначенную для помощи в фильтрации:

public class NullableBoolFilter : IGenericSearchFilter<bool?>
{
    public Func<bool?, bool> GetFilterFunc(string valuesToProcess)
    {
        var acceptableValues = new List<bool?>();

        if (string.IsNullOrWhiteSpace(valuesToProcess))
        {
            // all values acceptable
            acceptableValues = new List<bool?>{true, false, null};
        }
        else
        {
            if (!valuesToProcess.Contains("0") && !valuesToProcess.Contains("1"))
            {
                throw new ArgumentException("Invalid Nullable boolean filter attribute specified");
            }
            if (valuesToProcess.Contains("0"))
            {
                acceptableValues.Add(false);

            }
            if (valuesToProcess.Contains("1"))
            {
                acceptableValues.Add(true);
            }
        }

        Func<bool?, bool> returnFunc = delegate(bool? item) { return acceptableValues.Any(x=>x == item); };
        return returnFunc;
    }
}

Затем у меня есть еще один фильтр, который наследуется от NullableBoolFilter и пытается использовать Func:

public class ClaimsReportIsMDLFilter : NullableBoolFilter, ISearchFilter<vSEARCH_ClaimsReport>
{
    public Expression<Func<vSEARCH_ClaimsReport, bool>> GetExpression(string valuesToProcess)
    {
        var theFunc = base.GetFilterFunc(valuesToProcess);

        Expression<Func<vSEARCH_ClaimsReport, bool>> mdlMatches = item => theFunc(item.IsMDL);

        var predicate = PredicateBuilder.False<vSEARCH_ClaimsReport>();
        predicate = predicate.Or(mdlMatches);

        return predicate;

    }
}

Проходит следующий тест:

public class ClaimsReportIsMDLFilterTests
{
    // ReSharper disable InconsistentNaming
    private readonly vSEARCH_ClaimsReport ItemWithMDL = new vSEARCH_ClaimsReport { IsMDL = true };
    private readonly vSEARCH_ClaimsReport ItemWithoutMDL = new vSEARCH_ClaimsReport { IsMDL = false };
    private readonly vSEARCH_ClaimsReport ItemWithNullMDL = new vSEARCH_ClaimsReport { IsMDL = null };
    // ReSharper restore InconsistentNaming

    [Fact]
    public void WithSearchValueOf1_HidesNonMDLAndNull()
    {

        var sut = this.GetCompiledExpressionForValues("1");

        sut.Invoke(ItemWithMDL).Should().BeTrue();
        sut.Invoke(ItemWithoutMDL).Should().BeFalse();
        sut.Invoke(ItemWithNullMDL).Should().BeFalse();

    }

    private Func<vSEARCH_ClaimsReport, bool> GetCompiledExpressionForValues(string searchValue)
    {
        return new ClaimsReportIsMDLFilter().GetExpression(searchValue).Compile();
    }

}

Проблема

Когда я на самом деле пытаюсь запустить это, я получаю сообщение об ошибке:

переменная 'param' типа 'vSEARCH_ClaimsReport', на которую ссылается область видимости '', но она не определена

Мне понятно, почему это может произойти - во время его оценки у меня нет реального объекта для передачи в Func. Тем не менее, я не понимаю, почему мои тесты могут пройти, но на самом деле это не так.

Вопросы

  • Почему мои тесты могут пройти, но я все еще получаю эту ошибку?
  • Как, черт возьми, я должен начать пытаться исправить это?
  • Есть ли относительно простой способ взять это Func и превратить его в Expression, в которое я могу передать поле?
  • Нужно ли мне отказываться от общей идеи фильтра и каждый класс вручную добавлять выражения в PredicateBuilder на основе переданного ввода? Это выполнимо, но кажется, что работа может быть сокращена больше.

person SeanKilleen    schedule 06.05.2014    source источник
comment
Ваш NullableBoolFilter вращается вокруг делегатов, а ваш ClaimsReportIsMDLFilter вращается вокруг деревьев выражений. Это почти наверняка проблема. Если вы хотите использовать деревья выражений, вам нужно использовать их последовательно - как только у вас есть дерево выражений, которое ссылается на какой-то локальный Func<>, ничто не сможет преобразовать его в SQL (или что-то еще).   -  person Jon Skeet    schedule 06.05.2014
comment
@JonSkeet спасибо за (быстрый!) ответ. Этот код в настоящее время работает: gist.github.com/SeanKilleen/0a8ffa639c4916af0585 учитывая это, существует ли любой способ создать что-то, что я мог бы передать в поле логического значения, допускающего значение NULL, и каждый раз оценивать его на основе одного и того же процесса? Или когда я использую выражения, я как бы зацикливаюсь на этом? У меня есть несколько подобных фильтров, поэтому я просто стараюсь работать с ними одинаково и сохранять СУХОЙ, насколько это возможно.   -  person SeanKilleen    schedule 06.05.2014


Ответы (1)


Почему мои тесты могут пройти [...]

Потому что ваш тест просто компилирует выражение в код, который оно представляет, и вызывает его. Ему не нужно на самом деле анализировать дерево выражений и смотреть, что делает код, который он представляет, он просто запускает его и гарантирует, что вывод правильный.

Почему [...] я все еще получаю эту ошибку?

Потому что, когда вы на самом деле его используете, это не просто выполнение кода; скорее, он просматривает дерево выражений, чтобы попытаться определить, что делает код, чтобы его можно было преобразовать во что-то другое, а не для того, чтобы его можно было запустить как код C#.

Ваше выражение ничего не делает, кроме вызова делегата. У кого-то, кто просматривает дерево выражений, нет возможности заглянуть внутрь делегата и узнать, что он делает. Знание того, что вы вызываете другой метод, нельзя перевести на другой язык.

Как, черт возьми, я должен начать пытаться исправить это?

Вам нужно создать Expression с самого начала, а не генерировать Func, а затем просто создавать Expression, который его вызывает.

Есть ли относительно простой способ взять этот Func и превратить его в выражение, в которое я могу передать поле?

Нет. Вам нужно будет извлечь IL-код функции, декомпилировать его в код C#, а затем создать Expression объекта для представления этого кода. Этого почти просто не произойдет.


Вам в значительной степени понадобится, чтобы GetFilterFunc возвращал Expression, чтобы заставить это работать. К счастью, это довольно легко сделать, учитывая то, что у вас есть. Вам просто нужно изменить сигнатуру метода и заменить последние две строки на следующие:

return item => acceptableValues.Any(x => x == item);

И вуаля. Лямбда может быть скомпилирована в объект Expression, а не в делегат, в зависимости от контекста, поэтому, если возвращаемый тип метода — Expression<Func<bool?,bool>>, это то, что вы получите.

Теперь, чтобы использовать это в GetExpression. Во-первых, PredicateBuilder на самом деле ничего не делает. Добавление OR FALSE к вашему выражению ничего не меняет. Все это может уйти. Все, что нам остается, это использовать Expression<Func<bool?,bool>> и изменить его на Expression<Func<vSEARCH_ClaimsReport, bool>> путем извлечения логического свойства. Для этого требуется немного больше работы для выражений, чем для делегатов. Вместо того, чтобы просто вызывать выражение, нам нужно проделать немного больше работы, чтобы составить их. Мы хотим написать метод для выполнения этой операции:

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

И это зависит от использования следующего метода для замены всех экземпляров одного выражения другим:

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

Это заменяет все экземпляры параметра второго выражения телом первого выражения, эффективно встраивая это выражение во второе. Остальное просто заменяет все параметры новым одним параметром и снова обертывает его в лямбду.

Теперь, когда у нас это есть, наш метод довольно прост:

public Expression<Func<vSEARCH_ClaimsReport, bool>> GetExpression(
    string valuesToProcess)
{
    Expression<Func<vSEARCH_ClaimsReport, bool?>> selector = 
        item => item.IsMDL;
    return selector.Compose(base.GetFilterFunc(valuesToProcess));
}
person Servy    schedule 06.05.2014
comment
@Servy, не могу отблагодарить тебя за это объяснение. Я думаю, что я все еще могу что-то упустить. В этой сути я пытаюсь реализовать это: gist.github.com/SeanKilleen/1fa043025fed436eecc3. Я получаю ту же ошибку. Я сохранил PredicateBuilder, потому что мы выполняем поиск Or внутри более крупного поиска And, и OrmLite ожидает, что это произойдет в AFAIK. Возможно, это вызывает проблему? Еще раз спасибо за отличное объяснение; с нетерпением жду понимания этого лучше. - person SeanKilleen; 06.05.2014
comment
@SeanKilleen Я предполагаю, что у вас есть какая-то проблема с конфигурацией, которую нужно решить с вашим поставщиком запросов, и я не знаком с той, о которой вы упомянули. Фактическое выражение нужно было очистить, и это должно генерировать что-то, с чем может работать провайдер запросов, но если есть какие-либо проблемы, кроме построения правильного Expression, то я не могу вам с этим помочь. - person Servy; 06.05.2014
comment
@Servy понятно, спасибо! Я просто хотел убедиться, что я как-то не напортачил с реализацией. Я проверю это дальше. Спасибо! - person SeanKilleen; 06.05.2014
comment
@SeanKilleen Вы пробовали использовать методы .And и .Or, предоставляемые Ormlite? - person DanB; 09.05.2014
comment
@DanB мы фактически создаем наши собственные выражения, чтобы мы могли их стандартизировать и использовать в методах PredicateBuilder And() и Or Ormlite. Это позволяет нам видеть, какие столбцы и значения поступают, автоматически находить все фильтры и генерировать выражения, которые можно передать в Ormlite. Мы создаем отдельные или фильтры, а затем и их все вместе. У меня это сработало; опубликую свое решение в отдельном ответе, когда у меня будет шанс. - person SeanKilleen; 09.05.2014