LINQ选择动态列和值

由于各种原因,我需要能够允许用户根据他们对列和值的选择从数据库中选择项目。 例如,如果我有一张桌子:

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

用户可以请求1,2或更多列,并且他们请求的列可以是不同的。 例如,用户可以请求其中Specialty == Basket WeavingRank == 12. What I do currently is gather the user's request and create a list of条目Rank == 12. What I do currently is gather the user's request and create a list of KeyValuePair Rank == 12. What I do currently is gather the user's request and create a list of where the Key is the column name and the Value`是期望值列:

 class UserSearch { private List<KeyValuePair criteria = new List<KeyValuePair>(); public void AddTerm(string column, string value) { criteria.Add(new KeyValuePair(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(???); } 

编辑:如果我能帮助它,我想使用EntityFramework而不是原始SQL。

更新3 :我越来越近了。 一旦我从表中下载了所有值,我就发现了一种使用LINQ的方法。 这显然不是非常理想,因为它会下载表格中的所有内容。 所以我想最后一步是找出一种方法,我不必每次都下载整个表格。 这是对我正在做的事情的解释:

对于表中的每一行

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

我列出了一个bool列表,表示列是否符合条件。

 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 criteria = new List<KeyValuePair>(); public void AddTerm(string column, string value) { criteria.Add(new KeyValuePair(column, value); } public async Task<List> 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。

由于您的列和filter是动态的,因此动态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项目的所有者

编辑 :回答评论

注意,参数值类型必须与属性类型兼容。 例如,如果“Rank”属性是INT,则只有与INT兼容的类型才能工作(不是字符串)。

显然,您需要重构此方法,使其更适合您的应用程序。 但正如您所看到的,您可以轻松地使用Entity Framework中的异步方法。

如果您还自定义select(返回类型),则可能需要使用reflection获取异步结果,或者使用ExecuteAsync而不是ToList()。

 public async Task> 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>(); tuples.Add(new Tuple("Specialty", "Basket Weaving")); tuples.Add(new Tuple("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(); 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>)Eval.Execute("q.Where(x => " + where + ").ToListAsync(token)", parameters); // AWAIT the task var result = await task.ConfigureAwait(false); return result; } } 

尝试将此作为动态where子句的一般模式:

 //example lists, a solution for populating will follow List Names = new List() { "Adam", "Joe", "Bob" }; //these two deliberately left blank for demonstration purposes List Specialties = new List () { }; List Ranks = new List () { }; 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子句。 您甚至可以查询表中的列列表,以动态确定哪些选项可用于选择/过滤。

不知道你在这之后是什么。 但这应该给你一个想法。

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

我甚至不确定为什么你甚至不得不使用List。 因为List需要迭代。 您可以先使用Key,将First条件和Value作为最后一个条件,以避免KeyValuePair的List。

精细。 让我给我两分钱。 如果要使用动态LINQ,表达树应该是您的选择。 您可以根据需要生成动态LINQ语句。 像下面这样的事情应该是神奇的。

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

现在在另一个generics类中,您可以将表达式定义为。

 public static Expression> 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>(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(); var result = obj.GetWhere(criteria1, criteria2, criteria3, criteria4); 

这将为您提供一个强大的动态表达式,其中包含两个条件,它们之间使用AND运算符,以便在LINQ的where扩展方法中使用。 现在,您可以根据自己的策略根据需要传递参数。 例如,在params string []或键值对列表中……无所谓。

你可以看到这里没有任何东西被修复..它完全动态,比reflection更快,你可以制作尽可能多的表达式和许多标准……

继续@ Jakotheshadows的答案,但是当没有什么要检查的时候不要求EF输出中的所有额外检查,这更接近我们在这里做的事情:

 // Example lists, a solution for populating will follow var Names = new List { "Adam", "Joe", "Bob" }; // These two deliberately left blank for demonstration purposes var specialties = new List(); var ranks = new List(); 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 FilterMyTablesByName( this IQueryable 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(…)会出现严重的性能问题。 使用Predicate Builders的方法要快得多。 这是一个带有ID数组的示例(这需要LinqKit nuget包):

 public static IQueryable FilterByIDs( this IQueryable 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(ids)); } private static Expression> BuildIDsPredicate( IEnumerable ids) where TEntity : class, IBase { return ids.Aggregate( PredicateBuilder.New(false), (c, id) => c.Or(p => p.ID == id)); } 

这为非常快的查询输出“IN”语法:

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