1

左側と右側のプロパティ (オペランド) を表す 2 つ (または必要に応じて 4 つ) のラムダを受け取り、述語を生成する関数を記述して、「メタ述語」を作成することは可能ですか。次のコード サンプルのようなもの:

public Expression<Func<Something,bool>> StringEquals<Something>(Expression<Something> leftOperand, Expression<Something> leftOperandSelector){
    return (rightOperandSelector, leftOperandSelector) => (
        (rightOperandSelector == leftOperandSelector) 
        || (
            string.IsNullOrEmpty(rightOperandSelector) && 
            string.IsNullOrEmpty(leftOperandSelector)
        )
    );
}

また:

public Expression<Func<Something,bool>> DatesActive<Something>(Expression<Something> startDateOperandSelector, Expression<Something> endDateOperandSelector){
    return (startDateOperandSelector, endDateOperandSelector) => (
    (startDatePropertySelector >= DateTime.Now) 
    && (endDatePropertySelector <= DateTime.Now)
    );
}

各側の SomeStringProperty またはstartDatePropertySelectorまたはendDatePropertySelectorがラムダによって定義されている場所は? 述語式のオペランドを動的に渡す方法がわかりません。

理想的には、次のようにインライン化できるようにしたいと考えています。

return new Expression<Func<Request,bool>>[]{
    r => (r.Id != request.Id) && (!r.Reviewed),
    StringEquals(r => r.VendorName, request=>request.VendorName),
    NotExpired(r => r.ContractStart, request=>request.ContractEnd),                        
    ...
};

*誰かがこれにアプローチする最善の方法についてアイデアを持っていますか? 私の関心は、複数のプロパティで同じ式を繰り返し使用している場合に、簡単に使用できる「メタ」式を作成することです。参照/説明のために具体的な例を示します。これがばかげているかどうか、そしてより良いアプローチがあるかどうかを知ることに非常にオープンであり、喜んで学びます。*

必要に応じて、以下に背景を追加します。

背景:この Expression のより単純な形式が整った後、次のように C# を解釈する代わりに、EntityFramework が同等性を期待どおりに処理しないという事実を処理するために、リファクタリングを余儀なくされました (rightSide.SomeStringProperty == leftSide.SomeStringProperty)

(
    (rightSide.SomeStringProperty IS NULL AND leftSide.SomeStringProperty IS NULL)
    OR (rightSide.SomeStringProperty = leftSide.SomeStringProperty)
)

より文字通りに翻訳すると、次のようになります。

(rightSide.SomeStringProperty = leftSide.SomeStringProperty)

もちろん、両側がnullの場合は値を返しません。どうやらこれは EF6 で修正されているようです (修正: @Slauma は、これは UseCSharpNullComparisonBehavior を介して EF5 で利用できることを指摘しています。私は EF4 を使用しており、このリリースにアップグレードすることはできません。)

次のようなコードの繰り返しは避けたいです。

            var where = new Expression<Func<Request,bool>>[]{
                    r => (r.Id != request.Id) && (!r.Reviewed) 
                    && (
                        (r.Address == request.Address) 
                        || (string.IsNullOrEmpty(r.Address) && string.IsNullOrEmpty(request.Address))
                    )
                    && (
                        (r.City == request.City) 
                        || (string.IsNullOrEmpty(r.City) && string.IsNullOrEmpty(request.City))
                    )
                    && (
                        (r.Province == request.Province) 
                        || (string.IsNullOrEmpty(r.Province) && string.IsNullOrEmpty(request.Province))
                    )
                    && (
                        (r.PostalCode == request.PostalCode) 
                        || (string.IsNullOrEmpty(r.PostalCode) && string.IsNullOrEmpty(request.PostalCode))
                    )
                    && (
                        (r.Website == request.Website) 
                        || (string.IsNullOrEmpty(r.Website) && string.IsNullOrEmpty(request.Website))
                    )
                };
4

2 に答える 2

2

System.Linq.Expressions名前空間を使用して、手動で式を作成できます。投稿した 2 つの例では、次のように動作するはずです。

public static Expression<Func<T, bool>> StringEquals<T>(Expression<Func<T, string>> leftOperand, Expression<Func<T, string>> rightOperand)
{
   var p = leftOperand.Parameters[0];
   var leftOperandBody = leftOperand.Body;
   var rightOperandBody = ReplacementVisitor.Transform(rightOperand, rightOperand.Parameters[0], p);

   var isNullOrEmptyMethod = typeof(string).GetMethod("IsNullOrEmpty");
   var leftNullOrEmpty = Expression.Call(isNullOrEmptyMethod, leftOperandBody);
   var rightNullOrEmpty = Expression.Call(isNullOrEmptyMethod, rightOperandBody);
   var bothNullOrEmpty = Expression.AndAlso(leftNullOrEmpty, rightNullOrEmpty);
   var areEqual = Expression.Equal(leftOperandBody, rightOperandBody);
   var body = Expression.OrElse(bothNullOrEmpty, areEqual);

   return Expression.Lambda<Func<T, bool>>(body, p);
}

