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

How to use Queryable.Where when type is set at runtime?

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

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

我正在努力寻找一种方法来像这样过滤 DbSet,其中在运行时提供基础 'table' 类型。这里有一个大大简化的代码版本来说明:

    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 

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

恭喜你回答了第一个问题。

让我们首先了解一种基于某些自定义过滤器来过滤数据集合的方法。我假设您希望在过滤器中传递的 NameValueCollection Type 将 PropertyNames 作为键,将 PropertyValues 作为值。

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

步骤 1

- 获取所有 Class 属性

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

中的 PropertyDescriptorCollection

组件缓存

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

我们的 Dictionary 的键表示通用 class 的名称,值包含给定 class.

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

第 2 步

- 循环过滤器

如上所示,我们在变量 typeDescriptor 中获得了通用 class TPropertyDescriptorCollection 之后,现在让我们遍历我们的过滤器,看看是否有其中 属性 个名称与我们的任何过滤键匹配。如果 T 有一个 属性 名称匹配我们的任何过滤器键,现在我们检查 属性 的实际值是否匹配我们的过滤器值。为了提高 search/filter 函数的质量,我们将使用 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;
    }
}

步骤 3

- 实施扩展方法。

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

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

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

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

步骤 4

把所有东西放在一起。

既然我们已经拥有了所需的一切,让我们看看 final/full 代码在单个 static class.

中作为一个代码块的样子。
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<T>List<T>、数组

这就是您将如何在项目的任何位置使用上述代码。

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<T>

每个 DbContext 都有一个函数 .Set<T> 那 returns 一个 DbSet<T> 可以用作 IQueryable<T> 因此我们的函数可以用作以及如下所示。

例子

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

希望这能回答您的问题或为您指明正确的方向。

回答你的具体问题。鉴于

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