Введение
В приложении, над которым я сейчас работаю, есть два типа каждого бизнес-объекта: тип "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
построен просто отлично. Плохая новость в том, что она не работает; это взорвало меня, когда я пытаюсь его использовать, и сообщения об исключении действительно не помогают вообще.
Я изучил lambda, который производит мой код, и жестко закодированную лямбду с тем же выражением; они выглядят точно так же. Я провел часы в отладчике, пытаясь найти какую-то разницу, но не могу.
Когда предикат w => w.Id == 0
, lambda
выглядит точно как referenceLambda
. Но последнее работает, например, IQueryable<T>.Where
, а первый - нет; Я пробовал это в ближайшем окне отладчика.
Я должен также упомянуть, что когда предикат w => true
, все работает нормально. Поэтому я предполагаю, что я не делаю достаточно работы у посетителя, но я больше не могу найти, чтобы следовать.
Окончательное решение
После принятия правильных ответов на проблему (два из них ниже, один короткий, один с кодом), проблема была решена; Я добавил код вместе с несколькими важными примечаниями в отдельный ответ, чтобы этот длинный вопрос не становился еще длиннее.
Спасибо всем за ваши ответы и комментарии!