次のSOの質問のように、データベースを検索し、ユーザーが任意の基準を動的に追加できるようにするアプリケーションを作成できるアプリケーションを作成しています(約50件): エンティティフレームワーク 。現在、各基準をチェックする検索を行っており、空でない場合はクエリに追加します。
C#
var query = Db.Names.AsQueryable();
if (!string.IsNullOrWhiteSpace(first))
query = query.Where(q => q.first.Contains(first));
if (!string.IsNullOrWhiteSpace(last))
query = query.Where(q => q.last.Contains(last));
//.. around 50 additional criteria
return query.ToList();
このコードは、SQLサーバーで次のようなものを生成します(理解を容易にするために簡略化しました)
[〜#〜] sql [〜#〜]
SELECT
[Id],
[FirstName],
[LastName],
...etc
FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
AND [LastName] LIKE '%last%'
私は今、エンティティフレームワークを介してC#で次のSQLを生成する方法を追加しようとしていますが、[〜#〜] or [〜#〜][〜#〜] and [〜#〜]の代わりに、条件を動的に追加する機能を維持します。
[〜#〜] sql [〜#〜]
SELECT
[Id],
[FirstName],
[LastName],
...etc
FROM [dbo].[Names]
WHERE [FirstName] LIKE '%first%'
OR [LastName] LIKE '%last%' <-- NOTICE THE "OR"
通常、クエリの基準は2つまたは3つの項目よりも大きくなりませんが、それらを1つの巨大なクエリに結合することはオプションではありません。連結、結合、および交差を試みましたが、それらはすべてクエリを複製し、UNIONで結合します。
エンティティフレームワークを使用して動的に生成されたクエリに「OR」条件を追加する簡単でクリーンな方法はありますか?
ソリューションで編集-2015年9月29日
これを投稿してから、これが少し注目されていることに気づいたので、ソリューションを投稿することにしました
// Make sure to add required nuget
// PM> Install-Package LinqKit
var searchCriteria = new
{
FirstName = "sha",
LastName = "hill",
Address = string.Empty,
Dob = (DateTime?)new DateTime(1970, 1, 1),
MaritalStatus = "S",
HireDate = (DateTime?)null,
LoginId = string.Empty,
};
var predicate = PredicateBuilder.False<Person>();
if (!string.IsNullOrWhiteSpace(searchCriteria.FirstName))
{
predicate = predicate.Or(p => p.FirstName.Contains(searchCriteria.FirstName));
}
if (!string.IsNullOrWhiteSpace(searchCriteria.LastName))
{
predicate = predicate.Or(p => p.LastName.Contains(searchCriteria.LastName));
}
// Quite a few more conditions...
foreach(var person in this.Persons.Where(predicate.Compile()))
{
Console.WriteLine("First: {0} Last: {1}", person.FirstName, person.LastName);
}
おそらく、 述語ビルダー のようなものを探しているので、whereステートメントのANDとORを簡単に制御できます。
Dynamic Linq もあります。これにより、SQL文字列のようなWHERE句を送信でき、WHEREの正しい述語に解析されます。
LINQKitとそのPredicateBuilderは非常に汎用性がありますが、いくつかの簡単なユーティリティ(それぞれが他の式操作操作の基盤として機能することができます)でこれをより直接行うことができます。
まず、汎用のExpression Replacer:
_public class ExpressionReplacer : ExpressionVisitor
{
private readonly Func<Expression, Expression> replacer;
public ExpressionReplacer(Func<Expression, Expression> replacer)
{
this.replacer = replacer;
}
public override Expression Visit(Expression node)
{
return base.Visit(replacer(node));
}
}
_
次に、特定の式で1つのパラメーターの使用を別のパラメーターに置き換える簡単なユーティリティメソッド:
_public static T ReplaceParameter<T>(T expr, ParameterExpression toReplace, ParameterExpression replacement)
where T : Expression
{
var replacer = new ExpressionReplacer(e => e == toReplace ? replacement : e);
return (T)replacer.Visit(expr);
}
_
これは、2つの異なる式のラムダパラメータが実際には異なるパラメータであるため、同じ名前であっても必要です。たとえば、q => q.first.Contains(first) || q.last.Contains(last)
で終わる場合、q.last.Contains(last)
のq
はexact sameq
ラムダ式の先頭で提供されます。
次に、_Func<T, TReturn>
_スタイルのLambda式を特定のバイナリ式ジェネレーターと結合できる汎用Join
メソッドが必要です。
_public static Expression<Func<T, TReturn>> Join<T, TReturn>(Func<Expression, Expression, BinaryExpression> joiner, IReadOnlyCollection<Expression<Func<T, TReturn>>> expressions)
{
if (!expressions.Any())
{
throw new ArgumentException("No expressions were provided");
}
var firstExpression = expressions.First();
var otherExpressions = expressions.Skip(1);
var firstParameter = firstExpression.Parameters.Single();
var otherExpressionsWithParameterReplaced = otherExpressions.Select(e => ReplaceParameter(e.Body, e.Parameters.Single(), firstParameter));
var bodies = new[] { firstExpression.Body }.Concat(otherExpressionsWithParameterReplaced);
var joinedBodies = bodies.Aggregate(joiner);
return Expression.Lambda<Func<T, TReturn>>(joinedBodies, firstParameter);
}
_
_Expression.Or
_でこれを使用しますが、数値式を_Expression.Add
_と組み合わせるなど、さまざまな目的で同じメソッドを使用できます。
最後に、すべてをまとめると、次のようになります。
_var searchCriteria = new List<Expression<Func<Name, bool>>();
if (!string.IsNullOrWhiteSpace(first))
searchCriteria.Add(q => q.first.Contains(first));
if (!string.IsNullOrWhiteSpace(last))
searchCriteria.Add(q => q.last.Contains(last));
//.. around 50 additional criteria
var query = Db.Names.AsQueryable();
if(searchCriteria.Any())
{
var joinedSearchCriteria = Join(Expression.Or, searchCriteria);
query = query.Where(joinedSearchCriteria);
}
return query.ToList();
_
エンティティフレームワークを使用して動的に生成されたクエリに「OR」条件を追加する簡単でクリーンな方法はありますか?
はい、これは、実行時にwhere
部分が動的に「無効」または「有効」になる単一のブール式を含む単一のOR
句に依存するだけで実現できます。したがって、LINQKitまたはカスタム述語ビルダーを作成します。
あなたの例を参照して:
_var isFirstValid = !string.IsNullOrWhiteSpace(first);
var isLastValid = !string.IsNullOrWhiteSpace(last);
var query = db.Names
.AsQueryable()
.Where(name =>
(isFirstValid && name.first.Contains(first)) ||
(isLastValid && name.last.Contains(last))
)
.ToList();
_
上記の例でわかるように、以前に評価された施設(例:where
)に基づいて、isFirstValid
- filter式のOR部分を動的に「オン」または「オフ」に切り替えています。
たとえば、isFirstValid
がtrue
ではない場合、name.first.Contains(first)
は short-circuited であり、実行も結果セットにも影響しません。さらに、EF CoreのDefaultQuerySqlGenerator
は、実行前にwhere
内のブール式をさらに 最適化および削減 します(たとえば_false && x || true && y || false && z
_は単純に削減できます) y
単純な静的分析を介して)。
注:前提条件のいずれもtrue
でない場合、結果セットは空になります。これは、あなたの場合に望ましい動作であると思われます。ただし、何らかの理由でIQueryable
ソースからすべての要素を選択したい場合は、true
に評価される式に最終変数を追加できます(例:.Where( ... || shouldReturnAll)
with var shouldReturnAll = !(isFirstValid || isLastValid)
または類似のもの)。
最後の注意:この手法の欠点は、クエリが存在するメソッド本体(より正確にはクエリのwhere
部分)にある「中央集中型」ブール式を作成することを余儀なくされることです。何らかの理由で、述語の構築プロセスを分散化し、引数として注入したり、クエリビルダーを介してチェーンしたい場合は、他の回答で提案されているように、述語ビルダーに固執する必要があります。そうでなければ、この簡単なテクニックをお楽しみください:)