3

I'm implementing the back end of a search/filter UI for an app using EF6. I have code that builds an Expression to use with Queryable.Where for a given DbSet, where the type of the DbSet is determined at runtime (the DBContext has a lot of them, and they may change). The call to Where works fine if I cheat by casting the Expression to a specific type first. Otherwise, I get this error:

'The best overloaded method match for 'System.Linq.Queryable.Where(System.Linq.IQueryable, System.Linq.Expressions.Expression>)' has some invalid arguments'

I'm struggling to find a way to filter the DbSet like this where the underlying 'table' type is provided at runtime. Here's a greatly simplified version of the code to illustrate:

    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://stackoverflow.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://stackoverflow.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;
    }
Peter
  • 168
  • 7
  • The thing that jumps out at me the most is your use of Queryable.Where. This could be inexperience speaking, but my experience and a cursory search for that set of methods marks the first parameter for both returned overloads with a `this` keyword. This suggests that you should be calling `dynamicSet.Where(expression)`, which lines up with the only way I've ever used this method, or seen it used, for that matter. – Inagnikai Feb 01 '19 at 20:54
  • @Inagnikai - the thing is, dynamicSet.Where won't compile - see the article in the link in the comment under that line of code ... Because the type is provided at runtime, there isn't a strongly-typed IQueryable to use at compile time. – Peter Feb 01 '19 at 21:32
  • I see what you're saying. Ostensibly, your table type is being provided by a user through some sort of control. They way you are attempting to set it up provides a very high degree of flexibility, but predictably comes at the expense of type safety. Is there any reason you couldn't map user input to a concrete type, and possibly create a generic method with an IQueryable constraint to handle the .Where() call? – Inagnikai Feb 01 '19 at 21:48
  • For this case you will have to introduce `Reflection` – Timothy Macharia Feb 01 '19 at 23:58
  • @Inagnikai - yes, a workaround would be to have a function that maps each possible value of tableType to an explicit call to Where which has the type specified in code at compile time ... but that would be hard to maintain, and I'm hoping there's a way to do this that can be used library-style without having to have special-case code for all the table types in the DbContext ... – Peter Feb 02 '19 at 11:37

2 Answers2

1

Answering you concrete question. Given

IQueryable source
LambdaExpression predicate

how to call the static generic method

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

It can be done using (A) reflection, (B) DLR dynamic dispatch and (C) Expression.Call.

What you are trying to do is option (B). However

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

does dynamic search for method having second argument of type LambdaExpression, which of course fails.

In order to be able to dynamically match the target method, you need to make the second argument dynamic as well:

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

The equivalent option (C) implementation of the above is:

var result = source.Provider.CreateQuery(Expression.Call(
    typeof(Queryable), nameof(Queryable.Where), new[] { source.ElementType },
    source.Expression, predicate));
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • 1
    you are a legend. Passing the predicate as a dynamic works perfectly. Version (C) is also interesting and could help me elsehwere. Thank you! – Peter Feb 02 '19 at 18:45
0

congrats on your first question.

Let's begin by looking at an approach for filtering a collection of data based on some custom filters. I will assume that the NameValueCollection Type you prefer to pass in your filters, holds PropertyNames as Keys and PropertyValues as Value.

Before we go forth filtering an entire collection, let's first figure out how to determine whether one object has properties that match our filters. And since we do not know the Type of our object till runtime, we will need to use Generics in C# to accomplish this.

Step 1

- Get All Class Properties

We will need to get all properties of our generic class, e.g <TClass>. Doing this using Reflection is deemed as slow and Matt Warren explains Why Reflection is slow in .NET and how to work around it. We shall therefore implement caching of class component model to get its PropertyDescriptorCollection which exists in the namespace System.ComponentModel.PropertyDescriptorCollection.

Components Cache

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

The key of our Dictionary represents the name of the generic class and the value holds the PropertyDescriptorCollection of that given class.

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

Step 2

- Loop through filters

After we have gotten the PropertyDescriptorCollection for the generic class T in the variable typeDescriptor as shown above, now let's loop through our filters and see if any of its property names match any of our filter keys. If T has a property name that matches any of our filter keys, now we inspect if the actual value of the property matches our filter value. To improve the quality of our search/filter function, we are going to use Regular Expressions in C# to determine whether a comparison is a hit or a miss.

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

Step 3

- Implement Extension Methods.

To make the code that we've written easy to use and re-use, we are going to implement Extension Methods in C# so that we can better re-use our functions anywhere within our project.

- Generic Collection Filter Function that Uses the above Function.

Since an IQueryable<T> can be converted to an IEnumerable<T> by the .Where() function in System.Linq, we are going to utilize that in our function call as shown below.

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

Step 4

Put everything together.

Now that we have everything we need, let's look at how the final/full code looks as one block of code in a single 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;
    }
}

FINALLY

Filtering IEnumerable<T>, List<T>, Arrays

This is how you are going to use the above code anywhere in your project.

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

Filtering DbSet<T>

Every DbContext has a function .Set<T> that returns a DbSet<T> that can be used as an IQueryable<T> and thus our function can be used as well as shown below.

Example

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

Hope this answers your question or rather points you in the right direction.

Community
  • 1
  • 1
Timothy Macharia
  • 2,641
  • 1
  • 20
  • 27
  • thanks for looking at that, but I don't think that helps in this case, because 1. the code you've shown appears still to require the type to be specified in code at compile time as a template argument, which is exactly what I need to avoid, and 2. this app is using EF against a database, so the use of Queryable.Where ensures the filtering is performed in SQL by the provider, i.e. linq to sql, whereas your use of Where seems to be the linq to objects flavour operating on an IEnumerable that would require everything to be fetched from the DB. But tell me if I've misunderstood. – Peter Feb 02 '19 at 11:31