私が現在取り組んでいるアプリケーションには、「ActiveRecord」の種類と「DataContract」の種類の2種類の各ビジネスオブジェクトがあります。したがって、たとえば、次のようになります。
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;
...式ツリーには、DataContract.Widget.Id
を記述するタイプMemberAccessExpression
のプロパティを持つMemberInfo
インスタンスが含まれています。式ツリーとそのパラメータコレクション(predicate.Parameters
)の両方に、DataContract.Widget
を記述するParameterExpression
インスタンスもあります。クエリ可能な本体にはそのタイプのウィジェットではなく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
の場合、すべてが正常に機能することにも言及する必要があります。したがって、私は訪問者で十分な仕事をしていないと思いますが、これ以上フォローするリードを見つけることができません。
問題に対する正解(以下に2つ、短いもの、コード付きのもの)を考慮した後、問題は解決しました。この長い質問がさらに長くなるのを防ぐために、コードといくつかの重要なメモを 別の回答 に入れました。
あなたの答えとコメントをみんなに感謝します!
ここのVisitMember()で、パラメーター式を2回生成しているようです。
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に対して宣言されているインスタンスと同じインスタンス(同じタイプと名前だけでなく)である必要があります。以前、この種のシナリオで問題が発生しましたが、結果として式を作成できなかったと思いますが、例外がスローされます。いずれにせよ、パラメータインスタンスを再利用してみて、それが役立つかどうかを確認してください。
トリッキーな部分は、新しいラムダの式ツリーに存在するParameterExpression
インスタンスは、_IEnumerable<ParameterExpression>
_で渡されるのと同じ同じインスタンスでなければならないということです。 _Expression.Lambda
_のパラメーター。
TransformPredicateLambda
内で、「タイプコンバーター」関数としてt => typeof(TNewTarget)
を指定していることに注意してください。これは、この特定のケースでは、すべてのパラメーターとメンバーアクセスがその1つの特定のタイプであると想定できるためです。より高度なシナリオでは、追加のロジックが必要になる場合があります。
コード:
_internal class DbAccessLayer {
private static Expression<Func<TNewTarget, bool>>
TransformPredicateLambda<TOldTarget, TNewTarget>(
Expression<Func<TOldTarget, bool>> predicate)
{
var lambda = (LambdaExpression) predicate;
if (lambda == null) {
throw new NotSupportedException();
}
var mutator = new ExpressionTargetTypeMutator(t => typeof(TNewTarget));
var Explorer = new ExpressionTreeExplorer();
var converted = mutator.Visit(predicate.Body);
return Expression.Lambda<Func<TNewTarget, bool>>(
converted,
lambda.Name,
lambda.TailCall,
Explorer.Explore(converted).OfType<ParameterExpression>());
}
private class ExpressionTargetTypeMutator : ExpressionVisitor
{
private readonly Func<Type, Type> typeConverter;
public ExpressionTargetTypeMutator(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);
}
}
}
/// <summary>
/// Utility class for the traversal of expression trees.
/// </summary>
public class ExpressionTreeExplorer
{
private readonly Visitor visitor = new Visitor();
/// <summary>
/// Returns the enumerable collection of expressions that comprise
/// the expression tree rooted at the specified node.
/// </summary>
/// <param name="node">The node.</param>
/// <returns>
/// The enumerable collection of expressions that comprise the expression tree.
/// </returns>
public IEnumerable<Expression> Explore(Expression node)
{
return this.visitor.Explore(node);
}
private class Visitor : ExpressionVisitor
{
private readonly List<Expression> expressions = new List<Expression>();
protected override Expression VisitBinary(BinaryExpression node)
{
this.expressions.Add(node);
return base.VisitBinary(node);
}
protected override Expression VisitBlock(BlockExpression node)
{
this.expressions.Add(node);
return base.VisitBlock(node);
}
protected override Expression VisitConditional(ConditionalExpression node)
{
this.expressions.Add(node);
return base.VisitConditional(node);
}
protected override Expression VisitConstant(ConstantExpression node)
{
this.expressions.Add(node);
return base.VisitConstant(node);
}
protected override Expression VisitDebugInfo(DebugInfoExpression node)
{
this.expressions.Add(node);
return base.VisitDebugInfo(node);
}
protected override Expression VisitDefault(DefaultExpression node)
{
this.expressions.Add(node);
return base.VisitDefault(node);
}
protected override Expression VisitDynamic(DynamicExpression node)
{
this.expressions.Add(node);
return base.VisitDynamic(node);
}
protected override Expression VisitExtension(Expression node)
{
this.expressions.Add(node);
return base.VisitExtension(node);
}
protected override Expression VisitGoto(GotoExpression node)
{
this.expressions.Add(node);
return base.VisitGoto(node);
}
protected override Expression VisitIndex(IndexExpression node)
{
this.expressions.Add(node);
return base.VisitIndex(node);
}
protected override Expression VisitInvocation(InvocationExpression node)
{
this.expressions.Add(node);
return base.VisitInvocation(node);
}
protected override Expression VisitLabel(LabelExpression node)
{
this.expressions.Add(node);
return base.VisitLabel(node);
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
this.expressions.Add(node);
return base.VisitLambda(node);
}
protected override Expression VisitListInit(ListInitExpression node)
{
this.expressions.Add(node);
return base.VisitListInit(node);
}
protected override Expression VisitLoop(LoopExpression node)
{
this.expressions.Add(node);
return base.VisitLoop(node);
}
protected override Expression VisitMember(MemberExpression node)
{
this.expressions.Add(node);
return base.VisitMember(node);
}
protected override Expression VisitMemberInit(MemberInitExpression node)
{
this.expressions.Add(node);
return base.VisitMemberInit(node);
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
this.expressions.Add(node);
return base.VisitMethodCall(node);
}
protected override Expression VisitNew(NewExpression node)
{
this.expressions.Add(node);
return base.VisitNew(node);
}
protected override Expression VisitNewArray(NewArrayExpression node)
{
this.expressions.Add(node);
return base.VisitNewArray(node);
}
protected override Expression VisitParameter(ParameterExpression node)
{
this.expressions.Add(node);
return base.VisitParameter(node);
}
protected override Expression VisitRuntimeVariables(RuntimeVariablesExpression node)
{
this.expressions.Add(node);
return base.VisitRuntimeVariables(node);
}
protected override Expression VisitSwitch(SwitchExpression node)
{
this.expressions.Add(node);
return base.VisitSwitch(node);
}
protected override Expression VisitTry(TryExpression node)
{
this.expressions.Add(node);
return base.VisitTry(node);
}
protected override Expression VisitTypeBinary(TypeBinaryExpression node)
{
this.expressions.Add(node);
return base.VisitTypeBinary(node);
}
protected override Expression VisitUnary(UnaryExpression node)
{
this.expressions.Add(node);
return base.VisitUnary(node);
}
public IEnumerable<Expression> Explore(Expression node)
{
this.expressions.Clear();
this.Visit(node);
return expressions.ToArray();
}
}
}
_
式p => p.Id == 15
を変更するための単純な(完全ではない)実装を試しました(コードは以下にあります)。元のタイプと「新しい」タイプおよびタイプメンバー間のマッピングを定義する「CrossMapping」という名前のクラスが1つあります。
すべての式タイプにMutate_XY_Expression
という名前の方法がいくつかあり、新しい変異式を作成します。メソッド入力には、式のモデルとして元の式(MemberExpression originalExpression
)、「親」式によってパラメーターが定義され、「親」の本体で使用されるリストまたはパラメーター式(IList<ParameterExpression> parameterExpressions
)が必要です。タイプとメンバー間のマッピングを定義するマッピングオブジェクト(CrossMapping mapping
)。
完全に実装するには、パラメータよりも親の式からより多くの情報が必要になる場合があります。しかし、パターンは同じでなければなりません。
ご存知のように、サンプルはビジターパターンを実装していません。これは単純だからです。しかし、それらに変換することへの障壁はありません。
私はそれが役立つことを願っています。
コード(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;
}
}
}
Jon自身の答え 上記は素晴らしいので、メソッド呼び出しや定数式などを処理するように拡張して、次のような式でも機能するようにしました。
x => x.SubObjects
.AsQueryable()
.SelectMany(y => y.GrandChildObjects)
.Any(z => z.Value == 3)
また、必要なのはParameterExpressionsだけなので、ExpressionTreeExplorer
も廃止しました。
コードは次のとおりです(更新:変換が完了したらキャッシュをクリアします)
public class ExpressionTargetTypeMutator : ExpressionVisitor
{
private readonly Func<Type, Type> typeConverter;
private readonly Dictionary<Expression, Expression> _convertedExpressions
= new Dictionary<Expression, Expression>();
public ExpressionTargetTypeMutator(Func<Type, Type> typeConverter)
{
this.typeConverter = typeConverter;
}
// Clear the ParameterExpression cache between calls to Visit.
// Not thread safe, but you can probably fix it easily.
public override Expression Visit(Expression node)
{
bool outermostCall = false;
if (false == _isVisiting)
{
this._isVisiting = true;
outermostCall = true;
}
try
{
return base.Visit(node);
}
finally
{
if (outermostCall)
{
this._isVisiting = false;
_convertedExpressions.Clear();
}
}
}
protected override Expression VisitMember(MemberExpression node)
{
var sourceType = node.Member.ReflectedType;
var targetType = this.typeConverter(sourceType);
var converted = Expression.MakeMemberAccess(
base.Visit(node.Expression),
targetType.GetProperty(node.Member.Name));
return converted;
}
protected override Expression VisitParameter(ParameterExpression node)
{
Expression converted;
if (false == _convertedExpressions.TryGetValue(node, out converted))
{
var sourceType = node.Type;
var targetType = this.typeConverter(sourceType);
converted = Expression.Parameter(targetType, node.Name);
_convertedExpressions.Add(node, converted);
}
return converted;
}
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.IsGenericMethod)
{
var convertedTypeArguments = node.Method.GetGenericArguments()
.Select(this.typeConverter)
.ToArray();
var genericMethodDefinition = node.Method.GetGenericMethodDefinition();
var newMethod = genericMethodDefinition.MakeGenericMethod(convertedTypeArguments);
return Expression.Call(newMethod, node.Arguments.Select(this.Visit));
}
return base.VisitMethodCall(node);
}
protected override Expression VisitConstant(ConstantExpression node)
{
var valueExpression = node.Value as Expression;
if (null != valueExpression)
{
return Expression.Constant(this.Visit(valueExpression));
}
return base.VisitConstant(node);
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
return Expression.Lambda(this.Visit(node.Body), node.Name, node.TailCall, node.Parameters.Select(x => (ParameterExpression)this.VisitParameter(x)));
}
}