2016-02-10 13 views
6

Per vari motivi, devo essere in grado di consentire all'utente di selezionare un elemento da un database in base alla scelta di colonne e valori. Ad esempio, se ho una tabella:LINQ Seleziona colonne e valori dinamici

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

L'utente può richiedere un 1, 2, o più colonne e le colonne che richiedono può essere diverso. Ad esempio, l'utente può richiedere voci in cui Specialty == Basket Weaving e Rank == 12. What I do currently is gather the user's request and create a list of KeyValuePair where the chiave is the column name and the Value` è il valore desiderato della colonna:

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

Usando questa lista di KeyValuePair 's, come posso più succintamente selezionare le voci del database che corrisponde a tutti i criteri?

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

MODIFICA: Vorrei utilizzare EntityFramework anziché SQL raw se posso aiutarlo.

UPDATE 3: Mi sto avvicinando. Ho scoperto un modo per utilizzare LINQ dopo aver scaricato tutti i valori dalla tabella. Questo ovviamente non è super ideale perché scarica tutto nella tabella. Quindi immagino che l'ultimo passo sarebbe quello di capire un modo in cui non devo scaricare l'intera tabella ogni volta. Ecco una spiegazione di quello che sto facendo:

Per ogni riga della tabella

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

faccio una lista di Caccio che rappresentano se la colonna soddisfa i criteri.

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

Poi ho controllare per vedere se questa lista bool è tutto vero

.All(c => c == true) 

Un esempio del codice completo è qui sotto:

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

Sembra come se il mio problema si trova con questo segmento di codice:

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

Non ho familiarità con gli alberi delle espressioni, quindi forse la risposta sta in loro. Potrei anche provare Dynamic LINQ.

+0

ritengo questo post ti aiuterà, http://stackoverflow.com/questions/821365/how-to-convert-a-string-to-its-equivalent-expression-tree Stai andando a dover convertire la stringa che vuoi nel punto Dove a un'espressione reale che funzionerà. – Dylan

+0

Ho aggiornato la mia risposta per includere l'SQL generato da linq per alleviare i problemi relativi ai confronti non necessari in SQL. – Jakotheshadows

+0

hai già risolto? –

risposta

0

Non sai cosa stai cercando qui. Ma questo dovrebbe darti un'idea.

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

Non sono nemmeno sicuro del motivo per cui è necessario utilizzare Elenco. Come lista deve essere iterata. Puoi semplicemente usare la chiave prima la prima condizione e il valore per l'ultima condizione per evitare l'elenco di KeyValuePair.

+0

Forse ho bisogno di essere più chiaro. Il numero e il nome delle colonne possono variare. Non riesco a codificare il nome della colonna come 'x.Specialty' e non posso avere un numero fisso di termini di ricerca nella clausola' .Where'. – thndrwrks

1

Prova questo come un modello generale per cui le clausole dinamiche:

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

} 

facendo alcune ipotesi circa i dati sottostanti, il seguente è il codice SQL che rischia di essere generato dal LINQ mostrato sopra:

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) 

per popolare questi elenchi all'interno della vostra 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; 
    } 
} 

Se si è interessati alla manutenibilità e le colonne della tabella cambieranno spesso, è possibile tornare a utilizzare SQL raw tramite la classe SqlCommand. In questo modo, puoi facilmente generare selezioni dinamiche e clausole dove. È anche possibile interrogare l'elenco di colonne sulla tabella per determinare dinamicamente quali opzioni sono disponibili per la selezione/il filtro.

+0

Questo è più vicino. C'è un modo per evitare di fare confronti su ogni colonna e confrontare solo le colonne che specificano dinamicamente? – thndrwrks

+0

Non con LINQ, no. Se la tua preoccupazione è che il codice sia brutto, allora temo che ci sia ben poco da fare per aggirare il problema. Se la tua preoccupazione è che stai eseguendo confronti non necessari, non preoccuparti perché la parte! Names.Any() di ogni condizione ignorerà fondamentalmente il tuo filtro fintanto che non avrai filtri in Nomi. Nessun altro SQL verrà generato per gli elenchi Specialità e Classifiche nel mio esempio. – Jakotheshadows

+0

La mia preoccupazione è la manutenibilità. Sono solo altre cose che devono essere cambiate se cambiano le colonne della tabella. – thndrwrks

9

Poiché le colonne e filtri sono dinamici, dinamica LINQ libreria può aiutare qui

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

Un'altra soluzione sta usando Eval Expression.NET che ti consente di valutare in modo dinamico il codice C# in fase di runtime.

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

Diniego: Sono il proprietario del progetto Eval Expression.NET

Edit: rispondi Commento

Attenzione, il tipo di valore di parametro deve essere compatibile con il tipo di proprietà. Per esempio, se la proprietà "Rank" è un INT, solo il tipo compatibile con INT funzionerà (non stringa).

Ovviamente, sarà necessario ridefinire questo metodo per renderlo più adatto alla propria applicazione. Ma come puoi vedere, puoi usare facilmente anche il metodo asincrono da Entity Framework.

Se si personalizza anche la selezione (il tipo di ritorno), potrebbe essere necessario ottenere il risultato asincrono mediante riflessione oppure utilizzare ExecuteAsync invece con 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; 
    } 
} 
+0

La query potrebbe apparire come segue 'attendi db.MyTable.Where (" Specialità == "Tessitura di cesti" && Rank == "12" "). ToListAsync()' – Eldho

+0

Pls aggiorna la documentazione dell'espressione 'Where' nel tuo progetto . Grazie per la biblioteca – Eldho

+1

La tua biblioteca è fantastica! Grazie per averlo scritto :) – nawfal

0

Fine. Lasciami dare i miei due centesimi. Se si desidera utilizzare LINQ dinamico, gli alberi di espressione dovrebbero essere la vostra opzione. È possibile generare istruzioni LINQ come dinamiche come si desidera. Qualcosa come il seguire dovrebbe fare la magia.

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

Ora in un'altra classe generica è possibile definire le espressioni come.

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

Ora è possibile chiamare i metodi ovunque.

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

Questo vi darà un'espressione fortemente dinamico con due condizioni con operatore AND tra loro di utilizzare nel vostro in cui metodo di estensione di LINQ. Ora puoi passare i tuoi argomenti come vuoi in base alla tua strategia. per esempio. in params string [] o nella coppia chiave valore elenco ... non importa.

Si può vedere che nulla è fisso qui .. il suo completamente dinamico e più veloce di riflessione e di fare come molti espressioni e altrettanti criteri ...

0

Continuando @ risposta di Jakotheshadows ma non richiedono tutti i controlli supplementari nell'output EF quando non c'è nulla per controllare, questo è più vicino a quello che facciamo in casa qui:

// 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 Tavola

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

l'Fi ltro da estensioni

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

Inoltre, ci sono problemi di prestazioni significative con l'utilizzo Contiene (...) o qualsiasi (...) all'interno di una query di EF. C'è un metodo molto più veloce usando i Predicate Builders. Questo è un esempio di una matrice di ID (questo richiede il pacchetto NuGet 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)); 
} 

questo emette il "IN" sintassi per una query che è molto veloce:

WHERE ID IN [1,2,3,4,5]