web-dev-qa-db-ja.com

LINQ Selectの動的な列と値

さまざまな理由で、ユーザーが列と値の選択に基づいてデータベースからアイテムを選択できるようにする必要があります。たとえば、テーブルがある場合:

Name   | Specialty       | Rank
-------+-----------------+-----
John   | Basket Weaving  | 12
Sally  | Basket Weaving  | 6
Smith  | Fencing         | 12

ユーザーは1つ、2つ、またはそれ以上の列を要求する場合があり、要求する列は異なる場合があります。たとえば、ユーザーはSpecialty == Basket WeavingおよびRank == 12. What I do currently is gather the user's request and create a list ofKeyValuePairwhere theキーis the column name and theValue`は列の目的の値です。

class UserSearch
{
    private List<KeyValuePair<string, string> criteria = new List<KeyValuePair<string, string>>();

    public void AddTerm(string column, string value)
    {
        criteria.Add(new KeyValuePair<string, string>(column, value);
    }

    public void Search()
    {
        using (var db = new MyDbContext())
        {
            // Search for entries where the column's (key's) value matches
            // the KVP's value.
            var query = db.MyTable.Where(???);
        }
    }
}

/* ... Somewhere else in code, user adds terms to their search 
 * effectively performing the following ... */
UserSearch search = new UserSearch();
search.Add("Specialty", "Basket Weaving");
search.Add("Rank", "12");

このKeyValuePairのリストを使用して、すべての基準に一致するデータベースアイテムを最も簡単に選択するにはどうすればよいですか?

using (var db = new MyDbContext)
{
    // Where each column name (key) in criteria matches 
    // the corresponding value in criteria.
    var query = db.MyTable.Where(???);
}

編集:私がそれを助けることができるなら、私は生のSQLの代わりにEntityFrameworkを使いたいです。

PDATE:近づいてきています。テーブルからすべての値をダウンロードしたら、LINQを使用する方法を発見しました。テーブル内のすべてをダウンロードするため、これは明らかに理想的ではありません。したがって、最後のステップは、テーブル全体を毎回ダウンロードする必要がない方法を見つけることだと思います。これが私がやっていることの説明です:

テーブルのすべての行

db.MyTable.ToList().Where(e => ...

列が基準に一致するかどうかを表すブール値のリストを作成します。

criteria.Select(c => e.GetType()?.GetProperty(c.Key)?.GetValue(e)?.ToString() == c.Value)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                         Basically just gets the value of specific column
                                            by string

次に、このブールリストがすべて真であるかどうかを確認します

.All(c => c == true)

完全なコードの例を以下に示します。

// This class was generated from the ADO.NET Entity Data Model template 
// from the database. I have stripped the excess stuff from it leaving 
// only the properties.
public class MyTableEntry
{
    public string Name { get; }
    public string Specialty { get; }
    public string Rank { get; }
}

class UserSearch
{
    private List<KeyValuePair<string, string> criteria = new List<KeyValuePair<string, string>>();

    public void AddTerm(string column, string value)
    {
        criteria.Add(new KeyValuePair<string, string>(column, value);
    }

    public async Task<List<MyTableEntry>> Search()
    {
        using (var db = new MyDbContext())
        {
            var entries = await db.MyTable.ToListAsync();
            var matches = entries.Where(e => criteria.Select(c => e.GetType()
                                                                  ?.GetProperty(c.Key)
                                                                  ?.GetValue(e)
                                                                  ?.ToString() == c.Value)
                                                      .All(c => c == true));

            return matches.ToList();
        }
    }
}

私の問題はこのコードセグメントにあるようです:

e.GetType()?.GetProperty(c.Key)?.GetValue(e)?.ToString()

私は式ツリーに慣れていないので、おそらく答えはそれらにあります。動的LINQを試すこともできます。

11
thndrwrks

列とフィルターは動的であるため、Dynamic LINQライブラリが役立ちます

NuGet: https://www.nuget.org/packages/System.Linq.Dynamic/

ドキュメント: http://dynamiclinq.azurewebsites.net/

using System.Linq.Dynamic; //Import the Dynamic LINQ library

//The standard way, which requires compile-time knowledge
//of the data model
var result = myQuery
    .Where(x => x.Field1 == "SomeValue")
    .Select(x => new { x.Field1, x.Field2 });

//The Dynamic LINQ way, which lets you do the same thing
//without knowing the data model before hand
var result = myQuery
    .Where("Field1=\"SomeValue\"")
    .Select("new (Field1, Field2)");

別の解決策は、Eval Expression.NETを使用することです。これにより、実行時にC#コードを動的に評価できます。

using (var ctx = new TestContext())
{
    var query = ctx.Entity_Basics;

    var list = Eval.Execute(@"
q.Where(x => x.ColumnInt < 10)
 .Select(x => new { x.ID, x.ColumnInt })
 .ToList();", new { q = query });
}

免責事項:私はプロジェクトの所有者です Eval Expression.NET

Edit:回答コメント

パラメータ値のタイプは、プロパティタイプと互換性がある必要があります。たとえば、「Rank」プロパティがINTの場合、INTと互換性のある型のみが機能します(文字列ではありません)。

明らかに、このメソッドをリファクタリングして、アプリケーションにより適したものにする必要があります。しかし、ご覧のとおり、Entity Frameworkの非同期メソッドでも簡単に使用できます。

選択(戻り値の型)もカスタマイズする場合は、リフレクションを使用して非同期結果を取得するか、ToList()の代わりにExecuteAsyncを使用する必要があります。

public async Task<List<Entity_Basic>> DynamicWhereAsync(CancellationToken cancellationToken = default(CancellationToken))
{
    // Register async extension method from entity framework (this should be done in the global.asax or STAThread method
    // Only Enumerable && Queryable extension methods exists by default
    EvalManager.DefaultContext.RegisterExtensionMethod(typeof(QueryableExtensions));

    // GET your criteria
    var tuples = new List<Tuple<string, object>>();
    tuples.Add(new Tuple<string, object>("Specialty", "Basket Weaving"));
    tuples.Add(new Tuple<string, object>("Rank", "12"));

    // BUILD your where clause
    var where = string.Join(" && ", tuples.Select(Tuple => string.Concat("x.", Tuple.Item1, " > p", Tuple.Item1)));

    // BUILD your parameters
    var parameters = new Dictionary<string, object>();
    tuples.ForEach(x => parameters.Add("p" + x.Item1, x.Item2));

    using (var ctx = new TestContext())
    {
        var query = ctx.Entity_Basics;

        // ADD the current query && cancellationToken as parameter
        parameters.Add("q", query);
        parameters.Add("token", cancellationToken);

        // GET the task
        var task = (Task<List<Entity_Basic>>)Eval.Execute("q.Where(x => " + where + ").ToListAsync(token)", parameters);

        // AWAIT the task
        var result = await task.ConfigureAwait(false);
        return result;
    }
}
9
Jonathan Magnan

動的なwhere句の一般的なパターンとしてこれを試してください。

//example lists, a solution for populating will follow
List<string> Names = new List<string>() { "Adam", "Joe", "Bob" };
//these two deliberately left blank for demonstration purposes
List<string> Specialties = new List<string> () { };
List<string> Ranks = new List<string> () { };
using(var dbContext = new MyDbContext())
{
    var list = dbContext.MyTable
                        .Where(x => (!Names.Any() || Names.Contains(x.Name)) &&
                                    (!Specialties.Any() || Specialties.Contains(x.Specialty)) &&
                                    (!Ranks.Any() || Ranks.Contains(x.Rank))).ToList();

}

基礎となるデータについていくつかの仮定を行うと、以下は上記のLINQによって生成される可能性が高いSQLです。

DECLARE @p0 NVarChar(1000) = 'Adam'
DECLARE @p1 NVarChar(1000) = 'Joe'
DECLARE @p2 NVarChar(1000) = 'Bob'

SELECT [t0].[Name], [t0].[Specialty], [t0].[Rank]
FROM [MyTable] AS [t0]
WHERE [t0].[Name] IN (@p0, @p1, @p2)

これらのリストをUserSearchクラス内に入力するには:

foreach(var kvp in criteria)
{
    switch(kvp.Key)
    {
        case "Name": Names.Add(kvp.Value); break;
        case "Specialty": Specialties.Add(kvp.Value); break;
        case "Rank": Ranks.Add(kvp.Value); break;
    }
}

保守性に懸念があり、テーブルの列が頻繁に変更される場合は、SqlCommandクラスを介して生のSQLを使用することをお勧めします。このようにして、動的選択とwhere句を簡単に生成できます。テーブルの列のリストをクエリして、選択/フィルタリングに使用できるオプションを動的に決定することもできます。

1
Jakotheshadows

@Jakotheshadowsの回答を続けますが、チェックするものが何もないときにEF出力で追加のチェックをすべて要求する必要はありません。これは、ここで社内で行うことにより近いものです。

// Example lists, a solution for populating will follow
var Names = new List<string> { "Adam", "Joe", "Bob" };
// These two deliberately left blank for demonstration purposes
var specialties = new List<string>();
var ranks = new List<string>();
using(var dbContext = new MyDbContext())
{
    var list = dbContext.MyTable
       .FilterByNames(names)
       .FilterBySpecialties(specialties)
       .FilterByRanks(ranks)
       .Select(...)
       .ToList();
}

テーブル

[Table(...)]
public class MyTable : IMyTable
{
    // ...
}

拡張によるフィルター

public static class MyTableExtensions
{
    public static IQueryable<TEntity> FilterMyTablesByName<TEntity>(
        this IQueryable<TEntity> query, string[] names)
        where TEntity : class, IMyTable
    {
        if (query == null) { throw new ArgumentNullException(nameof(query)); }
        if (!names.Any() || names.All(string.IsNullOrWhiteSpace))
        {
            return query; // Unmodified
        }
        // Modified
        return query.Where(x => names.Contains(x.Name));
    }
    // Replicate per array/filter...
}

また、EFクエリ内でContains(...)またはAny(...)を使用すると、パフォーマンス上の重大な問題があります。述語ビルダーを使用するはるかに高速な方法があります。これはIDの配列の例です(これにはLinqKit nugetパッケージが必要です):

public static IQueryable<TEntity> FilterByIDs<TEntity>(
    this IQueryable<TEntity> query, int[] ids)
    where TEntity : class, IBase
{
    if (ids == null || !ids.Any(x => x > 0 && x != int.MaxValue)) { return query; }
    return query.AsExpandable().Where(BuildIDsPredicate<TEntity>(ids));
}
private static Expression<Func<TEntity, bool>> BuildIDsPredicate<TEntity>(
    IEnumerable<int> ids)
    where TEntity : class, IBase
{
    return ids.Aggregate(
        PredicateBuilder.New<TEntity>(false),
        (c, id) => c.Or(p => p.ID == id));
}

これは、非常に高速なクエリの「IN」構文を出力します。

WHERE ID IN [1,2,3,4,5]
0
James Gray

あなたがここにいるのかわからない。しかし、これはあなたにアイデアを与えるはずです。

var query = db.Mytable.Where(x=> x.Specialty == criteria[0].Value && c=> c.Rank == criteria[1].Value).ToString(); 

Listを使用する必要がある理由すらわかりません。リストは反復する必要があります。 KeyValuePairのリストを回避するために、最初のキーを最初の条件、最後の条件の値を使用できます。

0
Aizen

いいね。 2セントあげましょう。動的LINQを使用する場合は、式ツリーを選択する必要があります。 LINQステートメントは、必要に応じて動的に生成できます。次のようなものが魔法をかけるはずです。

// inside a generic class.
public static IQueryable<T> GetWhere(string criteria1, string criteria2, string criteria3, string criteria4)
{
    var t = MyExpressions<T>.DynamicWhereExp(criteria1, criteria2, criteria3, criteria4);
    return db.Set<T>().Where(t);
}

これで、別のジェネリッククラスで、式を次のように定義できます。

public static Expression<Func<T, bool>> DynamicWhereExp(string criteria1, string criteria2, string criteria3, string criteria4)
{
    ParameterExpression Param = Expression.Parameter(typeof(T));

    Expression exp1 = WhereExp1(criteria1, criteria2, Param);
    Expression exp2 = WhereExp1(criteria3, criteria4, Param);

    var body = Expression.And(exp1, exp2);

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

private static Expression WhereExp1(string field, string type, ParameterExpression param) 
{
    Expression aLeft = Expression.Property(param, typeof(T).GetProperty(field));
    Expression aRight = Expression.Constant(type);
    Expression typeCheck = Expression.Equal(aLeft, aRight);
    return typeCheck;   
}

これで、メソッドをどこからでも呼び出すことができます。

// get search criterias from user
var obj = new YourClass<YourTableName>();
var result = obj.GetWhere(criteria1, criteria2, criteria3, criteria4);

これは、LINQのwhere拡張メソッドで使用するために、2つの条件の間にAND演算子を使用した強力な動的式を提供します。これで、戦略に基づいて、必要に応じて引数を渡すことができます。例えばparams string []またはキーと値のペアのリストで...関係ありません。

ここでは何も修正されていないことがわかります。完全に動的であり、リフレクションよりも高速で、多くの式と多くの基準を作成します...

0
Awais Mahmood