public static Expression<Func<T, bool>> DatesActive<T>(Expression<Func<T, DateTime>> startDate, Expression<Func<T, DateTime>> endDate)
{
   var p = startDate.Parameters[0];
   var startDateBody = startDate.Body;
   var endDateBody = ReplacementVisitor.Transform(endDate, endDate.Parameters[0], p);

   var nowProperty = typeof(DateTime).GetProperty("Now");
   var nowValue = Expression.Property(null, nowProperty);
   var startValid = Expression.GreaterThanOrEqual(startDateBody, nowValue);
   var endValid = Expression.LessThanOrEqual(endDateBody, nowValue);
   var body = Expression.AndAlso(startValid, endValid);

   return Expression.Lambda<Func<T, bool>>(body, p);
}

internal sealed class ReplacementVisitor : ExpressionVisitor
{
   private IList<ParameterExpression> SourceParameters { get; set; }
   private Expression Find { get; set; }
   private Expression Replace { get; set; }

   public static Expression Transform(LambdaExpression source, Expression find, Expression replace)
   {
      var visitor = new ReplacementVisitor
      {
         SourceParameters = source.Parameters,
         Find = find,
         Replace = replace,
      };

      return visitor.Visit(source.Body);
   }

   private Expression ReplaceNode(Expression node)
   {
      return (node == Find) ? Replace : node;
   }

   protected override Expression VisitConstant(ConstantExpression node)
   {
      return ReplaceNode(node);
   }

   protected override Expression VisitBinary(BinaryExpression node)
   {
      var result = ReplaceNode(node);
      if (result == node) result = base.VisitBinary(node);
      return result;
   }

   protected override Expression VisitParameter(ParameterExpression node)
   {
      if (SourceParameters.Contains(node)) return ReplaceNode(node);
      return SourceParameters.FirstOrDefault(p => p.Name == node.Name) ?? node;
   }
}
于 2013-06-25T18:48:32.053 に答える
0

これは、Richard が示したように、式ツリーを動的に構成することで実現できますが、それほど複雑である必要はありません。具体的には、ビジター パターンは不要なオーバーヘッドを追加します。最初の例に対する次の簡単な解決策を検討してください。

using E = System.Linq.Expressions.Expression;

/* ... */

private static readonly MethodInfo IsNullOrEmptyMethod = typeof(string).GetMethod("IsNullOrEmpty");

public static Expression<Func<bool>> StringEquals(Expression<Func<String>> leftAccessor, Expression<Func<String>> rightAccessor)
{
    var left = E.Parameter(typeof(string), "left");
    var right = E.Parameter(typeof(string), "left");

    // () => {
    //     string left = leftAccessor();
    //     string right = rightAccessor();
    //     
    //     return left == right ||
    //            string.IsNullOrEmpty(left) && string.IsNullOrEmpty(right);
    // }

    return E.Lambda<Func<bool>>(
        E.Block(
            new[] { left, right },
            E.Assign(left, E.Invoke(leftAccessor)),
            E.Assign(right, E.Invoke(rightAccessor)),
            E.OrElse(
                E.Equal(left, right),
                E.AndAlso(
                    E.Call(IsNullOrEmptyMethod, left),
                    E.Call(IsNullOrEmptyMethod, right)))));
}

同様の手法を適用して、2 番目の例の解決策を考案できます。汎用パラメーターがないことに注意してください。プロパティを含む実際のアイテムを公開する必要はありません。このメソッドを使用して、同じオブジェクトの 2 つのプロパティを比較できます。2 つの異なるオブジェクトの同じプロパティ。または任意の値。

ラムダ コンパイラは、 のような単純なアクセサをインライン展開することに注意してください() => r.Address。アクセサーは式自体であるため、これを簡単に行うことができます。

編集:あなたの質問をもう一度読んで、Entity Framework を使用していることがわかります。EF のクエリ プロバイダーが、アクセサーをインライン化できるほど洗練されているかどうかはわかりません。そうでない場合、これは機能しない可能性があります。その場合、リチャードが回答で行ったように、手動で変換する必要がある場合があります。これがあなたのケースでうまくいくかどうか聞いてみたいと思います. EFを使用していない人にとっては役立つかもしれないので、この答えはどちらにしても残しておきます。

また、@svick がコメントで指摘しているように、EF はほぼ確実にブロック式をサポートしていません。おそらく、次のようにラムダを作成する必要があります。

return E.Lambda<Func<bool>>(
    E.OrElse(
        E.Equal(E.Invoke(leftAccessor), E.Invoke(rightAccessor)),
        E.AndAlso(
            E.Call(IsNullOrEmptyMethod, E.Invoke(leftAccessor)),
            E.Call(IsNullOrEmptyMethod, E.Invoke(rightAccessor)))));
于 2013-06-26T14:40:15.360 に答える