3

I'm building an MVC API in .NET Core. I have several really big tables (3rd party system) and I'm trying to allow the name of any column to be passed as a URL parameter with a value to use in a Where clause. This uses the Entity Framework and .NET Core.

The idea is to grab the column matching the parameter name and use it in the Where clause with the parameter's value. I want the lambda in the Where query to end up looking like:

//GET: api/DWRWorkItems?Parameter=Value
...
dWRWorkItem = dWRWorkItem.Where(m => 
   m.Parameter == Value
);

This is my attempt at a clean solution, the route code from the controller. The Model and Views are unchanged from the default created by Entity Framework.

    // GET: api/DWRWorkItems
    [HttpGet]
    public IEnumerable<DWRWorkItem> GetTDwrWrkItm()
    {
        IQueryable<DWRWorkItem> dWRWorkItem = _context.TDwrWrkItm.Where(m => 1 == 1);
        var q = HttpContext.Request.Query;
        foreach (string p in q.Keys)
        {

            dWRWorkItem = dWRWorkItem.Where(m => 
                m.GetType().GetProperty(p).GetValue(m, null) == q[p]
            );

        }

        return dWRWorkItem.ToList();
    }

Intellisense (VS 2017) doesn't expect an errors, but when I run this, I get:

The operands for operator 'Equal' do not match the parameters of method 'op_Equality'.

Stepping through the code, it appears that the reflection in the lambda isn't working as expected. Here is what dWRWorkItem.Expression.Arguments[1] looks like:

{m => (GetProperty(m.GetType(), value(EBOOKS.Controllers.DWRWorkItemsController+<>c__DisplayClass0#1).p).GetValue(m, null) == value(EBOOKS.Controllers.DWRWorkItemsController+<>c__DisplayClass0#1).CS$<>8__locals1.q.get_Item(value(EBOOKS.Controllers.DWRWorkItemsController+<>c__DisplayClass0#1).p))}

While an example where the parameter is not dynamic looks like this:

{m => (m.ContId == value(EBOOKS.Controllers.DWRWorkItemsController+<>c__DisplayClass3_0).id)}

Josiah
  • 3,008
  • 1
  • 34
  • 45
  • 1
    `m.GetType().GetProperty(p).GetValue(m, null)` is returning an object, but `q[p]` is returning a string. I would expect that is where the problem lies. – Matthew Jaspers Mar 21 '17 at 18:25
  • @BenJaspers I guess my question is: how is `m.GetType().GetProperty(p).GetValue(m, null) // string p = "CustomString"` different from `m.CustomString`? In my case, m.CustomString works, while the reflection method doesn't. Are they different types? – Josiah Mar 21 '17 at 18:27
  • Googling your error message suggests the issue could be that the object returned by `m.GetType().GetProperty(p).GetValue(m, null)` is a nullable string if the property `p` is a string while `q[p]` is not nullable. Or I have that backwards and `q[p]` is nullable and the other is not, the docs aren't entirely clear. – Matthew Jaspers Mar 21 '17 at 18:31
  • Reflection based implementation cannot be translated to SQL, so even if you fix the current exception you'll get just another. What you really need is to build dynamically predicate expression. There must be a lot of SO examples how to do something similar. – Ivan Stoev Mar 21 '17 at 18:37
  • @BenJaspers - Following the SO question chain that seemed to say that, it appeared that that was a bug that was resolved in 2013: http://stackoverflow.com/questions/16797890/why-are-generic-and-non-generic-structs-treated-differently-when-building-expres – Josiah Mar 21 '17 at 18:37
  • @IvanStoev - that seems plausible. I ran up against limitations of the lambda that were different than "normal" lambdas. Is there a place to look for such limitations? – Josiah Mar 21 '17 at 18:39
  • Oops, sorry, you are on EF Core. So probably it could be made to work with reflection, but EF Core will evaluate it in memory (after retrieving all the table data), so it's still better to build expression using `System.Linq.Expressions.Expression` class methods. What is the type of the `q` variable - `Dictionary`? – Ivan Stoev Mar 21 '17 at 18:44
  • @IvanStoev q is an IQueryCollection, from `HttpContext.Request.Query` – Josiah Mar 21 '17 at 19:08

1 Answers1

3

In general query providers other than LINQ to Objects do not like reflection calls inside expression tree, so it's better to compose dynamically expression using the System.Linq.Expressions.Expression class methods.

Here is a custom extension method suitable for your case:

public static partial class QueryableExtensions
{
    public static IQueryable<T> WhereEquals<T>(this IQueryable<T> source, string memberPath, string value)
    {
        var parameter = Expression.Parameter(typeof(T), "e");
        var left = memberPath.Split('.').Aggregate((Expression)parameter, Expression.PropertyOrField);
        var right = Expression.Constant(ToType(value, left.Type), left.Type);
        var predicate = Expression.Lambda<Func<T, bool>>(Expression.Equal(left, right), parameter);
        return source.Where(predicate);
    }

    private static object ToType(string value, Type type)
    {
        if (type == typeof(string)) return value;
        if (string.IsNullOrEmpty(value)) return null;
        return Convert.ChangeType(value, Nullable.GetUnderlyingType(type) ?? type);
    }
}

Additionally to using Expression.Equal (which is the equivalent of == operator) it has to deal with string to actual value type conversion, which btw is the cause of your original exception.

The usage is like this:

var dWRWorkItem = _context.TDwrWrkItm.AsQueryable();
var q = HttpContext.Request.Query;
foreach (string p in q.Keys)
    dWRWorkItem = dWRWorkItem.WhereEquals(p, q[p]);    
return dWRWorkItem.ToList();
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • That is quite elegant and works well. I'm relatively new to .NET, but do you have any ideas why isn't this standard practice for APIs? – Josiah Mar 22 '17 at 10:29
  • I have no idea, it might be for some special framework APIs like OData, but again I'm not sure. Could be because they just provide you the arguments through `IQueryCollection` and let you do whatever you like with them? :) – Ivan Stoev Mar 22 '17 at 10:41
  • Well, thanks very much. For completeness, in my implementation I wrapped the `dWRWorkItem.WhereEquals()` in a try-catch block so it gracefully handles parameters that aren't column names. – Josiah Mar 22 '17 at 10:49