Отключение дерева выражений предиката для нацеливания на другой тип
В приложении, над которым я сейчас работаю, есть два вида каждого бизнес-объекта: вид «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
все работает нормально. Поэтому я предполагаю, что я не выполняю достаточную работу в гостях, но я не могу найти больше подсказок, чтобы следовать.
После учета правильных ответов на проблему (два из них ниже; один короткий, другой с кодом) проблема была решена; Я положил код вместе с несколькими важными заметками вотдельный ответ чтобы этот длинный вопрос не стал еще длиннее.
Спасибо всем за ваши ответы и комментарии!