【问题标题】:LINQ Select Dynamic Columns and ValuesLINQ 选择动态列和值
【发布时间】:2016-02-10 05:39:11
【问题描述】:

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

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

编辑:如果可以的话,我想使用 EntityFramework 而不是原始 SQL。

更新 3:我越来越近了。下载后,我发现了一种使用 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。

【问题讨论】:

  • 我认为这篇文章会有所帮助,stackoverflow.com/questions/821365/… 您必须将 Where 中所需的字符串转换为有效的实际表达式。
  • 我已经更新了我的答案以包括从 linq 生成的 SQL,以减轻您在 SQL 中进行不必要的比较的担忧。
  • 你解决了吗?

标签: c# wpf entity-framework linq


【解决方案1】:

由于您的列和过滤器是动态的,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

编辑:回复评论

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

显然,您需要重构此方法以使其更适合您的应用程序。但正如您所见,您甚至可以轻松使用 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;
    }
}

【讨论】:

  • 查询可能看起来像这样await db.MyTable.Where("Specialty=="Basket Weaving" &amp;&amp; Rank=="12"").ToListAsync()
  • 请更新您项目中Where 表达式的文档。感谢图书馆
  • 你的图书馆很棒!谢谢你写它:)
【解决方案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);

这将为您提供一个强大的动态表达式,其中包含两个条件以及它们之间的 AND 运算符,以便在 LINQ 的 where 扩展方法中使用。现在,您可以根据自己的策略随意传递参数。例如在参数字符串[] 或键值对列表中... 没关系。

你可以看到这里没有什么是固定的。它是完全动态的,比反射更快,你可以做出尽可能多的表达式和标准......

【讨论】:

    【解决方案3】:

    试试这个作为动态 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 子句。您甚至可以查询表上的列列表,以动态确定哪些选项可用于选择/过滤。

    【讨论】:

    • 这更近了。有什么办法可以避免对每一列进行比较,而是只比较我动态指定的列?
    • 不使用 LINQ,不。如果您担心代码很难看,那么恐怕您无能为力。如果您担心您正在执行不必要的比较,那么请不要担心,因为只要您在 Names 中没有过滤器,每个条件的 !Names.Any() 部分基本上会忽略您的过滤器。在我的示例中,不会为 Specialties 和 Ranks 列表生成额外的 SQL。
    • 我关心的是可维护性。如果表的列发生更改,则必须更改更多内容。
    • 那么你可能不需要实体框架。如果表列更改可能只使用好的'ol vanilla SqlCommand 类,我会建议你的情况。我将更新我的答案以反映这一观察结果。
    【解决方案4】:

    不知道你在这里追求什么。但这应该会给你一个想法。

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

    我什至不确定您为什么必须使用 List。因为 List 需要迭代。您可以先使用键作为第一个条件,然后使用值作为最后一个条件,以避免列出 KeyValuePair。

    【讨论】:

    • 也许我需要更清楚一些。列的数量和名称可能会有所不同。我不能像x.Specialty 这样对列名进行硬编码,而且.Where 子句中不能有固定数量的搜索词。
    【解决方案5】:

    继续@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(...) 会导致严重的性能问题。使用 Predicate Builders 有一个更快的方法。这是一个带有 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]
    

    【讨论】:

      猜你喜欢
      • 2016-05-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多