2016-02-10 3 views
6

По различным причинам я должен иметь возможность разрешить пользователю выбирать элемент из базы данных на основе их выбора столбцов и значений. Например, если у меня есть таблица:LINQ Выбор динамических столбцов и значений

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 of KeyValuePair where the Key is the column name and the value` является искомым значением столбца:

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

EDIT: Я хотел бы использовать EntityFramework вместо raw SQL, если я могу ему помочь.

ОБНОВЛЕНИЕ 3: Я приближаюсь. Я обнаружил способ использовать LINQ, как только я загрузил все значения из таблицы. Это, очевидно, не супер идеально, потому что он загружает все в таблице. Поэтому я думаю, что последним шагом было бы выяснить способ, которым Мне не нужно загружать всю таблицу каждый раз. Вот объяснение того, что я делаю:

Для каждой строки в таблице

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

Я составьте список BOOLS представляющих, если столбец соответствует критериям.

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

Затем я проверяю, если этот список BOOL все верно

.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() 

Я не знаком с деревьями выражений, поэтому, возможно, в них лежит ответ. Я также могу попробовать Dynamic LINQ.

+0

Я думаю, что это сообщение поможет, http://stackoverflow.com/questions/821365/how-to-convert-a-string-to-its-equivalent-expression-tree Вам нужно будет преобразовать строку, которую вы хотите в Where на фактическое выражение, которое будет работать. – Dylan

+0

Я обновил свой ответ, чтобы включить сгенерированный SQL из linq, чтобы облегчить проблемы, которые вы делаете ненужными сравнениями в SQL. – Jakotheshadows

+0

У вас еще не решили? –

ответ

0

Не уверен, что вы здесь. Но это должно дать вам Идею.

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

Я даже не уверен, почему вам даже нужно использовать Список. Поскольку Список необходимо повторить. Вы можете просто использовать Key first первое условие и значение для последнего условия, чтобы избежать списка KeyValuePair.

+0

Возможно, мне нужно быть более ясным. Количество и название столбцов могут отличаться. Я не могу жестко закодировать имя столбца, например 'x.Specialty', и я не могу фиксировать число поисковых терминов в предложении' .Where'. – thndrwrks

1

Попробуйте это как общий шаблон для динамических где статей:

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

} 

внесения некоторых предположений о ваших исходных данных, следующий является SQL, который, вероятно, будет порождено LINQ показано выше:

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

Если вы заинтересованы в ремонтопригодности и что столбцы таблицы будут меняться часто, то вам, возможно, захочется вернуться к использованию raw SQL через класс SqlCommand. Таким образом, вы можете легко создавать динамические выделения и предложения. Вы даже можете запросить список столбцов в таблице, чтобы динамически определить, какие опции доступны для выбора/фильтрации.

+0

Это ближе. Есть ли способ избежать сравнения по каждому столбцу и вместо этого сравнивать только столбцы, которые я указываю динамически? – thndrwrks

+0

Не с LINQ, нет. Если вы обеспокоены тем, что код уродливый, то я боюсь, что вы можете сделать это очень мало, чтобы обойти это. Если вы обеспокоены тем, что выполняете ненужные сравнения, то не беспокойтесь об этом, потому что часть! Names.Any() каждого условия будет в основном игнорировать ваш фильтр, если у вас нет фильтров в именах. В моем примере дополнительный номер SQL не будет создан для списков Specialties и Ranks. – Jakotheshadows

+0

Моя забота - ремонтопригодность. Это просто больше вещей, которые нужно изменить, если столбцы таблицы меняются. – thndrwrks

9

Поскольку ваши столбцы и фильтры являются динамическими, Dynamic LINQ библиотека может помочь вам здесь

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

Другим решением является использование 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

Редактировать: Ответ комментарий

Будьте осторожны, тип значения параметра должен быть совместим с типом недвижимости. Например, если свойство «Ранг» является INT, будет работать только тип, совместимый с INT (не строка).

Очевидно, что вам необходимо будет реорганизовать этот метод, чтобы он стал более подходящим для вашего приложения. Но, как вы можете видеть, вы легко можете использовать метод async из Entity Framework.

Если вы настроили выбор также (тип возврата), вам может потребоваться либо получить результат асинхронизации с использованием отражения, либо использовать ExecuteAsync вместо 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

Запрос может выглядеть так: 'await db.MyTable.Where (« Специальность == «Сорение корзины» && Rank == «12»). ToListAsync() ' – Eldho

+0

Pls обновляет документацию выражения' Where' в вашем проекте , Спасибо за библиотеку – Eldho

+1

Ваша библиотека замечательная! Спасибо, что написал это :) – nawfal

0

Изобразительное. Позвольте мне дать мои два цента. Если вы хотите использовать динамический 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. Теперь вы можете передать свои аргументы, как хотите, на основе вашей стратегии. например в параметрах string [] или в списке пар ключей значений ... не имеет значения.

Вы можете увидеть, что ничего не фиксировано .. его полностью динамичны и быстрее, чем отражение и для Вас сделать так много выражений и столько же ...

критерии
0

Продолжая @ ответ 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 
{ 
    // ... 
} 

ФИ По 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... 
} 

Кроме того, существуют значительные проблемы с производительностью с помощью Содержит (...) или любой (...) внутри запроса EF. Существует более быстрый метод использования Predicate Builders. Это пример с массивом идентификаторов (для этого требуется пакет 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)); 
} 

Это выводит «IN» синтаксис для запроса, который очень быстро:

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