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?