Отключение дерева выражений предиката для нацеливания на другой тип

вступление

В приложении, над которым я сейчас работаю, есть два вида каждого бизнес-объекта: вид «ActiveRecord» и вид «DataContract». Так, например, было бы:

namespace ActiveRecord {
    class Widget {
        public int Id { get; set; }
    }
}

namespace DataContract {
    class Widget {
        public int Id { get; set; }
    }
}

Слой доступа к базе данных заботится о переводе между семействами: вы можете сказать ему обновитьDataContract.Widget и это волшебным образом создастActiveRecord.Widget с теми же значениями свойств и сохраните их.

Проблема возникла при попытке провести рефакторинг этого уровня доступа к базе данных.

Эта проблема

Я хочу добавить методы, подобные следующим, к слою доступа к базе данных:

// Widget is DataContract.Widget

interface IDbAccessLayer {
    IEnumerable<Widget> GetMany(Expression<Func<Widget, bool>> predicate);
}

Выше приведен простой метод get общего назначения с пользовательским предикатом. Единственный интересный момент заключается в том, что я передаю дерево выражений вместо лямбды, потому что внутриIDbAccessLayer Я запрашиваюIQueryable<ActiveRecord.Widget>; чтобы сделать это эффективно (подумайте LINQ to SQL) мне нужно передать дерево выражений, поэтому этот метод запрашивает именно это.

Загвоздка: параметр должен быть магическим образом преобразован изExpression<Func<DataContract.Widget, bool>> дляExpression<Func<ActiveRecord.Widget, bool>>.

Попытка решения

Что я хотел бы сделать внутриGetMany является:

IEnumerable<DataContract.Widget> GetMany(
    Expression<Func<DataContract.Widget, bool>> predicate)
{
    var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
        predicate.Body,
        predicate.Parameters);

    // use lambda to query ActiveRecord.Widget and return some value
}

Это не будет работать, потому что в типичном сценарии, например, если:

predicate == w => w.Id == 0;

... дерево выражений содержитMemberAccessExpression экземпляр, который имеет свойство типаMemberInfo это описываетDataContract.Widget.Id, Это такжеParameterExpression экземпляры как в дереве выражений, так и в его коллекции параметров (predicate.Parameters) которые описываютDataContract.Widget; все это приведет к ошибкам, так как запрашиваемое тело не содержит виджет такого типа, а скорееActiveRecord.Widget.

Немного поиска, я нашелSystem.Linq.Expressions.ExpressionVisitor (его источник можно найтиВот в контексте с практическими рекомендациями), который предлагает удобный способ изменить дерево выражений. В .NET 4 этот класс включен из коробки.

Вооружившись этим, я реализовал посетителя. Этот простой посетитель заботится только об изменении типов доступа к элементам и выражений параметров, но этого достаточно для работы с предикатом.w => w.Id == 0.

internal class Visitor : ExpressionVisitor
{
    private readonly Func<Type, Type> typeConverter;

    public Visitor(Func<Type, Type> typeConverter)
    {
        this.typeConverter = typeConverter;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        var dataContractType = node.Member.ReflectedType;
        var activeRecordType = this.typeConverter(dataContractType);

        var converted = Expression.MakeMemberAccess(
            base.Visit(node.Expression),
            activeRecordType.GetProperty(node.Member.Name));

        return converted;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        var dataContractType = node.Type;
        var activeRecordType = this.typeConverter(dataContractType);

        return Expression.Parameter(activeRecordType, node.Name);
    }
}

С этим посетителемGetMany будет выглядеть так:

IEnumerable<DataContract.Widget> GetMany(
    Expression<Func<DataContract.Widget, bool>> predicate)
{
    var visitor = new Visitor(...);
    var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
        visitor.Visit(predicate.Body),
        predicate.Parameters.Select(p => visitor.Visit(p));

    var widgets = ActiveRecord.Widget.Repository().Where(lambda);

    // This is just for reference, see below
    Expression<Func<ActiveRecord.Widget, bool>> referenceLambda = 
        w => w.Id == 0;

    // Here we 'd convert the widgets to instances of DataContract.Widget and
    // return them -- this has nothing to do with the question though.
}
Результаты

Хорошая новость в том, чтоlambda построен просто отлично. Плохая новость в том, что это не работает; это взрывает меня, когда я пытаюсь использовать это, и сообщения об исключениях действительно бесполезны вообще.

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

Когда предикатw => w.Id == 0, lambda выглядит так же, какreferenceLambda, Но последний работает, например, сIQueryable<T>.Whereв то время как первый не делает; Я попробовал это в ближайшем окне отладчика.

Я должен также упомянуть, что когда предикатw => trueвсе работает нормально. Поэтому я предполагаю, что я не выполняю достаточную работу в гостях, но я не могу найти больше подсказок, чтобы следовать.

Окончательное решение

После учета правильных ответов на проблему (два из них ниже; один короткий, другой с кодом) проблема была решена; Я положил код вместе с несколькими важными заметками вотдельный ответ чтобы этот длинный вопрос не стал еще длиннее.

Спасибо всем за ваши ответы и комментарии!

Ответы на вопрос(6)

Ваш ответ на вопрос