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

3
我正在使用EF6为应用程序实现搜索/筛选UI的后端。我有一些代码用于构建表达式,以便在运行时确定DbSet的类型并将其用于Queryable.Where。如果我先将表达式强制转换为特定类型,则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://dev59.com/Y1LTa4cB1Zd3GeqPYDLQ

        // 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://dev59.com/D2Ei5IYBdhLWcg3wMZ3a
        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
@Inagnikai - 问题在于,dynamicSet.Where无法编译 - 请查看代码行下方评论中链接的文章... 因为类型是在运行时提供的,所以在编译时没有强类型的IQueryable<T>可用。 - Peter
我明白你的意思。表格类型显然是通过某种控件由用户提供的。你尝试设置它的方式提供了非常高的灵活性,但可预见的代价是类型安全性。你是否有任何理由不能将用户输入映射到具体类型,并可能创建一个带有IQueryable约束的通用方法来处理.Where()调用? - Inagnikai
对于这种情况,您将不得不介绍“反射”。 - Timothy Macharia
@Inagnikai - 是的,一种解决方法是创建一个函数,将tableType的每个可能值映射到一个显式调用Where的函数,该函数在编译时指定了代码中的类型...但这样很难维护,我希望有一种可以像库一样使用的方法来实现这个功能,而不必为DbContext中的所有表格类型编写特殊情况代码... - Peter
2个回答

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

1
你是一个传奇。将谓词作为动态参数传递完美地运行。版本(C)也很有趣,可能会在其他地方帮助我。谢谢! - Peter

0
恭喜您第一次提问。
让我们从查看基于一些自定义过滤器过滤数据集的方法开始。我假设您希望传递NameValueCollection类型作为过滤器,将属性名称作为键,属性值作为值。
在我们继续过滤整个集合之前,让我们先找出如何确定一个对象是否具有与我们的过滤器匹配的属性。由于我们直到运行时才知道对象的类型,因此我们需要使用C#中的泛型来实现这一点。

步骤1

- 获取所有类属性

我们需要获取泛型类的所有属性,例如<TClass>。使用反射来实现这一点被认为是缓慢的,Matt Warren解释了为什么.NET中的反射很慢以及如何解决它。因此,我们将实现缓存类组件模型,以获取其存在于命名空间System.ComponentModel.PropertyDescriptorCollection中的PropertyDescriptorCollection组件缓存
private static IDictionary<string, PropertyDescriptorCollection> _componentsCache
        = new Dictionary<string, PropertyDescriptorCollection>();

我们的字典的键表示泛型类的名称,而值则保存了该给定类的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);
        }
}

步骤二

- 遍历筛选器

在上面展示的示例中,我们已经获取了泛型类 TPropertyDescriptorCollection 存储于变量 typeDescriptor 中。现在,让我们遍历筛选器,查看它的属性名称是否与我们的筛选键匹配。如果 T 具有与我们任何筛选键匹配的属性名称,则我们检查属性的实际值是否与我们的筛选值匹配。为了提高我们搜索/筛选函数的质量,我们将使用 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;
    }
}

步骤三

- 实现扩展方法。

为了使我们编写的代码易于使用和重复使用,我们将实现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));
}

步骤四

将所有内容整合在一起。

既然我们已经拥有了所有需要的东西,让我们来看看最终/完整代码作为一个单一的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<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>,返回一个DbSet<T>,可用作一个IQueryable<T>,因此我们的函数也可以像下面示例那样使用。

例子

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

希望这个回答解决了你的问题或者指引你朝着正确的方向前进。

谢谢您查看这个问题,但我不认为这对我的情况有帮助,因为:1. 您展示的代码似乎仍然需要在编译时指定类型作为模板参数,而这正是我需要避免的;2. 这个应用程序正在使用EF访问数据库,因此使用Queryable.Where可以确保由提供程序在SQL中执行过滤,即Linq to SQL,而您使用的Where似乎是操作IEnumerable的Linq to Objects风格,这将要求从数据库中获取所有内容。但如果我误解了,请告诉我。 - Peter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接