web-dev-qa-db-fra.com

LINQ Select Dynamic Columns and Values

Pour diverses raisons, je dois pouvoir permettre à l'utilisateur de sélectionner un élément dans une base de données en fonction de son choix de colonnes et de valeurs. Par exemple, si j'ai une table:

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

L'utilisateur peut demander 1, 2 colonnes ou plus et les colonnes qu'il demande peuvent être différentes. Par exemple, l'utilisateur peut demander des entrées où Specialty == Basket Weaving et Rank == 12. What I do currently is gather the user's request and create a list ofKeyValuePairwhere theKeyis the column name and theValue` est la valeur souhaitée de la colonne:

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");

En utilisant cette liste de KeyValuePair, comment puis-je sélectionner de manière succincte des éléments de base de données qui correspondent à tous les critères?

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

EDIT: Je voudrais utiliser EntityFramework au lieu de SQL brut si je peux l’aider.

UPDATE 3 : Je me rapproche. J'ai découvert un moyen d'utiliser LINQ une fois que j'ai téléchargé Toutes les valeurs du tableau. Ce n’est évidemment pas super idéal car il télécharge Tout dans la table. Donc, je suppose que la dernière étape serait de trouver un moyen où Je n'ai pas à télécharger tout le tableau à chaque fois. Voici une explication de ce que je fais: 

Pour chaque ligne du tableau 

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

Je fais une liste de bools indiquant si la colonne correspond aux critères.

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

Ensuite, je vérifie si cette liste est vraie

.All(c => c == true)

Un exemple du code complet est ci-dessous:

// 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();
        }
    }
}

Il semble que mon problème réside dans ce segment de code:

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

Je ne suis pas familier avec les arbres d'expression, alors peut-être que la réponse réside en eux. Je peux aussi essayer Dynamic LINQ.

8
thndrwrks

Comme vos colonnes et vos filtres sont dynamiques, la bibliothèque Dynamic LINQ peut vous aider.

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

Doc: 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)");

Une autre solution consiste à utiliser Eval Expression.NET, qui vous permet d'évaluer le code c # de manière dynamique au moment de l'exécution.

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 });
}

Disclaimer: Je suis propriétaire du projet Eval Expression.NET

Edit: Répondre au commentaire

Attention, le type de valeur du paramètre doit être compatible avec le type de propriété. Par exemple, si la propriété «Rank» est une INT, seul le type compatible avec INT fonctionnera (pas de chaîne).

De toute évidence, vous devrez revoir cette méthode pour la rendre plus adaptée à votre application. Mais comme vous pouvez le constater, vous pouvez facilement utiliser même une méthode asynchrone d’Entity Framework.

Si vous personnalisez également la sélection (le type de retour), vous devrez peut-être obtenir le résultat asynchrone à l'aide de la réflexion ou utiliser ExecuteAsync à la place avec ToList ().

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

Essayez ceci comme modèle général pour les clauses where dynamiques:

//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();

}

En formulant certaines hypothèses sur vos données sous-jacentes, voici le code SQL susceptible d'être généré par le LINQ présenté ci-dessus:

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)

Pour renseigner ces listes dans votre classe 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;
    }
}

Si la facilité de maintenance vous préoccupe et que les colonnes de la table vont souvent changer, vous voudrez peut-être revenir à l'utilisation de SQL brute via la classe SqlCommand. De cette façon, vous pouvez facilement générer des clauses de sélection dynamique et where. Vous pouvez même interroger la liste des colonnes de la table pour déterminer de manière dynamique les options disponibles pour la sélection/le filtrage.

1
Jakotheshadows

Poursuivre la réponse de @ Jakotheshadows mais ne nécessitant pas tous les contrôles supplémentaires dans la sortie EF quand il n'y a rien à vérifier, ceci est plus proche de ce que nous faisons ici chez nous:

// 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();
}

La table

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

Le filtre par extensions

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...
}

En outre, l'utilisation de Contains (...) ou Any (...) dans une requête EF présente des problèmes de performances importants. Il existe une méthode beaucoup plus rapide utilisant Predicate Builders. Voici un exemple avec un tableau d'identifiants (cela nécessite le paquetage LinqKit):

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));
}

Ceci sort la syntaxe "IN" pour une requête très rapide:

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

Vous ne savez pas ce que vous cherchez ici. Mais cela devrait vous donner une idée.

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

Je ne sais même pas pourquoi vous devez même utiliser List. As List doit être itéré. Vous pouvez simplement utiliser Key d'abord la première condition et la valeur pour la dernière condition à éviter Liste de KeyValuePair 

0
Aizen

Bien. Laissez-moi donner mes deux cents. Si vous souhaitez utiliser LINQ dynamique, les arbres d’expression devraient être votre option. Vous pouvez générer des instructions LINQ aussi dynamiques que vous le souhaitez. Quelque chose comme suivre devrait faire la magie.

// 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);
}

Maintenant, dans une autre classe générique, vous pouvez définir vos expressions comme.

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;   
}

Vous pouvez maintenant appeler les méthodes n'importe où en tant que.

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

Cela vous donnera une expression puissamment dynamique avec deux conditions avec l'opérateur AND entre eux à utiliser dans votre méthode d'extension where de LINQ. Vous pouvez maintenant passer vos arguments comme vous le souhaitez en fonction de votre stratégie. par exemple. dans params string [] ou dans la liste des paires de valeurs de clés ... n'a pas d'importance.

Vous pouvez voir que rien n’est corrigé ici .. c’est complètement dynamique et plus rapide que la réflexion et vous faites autant d’expressions et autant de critères ...

0
Awais Mahmood