如何在运行时设置类型时使用 Queryable.Where?

Posted

技术标签:

【中文标题】如何在运行时设置类型时使用 Queryable.Where?【英文标题】:How to use Queryable.Where when type is set at runtime? 【发布时间】:2019-02-01 18:06:14 【问题描述】:

我正在使用 EF6 为应用程序实现搜索/过滤 UI 的后端。我有代码构建一个表达式以与 Queryable.Where 一起使用给定的 DbSet,其中 DbSet 的类型在运行时确定(DBContext 有很多,它们可能会改变)。如果我首先通过将表达式转换为特定类型来作弊,那么对 Where 的调用可以正常工作。否则,我会收到此错误:

'System.Linq.Queryable.Where(System.Linq.IQueryable, System.Linq.Expressions.Expression>)'的最佳重载方法匹配有一些无效参数'

我正在努力寻找一种方法来过滤像这样的 DbSet,其中在运行时提供底层“表”类型。这是一个大大简化的代码版本来说明:

    void ProcessFilter(AppDbContext context, NameValueCollection filters, Type tableType)
    
        // If tableType == typeof(Organisation), expression is a Expression<Func<Organisation, bool>>
        var expression = GetFilterExpression(filters);
        var dbset = Set(context, tableType);

        dynamic dynamicSet = dbset;

        // This fails
        var results = Queryable.Where(dynamicSet, expression);
        // see https://***.com/questions/4285598/iqueryable-non-generic-missing-count-and-skip-it-works-with-iqueryablet

        // Suppose tableType == typeof(Organisation)
        // This works
        var typedExpression = expression as Expression<Func<Organisation, bool>>;
        var typedResults = Queryable.Where(dynamicSet, typedExpression);

    

    public static IQueryable Set(DbContext context, Type T)
    
        // Similar to code in
        // https://***.com/questions/21533506/find-a-specified-generic-dbset-in-a-dbcontext-dynamically-when-i-have-an-entity
        var method = typeof(DbContext).GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(x => x.Name == "Set" && x.IsGenericMethod).First();

        // Build a method with the specific type argument 
        method = method.MakeGenericMethod(T);

        return method.Invoke(context, null) as IQueryable;
    

【问题讨论】:

最让我印象深刻的是您对 Queryable.Where 的使用。说这可能是缺乏经验,但我的经验和对该组方法的粗略搜索将两个返回的重载的第一个参数标记为 this 关键字。这表明您应该调用 dynamicSet.Where(expression),这与我曾经使用过或看到使用过这种方法的唯一方式一致。 @Inagnikai - 问题是,dynamicSet.Where 不会编译 - 请参阅该代码行下评论中链接中的文章......因为类型是在运行时提供的,所以没有'不是在编译时使用的强类型 IQueryable 我明白你在说什么。从表面上看,您的表格类型是由用户通过某种控制提供的。他们尝试设置它的方式提供了非常高的灵活性,但可以预见的是以牺牲类型安全为代价。您是否有任何理由无法将用户输入映射到具体类型,并可能创建具有 IQueryable 约束的通用方法来处理 .Where() 调用? 对于这种情况,您将不得不介绍Reflection @Inagnikai - 是的,一种解决方法是使用一个函数将 tableType 的每个可能值映射到对 Where 的显式调用,该调用在编译时具有代码中指定的类型......但这会很难维护,我希望有一种方法可以做到这一点,可以使用库风格,而不必为 DbContext 中的所有表类型提供特殊情况代码...... 【参考方案1】:

回答您具体的问题。给定

IQueryable source
LambdaExpression predicate

如何调用静态泛型方法

Queryable.Where<T>(IQueryable<T> source, Expression<Func<T, bool>> predicate)

可以使用 (A) 反射、(B) DLR 动态调度和 (C) Expression.Call 来完成。

您正在尝试做的是选项(B)。不过

var result = Queryable.Where((dynamic)source, predicate);

动态搜索具有LambdaExpression 类型的第二个参数的方法,这当然会失败。

为了能够动态匹配目标方法,还需要将第二个参数dynamic设为:

var result = Queryable.Where((dynamic)source, (dynamic)predicate); 

上面的等效选项(C)实现是:

var result = source.Provider.CreateQuery(Expression.Call(
    typeof(Queryable), nameof(Queryable.Where), new[]  source.ElementType ,
    source.Expression, predicate));

【讨论】:

你是个传奇。将谓词作为动态传递非常有效。版本(C)也很有趣,可以帮助我。谢谢!【参考方案2】:

恭喜你提出第一个问题。

让我们首先了解一种基于一些自定义过滤器过滤数据集合的方法。我将假设您更喜欢传入过滤器的 NameValueCollection Type 将 PropertyNames 作为 Keys 并将 PropertyValues 作为 Value。

在我们过滤整个集合之前,让我们首先弄清楚如何确定一个对象是否具有与我们的过滤器匹配的属性。由于直到运行时我们才知道对象的Type,因此我们需要使用Generics in C# 来完成此操作。

步骤 1

- 获取所有类属性

我们需要获取泛型类的所有属性,例如&lt;TClass&gt;。使用反射执行此操作被认为很慢,Matt Warren 解释了Why Reflection is slow in .NET 以及如何解决它。因此,我们将实现类组件模型的缓存以获取其存在于命名空间 System.ComponentModel.PropertyDescriptorCollection 中的PropertyDescriptorCollection

组件缓存

private static IDictionary<string, PropertyDescriptorCollection> _componentsCache
        = new Dictionary<string, PropertyDescriptorCollection>();

