7
\$\begingroup\$

Up until recently, my LINQ-to-Sage provider didn't support projections, so the client code had to explicitly "transfer" to LINQ-to-Objects, like this:

var vendorCodes = context.Vendors.ToList().Select(e => e.Code);

Now, with a bit of help from Stack Overflow, I was able to modify my IQueryProvider implementation to support this:

var vendorCodes = context.Vendors.Select(e => e.Code);

Or even this:

var vendors = context.Vendors.Select(e => new { e.Code, e.Name });

Under the hood it's still LINQ-to-Objects handling it. Here's the IQueryProvider implementation:

public class SageQueryProvider<TEntity> : IQueryProvider
    where TEntity : EntityBase
{
    private readonly IView _view;
    private readonly SageContextBase _context;

    public SageQueryProvider(IView view, SageContextBase context)
    {
        _view = view;
        _context = context;
    }

    public IQueryable CreateQuery(Expression expression)
    {
        var elementType = TypeSystem.GetElementType(expression.Type);
        try
        {
            return (IQueryable)Activator.CreateInstance(typeof (ViewSet<TEntity>).MakeGenericType(elementType), _view, this, expression, _context);
        }
        catch (TargetInvocationException exception)
        {
            throw exception.InnerException;
        }
    }

    public IQueryable<TResult> CreateQuery<TResult>(Expression expression)
    {
        var elementType = TypeSystem.GetElementType(expression.Type);
        if (elementType == typeof(EntityBase))
        {
            Debug.Assert(elementType == typeof (TResult));
            return (IQueryable<TResult>)Activator.CreateInstance(typeof(ViewSet<>).MakeGenericType(elementType), _view, this, expression, _context);
        }

        var methodCallExpression = expression as MethodCallExpression;
        if(methodCallExpression != null && methodCallExpression.Method.Name == "Select")
        {
            return (IQueryable<TResult>)Execute(methodCallExpression);
        }

        throw new NotSupportedException(string.Format("Expression '{0}' is not supported by this provider.", expression));
    }

    public object Execute(Expression expression)
    {
        return Execute(expression, new ViewSet<TEntity>(_view, _context));
    }

    public TResult Execute<TResult>(Expression expression)
    {
        return (TResult)Execute(expression, new ViewSet<TEntity>(_view, _context));
    }

    private static object Execute<T>(Expression expression, ViewSet<T> viewSet)
        where T : EntityBase
    {
        var constantExpression = expression as ConstantExpression;
        if (constantExpression != null)
        {
            if (constantExpression.Value is ViewSet<T>)
            {
                return viewSet.Select(string.Empty);
            }
        }

        var filterFinder = new InnermostFilterFinder();
        var filterExpression = filterFinder.GetInnermostFilter(expression);
        var filter = string.Empty;
        if (filterExpression != null)
        {
            if (filterExpression.Arguments.Count > 1)
            {
                var lambdaExpression =
                    (LambdaExpression)((UnaryExpression)(filterExpression.Arguments[1])).Operand;

                // Send the lambda expression through the partial evaluator.
                lambdaExpression = (LambdaExpression)Evaluator.PartialEval(lambdaExpression);

                // Get the filter string to pass to the Sage API.
                var visitor = new FilterVisitor<T>(lambdaExpression.Body);
                filter = visitor.Filter;
            }

            switch (filterExpression.Method.Name)
            {
                case "Where":
                    return viewSet.Select(filter);
                case "Single":
                    var singleResult = viewSet.SingleOrDefault(filter);
                    if (singleResult == null)
                    {
                        throw new InvalidOperationException("Sequence contains more than one element.");
                    }
                    return singleResult;
                case "SingleOrDefault":
                    return viewSet.SingleOrDefault(filter);
                case "First":
                    var firstResult = viewSet.FirstOrDefault(filter);
                    if (firstResult == null)
                    {
                        throw new InvalidOperationException("Sequence contains no element matching specified criteria.");
                    }
                    return firstResult;
                case "FirstOrDefault":
                    return viewSet.FirstOrDefault(filter);

                case "Last":
                    var lastResult = viewSet.LastOrDefault(filter);
                    if (lastResult == null)
                    {
                        throw new InvalidOperationException("Sequence contains no element matching specified criteria.");
                    }
                    return lastResult;
                case "LastOrDefault":
                    return viewSet.LastOrDefault(filter);
                case "Count":
                    return viewSet.Count(filter);
                case "Any":
                    return viewSet.Any(filter);
                case "All":
                    return viewSet.All(filter);

                default:
                    throw new NotSupportedException("Method '" + filterExpression.Method.Name + "' is not currently supported by this provider.");
            }
        }

        var method = expression as MethodCallExpression;
        if (method != null && method.Method.Name == "Select")
        {
            // handle projections
            var lambda = ((UnaryExpression)method.Arguments[1]).Operand as LambdaExpression;
            if (lambda != null)
            {
                var returnType = lambda.ReturnType;
                var selectMethod = typeof(Queryable).GetMethods().First(m => m.Name == "Select");
                var typedGeneric = selectMethod.MakeGenericMethod(typeof(T), returnType);
                var result = typedGeneric.Invoke(null, new object[] { viewSet.ToList().AsQueryable(), lambda }) as IEnumerable;
                return result;
            }
        }

        return viewSet.Select(filter);
    }
}

As you can see this class has changed quite dramatically since when I first wrote it, and it doesn't look like it's becoming any prettier - especially now that I'm considering adding support for SelectMany.

How should I cure it?

\$\endgroup\$
  • \$\begingroup\$ This project doesn't use C# 6 does it? \$\endgroup\$ – RobH Jun 17 '16 at 8:40
  • \$\begingroup\$ @RobH indeed, it doesn't. \$\endgroup\$ – Mathieu Guindon Jun 17 '16 at 13:58
3
\$\begingroup\$

One of the things I think when I see a large case statement is would this be better off as some kind of lookup table. I think yours might have some scope for doing this since they all seem to do some processing on a viewSets and a filter. Using a simple lookup table to convert the string "Where", "Single" etc into a method call would allow you to separate the logic out a bit more. So, for example you could do something like this (better naming is left as an exercise for the reader):

public static class FilterExpressionHelper
{
    readonly static Dictionary<string, MethodInfo> _methods;

    static FilterExpressionHelper()
    {
        _methods = new Dictionary<string, MethodInfo>();

        foreach(var methodInfo in typeof(FilterExpressionHelper).GetMethods(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public).Where(x=>x.Name != "Execute"))
        {
            _methods.Add(methodInfo.Name, methodInfo);
        }
    }

    public static object Execute<T>(string methodName, ViewSet<T> view, string filter)
    {
        if(_methods.ContainsKey(methodName))
            return _methods[methodName].MakeGenericMethod(typeof(T)).Invoke(null, new object [] { view, filter });
        throw new NotSupportedException($"Method '{methodName}' is not currently supported by this provider.");
    }

    public static object Where<T>(ViewSet<T> viewSet, string filter)
    {
        return viewSet.Select(filter);
    }
    public static object Single<T>(ViewSet<T> viewSet, string filter)
    {
        var singleResult = viewSet.SingleOrDefault(filter);
        if (singleResult == null)
        {
            throw new InvalidOperationException("Sequence contains more than one element.");
        }
        return singleResult;
    }
    public static object SingleOrDefault<T>(ViewSet<T> viewSet, string filter)
    {
        return viewSet.SingleOrDefault(filter);
    }
    // etc
}

This would allow you to replace the large case statement in your filter logic with:

FilterExpressionHelper.Execute(filterExpression.Method.Name, viewSet, filter);
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.