В приложении I 'm в настоящее время продолжающий работать, существует два вида каждого бизнес-объекта: вид "ActiveRecord" и вид "DataContract". Так, например, был бы:
namespace ActiveRecord {
class Widget {
public int Id { get; set; }
}
}
namespace DataContract {
class Widget {
public int Id { get; set; }
}
}
Слой доступа к базе данных заботится о переводе между семействами: можно сказать этому обновлять a DataContract.Widget
и это волшебно создаст ActiveRecord.Widget
с теми же значениями свойств и сохраняют это вместо этого.
Проблема появилась при попытке осуществить рефакторинг этот слой доступа к базе данных.
Я хочу добавить методы как следующее к слою доступа к базе данных:
// Widget is DataContract.Widget
interface IDbAccessLayer {
IEnumerable GetMany(Expression> predicate);
}
Вышеупомянутое является простым общим использованием, "получают" метод с пользовательским предикатом. Единственное интересное место - то, что я являюсь передающим в дереве выражений вместо лямбды потому что внутри IDbAccessLayer
Я запрашиваю IQueryable
; чтобы сделать это эффективно (думают LINQ к SQL), я должен передать в дереве выражений, таким образом, этот метод просит просто это.
Препятствие: параметр должен быть волшебно преобразован от Expression
к Expression
.
Что мне 'd нравится делать внутри GetMany
:
IEnumerable GetMany(
Expression> predicate)
{
var lambda = Expression.Lambda>(
predicate.Body,
predicate.Parameters);
// use lambda to query ActiveRecord.Widget and return some value
}
Это не будет работать потому что в типичном сценарии, например, если:
predicate == w => w.Id == 0;
... дерево выражений содержит a MemberAccessExpression
экземпляр, который имеет свойство типа MemberInfo
это описывает DataContract.Widget.Id
. Существуют также ParameterExpression
экземпляры и в дереве выражений и в его наборе параметра (predicate.Parameters
) это описывает DataContract.Widget
; все это приведет к ошибкам, так как queryable тело не содержит тот тип виджета, а скорее ActiveRecord.Widget
.
После поиска немного, я нашел System.Linq.Expressions.ExpressionVisitor
(его источник может быть найден здесь в контексте практического руководства), который предлагает удобный способ изменить дерево выражений. В.NET 4, этот класс включен из поля.
Вооруженный этим, я реализовал посетителя. Этот простой посетитель только заботится об изменении типов в членском доступе и выражениях параметра, но это - достаточно функциональности для работы с предикатом w => w.Id == 0
.
internal class Visitor : ExpressionVisitor
{
private readonly Func typeConverter;
public Visitor(Func 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 GetMany(
Expression> predicate)
{
var visitor = new Visitor(...);
var lambda = Expression.Lambda>(
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> 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
создается очень хорошо. Плохие новости - то, что это не работает; это аварийно завершается на мне, когда я пытаюсь использовать его, и сообщения об исключениях действительно не полезны вообще.
Я исследовал лямбду, которую мой код производит и hardcoded лямбда с тем же выражением; они смотрят точно то же. Я провел часы в отладчике, пытающемся найти некоторое различие, но я не могу.
Когда предикат w => w.Id == 0
, lambda
взгляды точно как referenceLambda
. Но последние работы с, например. IQueryable
, в то время как первый не делает; я попробовал это в непосредственном окне отладчика.
Я должен также упомянуть это, когда предикат w => true
, все хорошо работает. Поэтому я предполагаю, что я 'm не делающий достаточно работы в посетителе, но не могу найти, больше ведет для следования.
После принятия во внимание корректных ответов на проблему (два из них ниже; одно короткое, один с кодом), проблема была решена; я поместил код наряду с несколькими важными примечаниями в отдельном ответе для препятствия этого долгого вопроса стать еще длиннее.
Благодаря всем для Ваших ответов и комментариев!
Кажется, вы генерируете выражение параметра дважды, в VisitMember () здесь:
var converted = Expression.MakeMemberAccess(
base.Visit(node.Expression),
activeRecordType.GetProperty(node.Member.Name));
... поскольку base.Visit () окажется в VisitParameter, как я полагаю, и в самом GetMany ():
var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
visitor.Visit(predicate.Body),
predicate.Parameters.Select(p => visitor.Visit(p));
Если вы используете ParameterExpression в теле, это должен быть тот же экземпляр (а не только тот же тип и имя), что и тот, который объявлен для Lambda. Раньше у меня были проблемы с таким сценарием, хотя я думаю, что в результате я просто не смог создать выражение, оно просто выбросило бы исключение. В любом случае вы можете попробовать повторно использовать экземпляр параметра и посмотреть, поможет ли это.
Я думаю, что Linq-To-Sql создаст желаемый SQL, если вы правильно сделаете свои запросы. В данном случае, используя IQueryable
и отложенное выполнение, можно избежать возврата всех записей ActiveRecord.Widget
.
IEnumerable<DataContract.Widget> GetMany(
Func<DataContract.Widget, bool> predicate)
{
// get Widgets
IQueryable<DataContract.Widget> qry = dc.Widgets.Select(w => TODO: CONVERT_TO_DataContract.Widget);
return qry.Where(predicate);
}
Я пробовал простую (не полную) реализацию для изменения выражения p => p.Id == 15
(код ниже). Есть один класс с именем «CrossMapping», который определяет соответствие между исходным и «новым» типами и членами типа.
Для каждого типа выражения существует несколько методов с именем Mutate_XY_Expression
, которые создают новое измененное выражение. Для входных данных метода требуется исходное выражение ( MemberExpression originalExpression
) в качестве модели выражения, список или выражение параметров ( IList
), которые определяются параметрами «родительским» выражением и должен использоваться "родительским" телом и объектом отображения ( Отображение CrossMapping
), который определяет отображение между типами и членами.
Для полной реализации вам может потребоваться больше информации из родительского выражения, чем из параметров. Но узор должен быть таким же.
Образец не реализует шаблон Visitor, как вы знаете - это из-за простоты. Но преград для перехода на них нет.
Надеюсь, это поможет.
Код (C # 4.0):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Linq.Expressions;
namespace ConsoleApplication1 {
public class Product1 {
public int Id { get; set; }
public string Name { get; set; }
public decimal Weight { get; set; }
}
public class Product2 {
public int Id { get; set; }
public string Name { get; set; }
public decimal Weight { get; set; }
}
class Program {
static void Main( string[] args ) {
// list of products typed as Product1
var lst1 = new List<Product1> {
new Product1{ Id = 1, Name = "One" },
new Product1{ Id = 15, Name = "Fifteen" },
new Product1{ Id = 9, Name = "Nine" }
};
// the expression for filtering products
// typed as Product1
Expression<Func<Product1, bool>> q1;
q1 = p => p.Id == 15;
// list of products typed as Product2
var lst2 = new List<Product2> {
new Product2{ Id = 1, Name = "One" },
new Product2{ Id = 15, Name = "Fifteen" },
new Product2{ Id = 9, Name = "Nine" }
};
// type of Product1
var tp1 = typeof( Product1 );
// property info of "Id" property from type Product1
var tp1Id = tp1.GetProperty( "Id", BindingFlags.Public | BindingFlags.Instance );
// delegate type for predicating for Product1
var tp1FuncBool = typeof( Func<,> ).MakeGenericType( tp1, typeof( bool ) );
// type of Product2
var tp2 = typeof( Product2 );
// property info of "Id" property from type Product2
var tp21Id = tp2.GetProperty( "Id", BindingFlags.Public | BindingFlags.Instance );
// delegate type for predicating for Product2
var tp2FuncBool = typeof( Func<,> ).MakeGenericType( tp2, typeof( bool ) );
// mapping object for types and type members
var cm1 = new CrossMapping {
TypeMapping = {
// Product1 -> Product2
{ tp1, tp2 },
// Func<Product1, bool> -> Func<Product2, bool>
{ tp1FuncBool, tp2FuncBool }
},
MemberMapping = {
// Product1.Id -> Product2.Id
{ tp1Id, tp21Id }
}
};
// mutate express from Product1's "enviroment" to Product2's "enviroment"
var cq1_2 = MutateExpression( q1, cm1 );
// compile lambda to delegate
var dlg1_2 = ((LambdaExpression)cq1_2).Compile();
// executing delegate
var rslt1_2 = lst2.Where( (Func<Product2, bool>)dlg1_2 ).ToList();
return;
}
class CrossMapping {
public IDictionary<Type, Type> TypeMapping { get; private set; }
public IDictionary<MemberInfo, MemberInfo> MemberMapping { get; private set; }
public CrossMapping() {
this.TypeMapping = new Dictionary<Type, Type>();
this.MemberMapping = new Dictionary<MemberInfo, MemberInfo>();
}
}
static Expression MutateExpression( Expression originalExpression, CrossMapping mapping ) {
var ret = MutateExpression(
originalExpression: originalExpression,
parameterExpressions: null,
mapping: mapping
);
return ret;
}
static Expression MutateExpression( Expression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
Expression ret;
if ( null == originalExpression ) {
ret = null;
}
else if ( originalExpression is LambdaExpression ) {
ret = MutateLambdaExpression( (LambdaExpression)originalExpression, parameterExpressions, mapping );
}
else if ( originalExpression is BinaryExpression ) {
ret = MutateBinaryExpression( (BinaryExpression)originalExpression, parameterExpressions, mapping );
}
else if ( originalExpression is ParameterExpression ) {
ret = MutateParameterExpression( (ParameterExpression)originalExpression, parameterExpressions, mapping );
}
else if ( originalExpression is MemberExpression ) {
ret = MutateMemberExpression( (MemberExpression)originalExpression, parameterExpressions, mapping );
}
else if ( originalExpression is ConstantExpression ) {
ret = MutateConstantExpression( (ConstantExpression)originalExpression, parameterExpressions, mapping );
}
else {
throw new NotImplementedException();
}
return ret;
}
static Type MutateType( Type originalType, IDictionary<Type, Type> typeMapping ) {
if ( null == originalType ) { return null; }
Type ret;
typeMapping.TryGetValue( originalType, out ret );
if ( null == ret ) { ret = originalType; }
return ret;
}
static MemberInfo MutateMember( MemberInfo originalMember, IDictionary<MemberInfo, MemberInfo> memberMapping ) {
if ( null == originalMember ) { return null; }
MemberInfo ret;
memberMapping.TryGetValue( originalMember, out ret );
if ( null == ret ) { ret = originalMember; }
return ret;
}
static LambdaExpression MutateLambdaExpression( LambdaExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
if ( null == originalExpression ) { return null; }
var newParameters = (from p in originalExpression.Parameters
let np = MutateParameterExpression( p, parameterExpressions, mapping )
select np).ToArray();
var newBody = MutateExpression( originalExpression.Body, newParameters, mapping );
var newType = MutateType( originalExpression.Type, mapping.TypeMapping );
var ret = Expression.Lambda(
delegateType: newType,
body: newBody,
name: originalExpression.Name,
tailCall: originalExpression.TailCall,
parameters: newParameters
);
return ret;
}
static BinaryExpression MutateBinaryExpression( BinaryExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
if ( null == originalExpression ) { return null; }
var newExprConversion = MutateExpression( originalExpression.Conversion, parameterExpressions, mapping );
var newExprLambdaConversion = (LambdaExpression)newExprConversion;
var newExprLeft = MutateExpression( originalExpression.Left, parameterExpressions, mapping );
var newExprRigth = MutateExpression( originalExpression.Right, parameterExpressions, mapping );
var newType = MutateType( originalExpression.Type, mapping.TypeMapping );
var newMember = MutateMember( originalExpression.Method, mapping.MemberMapping);
var newMethod = (MethodInfo)newMember;
var ret = Expression.MakeBinary(
binaryType: originalExpression.NodeType,
left: newExprLeft,
right: newExprRigth,
liftToNull: originalExpression.IsLiftedToNull,
method: newMethod,
conversion: newExprLambdaConversion
);
return ret;
}
static ParameterExpression MutateParameterExpression( ParameterExpression originalExpresion, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
if ( null == originalExpresion ) { return null; }
ParameterExpression ret = null;
if ( null != parameterExpressions ) {
ret = (from p in parameterExpressions
where p.Name == originalExpresion.Name
select p).FirstOrDefault();
}
if ( null == ret ) {
var newType = MutateType( originalExpresion.Type, mapping.TypeMapping );
ret = Expression.Parameter( newType, originalExpresion.Name );
}
return ret;
}
static MemberExpression MutateMemberExpression( MemberExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
if ( null == originalExpression ) { return null; }
var newExpression = MutateExpression( originalExpression.Expression, parameterExpressions, mapping );
var newMember = MutateMember( originalExpression.Member, mapping.MemberMapping );
var ret = Expression.MakeMemberAccess(
expression: newExpression,
member: newMember
);
return ret;
}
static ConstantExpression MutateConstantExpression( ConstantExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
if ( null == originalExpression ) { return null; }
var newType = MutateType( originalExpression.Type, mapping.TypeMapping );
var newValue = originalExpression.Value;
var ret = Expression.Constant(
value: newValue,
type: newType
);
return ret;
}
}
}
Разве ExecuteTypedList не выполняет то, что вы хотите? SubSonic заполнит ваши DTO / POCO. Из блога Роба Коннери:
ExecuteTypedList <> пытается сопоставить имена возвращаемых столбцов с именами свойств переданного типа . В этом примере они точно совпадают - и это не полностью реальный мир. Вы можете решить эту проблему, применив псевдонимы для столбцов - точно так же, как вы бы использовали псевдоним для вызова SQL :
return Northwind.DB.Select("ProductID as 'ID'", "ProductName as 'Name'", "UnitPrice as 'Price'")
.From<Northwind.Product>().ExecuteTypedList<Product>();
Вот ссылка на Writing Decoupled Роба , Тестируемый код с помощью SubSonic 2.1