Dictionary 的键代表泛型类的名称,值包含该给定类的 PropertyDescriptorCollection

internal static bool InnerFilter<T>(T obj, NameValueCollection filters)
        where T : class

        Type type = typeof(T);
        PropertyDescriptorCollection typeDescriptor = null;

        if (_componentsCache.ContainsKey(type.Name))
            typeDescriptor = _componentsCache[type.Name];
        else
        
            typeDescriptor = TypeDescriptor.GetProperties(type);
            _componentsCache.Add(type.Name, typeDescriptor);
        

第二步

- 循环过滤器

如上所示,在变量typeDescriptor 中获得泛型类TPropertyDescriptorCollection 后,现在让我们循环遍历我们的过滤器,看看它的任何属性名称是否与我们的任何过滤器键匹配。如果T 的属性名称与我们的任何过滤器键匹配,现在我们检查该属性的实际值是否与我们的过滤器值匹配。为了提高我们的搜索/过滤功能的质量,我们将使用Regular Expressions in C# 来确定比较是命中还是未命中。

for (int i = 0; i < filters.Count; i++)

    string filterName = filters.GetKey(i);
    string filterValue = filters[i];

    PropertyDescriptor propDescriptor = typeDescriptor[filterName];
    if (propDescriptor == null)
        continue;
    else
    
        string propValue = propDescriptor.GetValue(obj).ToString();
        bool isMatch = Regex.IsMatch(propValue, $"(filterValue)");
        if (isMatch)
            return true;
        else
            continue;
    

第三步

- 实现扩展方法。

为了使我们编写的代码易于使用和重用,我们将实现Extension Methods in C#,以便我们可以在项目中的任何地方更好地重用我们的函数。

- 使用上述函数的通用集合过滤函数。

由于IQueryable&lt;T&gt; 可以通过System.Linq 中的.Where() 函数转换为IEnumerable&lt;T&gt;,因此我们将在函数调用中使用它,如下所示。

public static IEnumerable<T> Filter<T>(this IEnumerable<T> collection, NameValueCollection filters)
        where T : class

    if (filters.Count < 1)
        return collection;

    return collection.Where(x => x.InnerFilter(filters));

第四步

把所有东西放在一起。

现在我们已经拥有了所需的一切,让我们看看最终/完整代码在单个 static 类中的一个代码块的外观。

public static class Question54484908 

    private static IDictionary<string, PropertyDescriptorCollection> _componentsCache = new Dictionary<string, PropertyDescriptorCollection> ();

    public static IEnumerable<T> Filter<T> (this IEnumerable<T> collection, NameValueCollection filters)
        where T : class 
    
        if (filters.Count < 1)
            return collection;

        return collection.Where (x => x.InnerFilter (filters));
    

    internal static bool InnerFilter<T> (this T obj, NameValueCollection filters)
        where T : class 
    
        Type type = typeof (T);
        PropertyDescriptorCollection typeDescriptor = null;

        if (_componentsCache.ContainsKey (type.Name))
            typeDescriptor = _componentsCache[type.Name];
        else 
            typeDescriptor = TypeDescriptor.GetProperties (type);
            _componentsCache.Add (type.Name, typeDescriptor);
        

        for (int i = 0; i < filters.Count; i++) 
            string filterName = filters.GetKey (i);
            string filterValue = filters[i];

            PropertyDescriptor propDescriptor = typeDescriptor[filterName];
            if (propDescriptor == null)
                continue;
            else 
                string propValue = propDescriptor.GetValue (obj).ToString ();
                bool isMatch = Regex.IsMatch (propValue, $"(filterValue)");
                if (isMatch)
                    return true;
                else
                    continue;
            
        

        return false;
    

终于

过滤IEnumerable&lt;T&gt;List&lt;T&gt;、数组

这就是您将如何在项目中的任何地方使用上述代码的方式。

private IEnumerable<Question> _questions;
_questions = new List<Question>()

    new Question("Question 1","How do i work with tuples"),
    new Question("Question 2","How to use Queryable.Where when type is set at runtime?")
;
var filters = new NameValueCollection 
 
    "Description", "work" 
;
var results = _questions.Filter(filters);

过滤DbSet&lt;T&gt;

每个DbContext 都有一个函数.Set&lt;T&gt;,它返回一个DbSet&lt;T&gt;,可以用作IQueryable&lt;T&gt;,因此我们的函数可以使用,如下所示。

例子

_dbContext.Set<Question>().Filter(filters);

希望这能回答您的问题,或者更确切地说,为您指明正确的方向。

【讨论】:

感谢您查看,但我认为这在这种情况下没有帮助,因为 1. 您显示的代码似乎仍然需要在编译时在代码中指定类型为一个模板参数,这正是我需要避免的,以及 2. 这个应用程序对数据库使用 EF,因此使用 Queryable.Where 可确保提供者在 SQL 中执行过滤,即 linq to sql,而您的使用 Where 似乎是在 IEnumerable 上运行的 linq to objects 风格,这将需要从数据库中获取所有内容。但如果我误解了,请告诉我。

以上是关于如何在运行时设置类型时使用 Queryable.Where?的主要内容,如果未能解决你的问题,请参考以下文章

您如何在运行时使用 GCC 和内联 asm 检测 CPU 架构类型?

在运行时设置向量类型

如何在运行时使用 C# 创建强类型数据集?

使用依赖注入时,我应该如何在运行时创建 I/O 类型(例如文件)

程序集特定设置未在运行时加载

使用 EPPlus 时如何设置列类型