4
\$\begingroup\$

This is a follow on to Part 1

In order to use my new Specifications within expression trees so I can use them in projections/navigation collections I had to write an ExpressionVisitor to find Specifications and replace them with their Expression.

In order to convert an IQueryable into one that supports the specifications, I created an Extension Method called AsSpecifiable():

public static class QueryableExtensions
{
    public static IQueryable<T> AsSpecifiable<T>(this IQueryable<T> query)
    {
        var visitableQuery = query as VisitableQuery<T>;
        return visitableQuery ?? new VisitableQuery<T>(query, new SpecificationExpander());
    }
}

Which uses a VisitableQuery:

// Based on:
// http://tomasp.net/blog/linq-expand.aspx
// http://www.albahari.com/nutshell/linqkit.html
public class VisitableQuery<T> : IOrderedQueryable<T>
{
    private readonly VisitableQueryProvider<T> provider;

    internal IQueryable<T> InnerQuery { get; } 

    internal VisitableQuery(IQueryable<T> inner, params ExpressionVisitor[] visitors)
    {
        InnerQuery = inner;
        provider = new VisitableQueryProvider<T>(this, visitors);
    }

    Expression IQueryable.Expression => InnerQuery.Expression;

    Type IQueryable.ElementType => typeof(T);

    IQueryProvider IQueryable.Provider => provider;

    public IEnumerator<T> GetEnumerator()
    {
        return InnerQuery.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return InnerQuery.GetEnumerator();
    }

    public override string ToString()
    {
        return InnerQuery.ToString();
    }
}

And the corresponding QueryProvider:

// Based on:
// http://tomasp.net/blog/linq-expand.aspx
// http://www.albahari.com/nutshell/linqkit.html
public class VisitableQueryProvider<T> : IQueryProvider
{
    private readonly ExpressionVisitor[] interceptors;

    private readonly VisitableQuery<T> query;

    internal VisitableQueryProvider(
        VisitableQuery<T> query,
        ExpressionVisitor[] interceptors)
    {
        this.query = query;
        this.interceptors = interceptors;
    }

    IQueryable<TElement> IQueryProvider.CreateQuery<TElement>(Expression expression)
    {
        return
            new VisitableQuery<TElement>(
                query.InnerQuery.Provider.CreateQuery<TElement>(
                    expression.VisitWith(interceptors)));
    }

    IQueryable IQueryProvider.CreateQuery(Expression expression)
    {
        return query.InnerQuery.Provider.CreateQuery(expression.VisitWith(interceptors));
    }

    TResult IQueryProvider.Execute<TResult>(Expression expression)
    {
        return
            query.InnerQuery.Provider.Execute<TResult>(expression.VisitWith(interceptors));
    }

    object IQueryProvider.Execute(Expression expression)
    {
        return query.InnerQuery.Provider.Execute(expression.VisitWith(interceptors));
    }
}

And finally the ExpressionVisitor:

public class SpecificationExpander : ExpressionVisitor
{
    protected override Expression VisitUnary(UnaryExpression node)
    {
        var declaringType = node.Method?.DeclaringType;
        if (declaringType != null
            && declaringType.IsGenericType
            && declaringType.GetGenericTypeDefinition() == typeof(Specification<>))
        {
            if (node.Method.Name == "op_Implicit")
            {
                var method = typeof(Specification<>).MakeGenericType(declaringType.GetGenericArguments()).GetMethod(nameof(Specification<object>.ToExpression));
                if (node.Operand.NodeType == ExpressionType.Not)
                {
                    var operand = ((UnaryExpression)node.Operand).Operand;
                    var expanded = ExpandConversion(Visit(operand), method);
                    if (expanded != null)
                    {
                        var negate = typeof(ExpressionExtensions).GetMethod(nameof(ExpressionExtensions.Negate)).MakeGenericMethod(declaringType.GetGenericArguments());
                        return (Expression)negate.Invoke(null, new object[] { expanded }); ;
                    }
                }
                else if (node.Operand.NodeType == ExpressionType.Call)
                {
                    var expression = Visit(node.Operand);
                    var expanded = ExpandConversion(expression, method);
                    if (expanded != null)
                    {
                        return expanded;
                    }
                }
                else
                {
                    var expanded = ExpandConversion(node.Operand, method);
                    if (expanded != null)
                    {
                        return expanded;
                    }
                    throw new InvalidOperationException($"Cannot convert the specification ({node.Type.Name}) to an expression");
                }
            }
        }
        return base.VisitUnary(node);
    }

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        var declaringType = node.Method.DeclaringType;
        if (declaringType != null
            && declaringType.IsGenericType
            && declaringType.GetGenericTypeDefinition() == typeof(Specification<>))
        {
            if (node.Method.Name == nameof(Specification<object>.ToExpression))
            {
                var expanded = ExpandConversion(Visit(node.Object), node.Method);
                if (expanded != null)
                {
                    return expanded;
                }
            }
            else if (node.Method.Name == nameof(Specification<object>.Negate))
            {
                var value = GetValue((MemberExpression)node.Object);
                var negate = typeof(Specification<>).MakeGenericType(declaringType.GetGenericArguments()).GetMethod(nameof(Specification<object>.Negate));
                var specification = negate.Invoke(value, null);
                return Expression.Constant(specification, specification.GetType());
            }
            else
            {
                throw new InvalidOperationException(
                    $"{node.Method.Name} cannot be used within an expression");
            }
        }
        return base.VisitMethodCall(node);
    }

    private Expression ExpandConversion(Expression node, MethodInfo method)
    {
        var newExpression = node as NewExpression;
        if (newExpression != null)
        {
            var parameters = GetArgumentValues(newExpression.Arguments);
            var specification = newExpression.Constructor.Invoke(parameters);
            return (Expression)method.Invoke(specification, null);
        }
        var memberExpression = node as MemberExpression;
        if (memberExpression != null)
        {
            return (Expression)method.Invoke(GetValue(memberExpression), null);
        }
        var constantExpression = node as ConstantExpression;
        if (constantExpression != null)
        {
            return (Expression)method.Invoke(constantExpression.Value, null);
        }
        return null;
    }

    private object[] GetArgumentValues(IReadOnlyList<Expression> arguments)
    {
        if (arguments?.Count == 0)
        {
            return null;
        }
        var results = new object[arguments.Count];
        for (var i = 0; i < arguments.Count; i++)
        {
            var constant = arguments[i] as ConstantExpression;
            if (constant != null)
            {
                results[i] = constant.Value;
                continue;
            }

            var member = arguments[i] as MemberExpression;
            if (member != null)
            {
                results[i] = GetValue(member);
                continue;
            }
            throw new InvalidOperationException("Invalid argument to specification constructor.");

        }
        return results;
    }

    // http://stackoverflow.com/a/2616980/1402923
    public static object GetValue(Expression expression)
    {
        var objectMember = Expression.Convert(expression, typeof(object));
        var getterLambda = Expression.Lambda<Func<object>>(objectMember);
        return getterLambda.Compile().Invoke();
    }
}

This question relies on code which is present in part 1 so you'll need to grab all of the code there if you want to run it. That said, here's what it looks like when it's used:

var activeMemberAlertFinders = from s in subscriptions.AsSpecifiable()
                               from m in s.Members.AsQueryable().Where(new MemberIsActive())
                               from a in m.Alerts.AsQueryable().Where(new AlertIsActive())
                               select a.Scope;

All of this code works but I can't help but feel I massively over complicated the Visitor - I've never written one that does this sort of thing before.

How can I improve this code or should I just scrap it and go back to using static fields storing Expressions?

Edit

Just in case it makes life easier, I've put all of the code on GitHub minus the tests which I have yet to migrate away from Machine.Specifications.

\$\endgroup\$
  • 1
    \$\begingroup\$ I know the pain about the Visitor. I was also experimenting with it and there is as good as no documentation, examples or anything hepful other then the basic stuff. \$\endgroup\$ – t3chb0t Aug 16 '16 at 16:53
  • \$\begingroup\$ @t3chb0t - yeah exactly. I thought I had a good grasp on expression trees before I started this. I now realise that they are much more complicated than I gave them credit for! \$\endgroup\$ – RobH Aug 17 '16 at 15:24
  • \$\begingroup\$ Would you let me know when you've migrated the tests please? I'd like to play around with it. \$\endgroup\$ – t3chb0t Aug 20 '16 at 13:48
  • \$\begingroup\$ @t3chb0t - have my in-laws staying over the weekend so haven't been able to make much progress on it. I just sneaked off for a bit and did a couple (9) of the easy tests which should hopefully give you something to go on! \$\endgroup\$ – RobH Aug 21 '16 at 17:41
  • \$\begingroup\$ Thank you ;-) it's nothing urgent. I've updated my fork. \$\endgroup\$ – t3chb0t Aug 21 '16 at 18:04
0
\$\begingroup\$

Looking through the code it's ok. I wouldn't scrap it all and start again as the usage is clean and sensible.

The code is readable and as you've based this on a pattern should be maintainable.

\$\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.