1

I have a custom implementation of IDataServiceMetadataProvider / IDataServiceQueryProvider / IDataServiceUpdateProvider, put together from various examples found around the web. Until now, all my entities had been well defined, and everything functions as desired. I am using EF 4.3. However now, I would like to allow an entity to include ad-hoc properties.

For this problem, let's say that I have two entities: Person and Property (collected in People and Properties). These are simple objects:

public class Person
{
    public Guid Id { get; set; }
    public virtual IList<Property> Properties { get; set; }
}

public class Property
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Value { get; set; }
    public Person Person { get; set; }
}

For configuration, Person has:

// specify person and property association
HasMany(p => p.Properties).WithRequired(a => a.Person).Map(x => x.MapKey("PERSONID"));

My DB structure matches, so there's nothing tricky there. The metadata for Person does not expose the Property list.

Implementing IDataServiceQueryProvider.GetOpenPropertyValues is straightforward enough; I can see how to return my entities with their particular properties. However, when I make a request along the lines of:

GET /Service/People?$filter=A eq '1'

...I run unto serious trouble. I am using a custom IQueryProvider so that I can insert my own ExpressionVisitor. I have confidence in this code, because I am using it to intercept and handle some ResourceProperty's with CanReflectOnInstanceTypeProperty set to false. So I have overridden ExpressionVisitor.VisitMethodCall, and detected when OpenTypeMethod.Equal and OpenTypeMethod.GetValue are called.

My problem is that once I have those, I have no idea how to effectively replace the expression tree with something that will handle my DB structure. The expression that I'm trying to replace looks something like ((GetValue(it, "A") == Convert("1")) == True). I know that 'it' is an expression representing my Person entity. What I can't figure out is how to create an Expression that is Linq-To-Entities compatible, which will evaluate, for a given Person, whether it has a property with the specified name and matching value. Any advice on this would be appreciated. Perhaps what has be most perplexed it how to reduce a general query which results in an IQueryable down to a single element that can be compared to.

Thanks for any advice!

THE ANSWER

OK, it took me a while more to get this sorted out, but thanks to Barry Kelly's answer to this post, I got it working.

We start off with the Expression Visitor implementation. We need to override VisitMethodCall and catch the call to OpenTypeMethods.GetValue, VisitBinary to handle comparison operations (Equal and GreaterThanOrEqual in this code, but more are required for full functionality), and VisitUnary to handle Convert (which I'm not sure of the need for, but it's working for me).

public class LinqToObjectExpressionVisitor : ExpressionVisitor
{
    internal static readonly MethodInfo GetValueOpenPropertyMethodInfo = 
        typeof(OpenTypeMethods).GetMethod("GetValue", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(object), typeof(string) }, null);

    internal static readonly MethodInfo GreaterThanOrEqualMethodInfo = 
        typeof(OpenTypeMethods).GetMethod("GreaterThanOrEqual", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(object), typeof(object) }, null);

    internal static readonly MethodInfo EqualMethodInfo = 
        typeof(OpenTypeMethods).GetMethod("Equal", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(object), typeof(object) }, null);

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Method == GetValueOpenPropertyMethodInfo)
        {
            return Visit(LinqResolver.ResolveOpenPropertyValue(node.Arguments[0], node.Arguments[1]));
        }

        return base.VisitMethodCall(node);
    }

    protected override Expression VisitBinary(BinaryExpression node)
    {
        MethodInfo mi = node.Method;
        if (null != mi && mi.DeclaringType == typeof(OpenTypeMethods))
        {
            Expression right = Visit(node.Right);
            ConstantExpression constantRight = right as ConstantExpression;
            if (constantRight != null && constantRight.Value is bool)
            {
                right = Expression.Constant(constantRight.Value, typeof(bool));
            }

            Expression left = Visit(node.Left);

            if (node.Method == EqualMethodInfo)
            {
                return Expression.Equal(left, right);
            }
            if (node.Method == GreaterThanOrEqualMethodInfo)
            {
                return Expression.GreaterThanOrEqual(left, right);
            }
        }

        return base.VisitBinary(node);
    }

    protected override Expression VisitUnary(UnaryExpression node)
    {
        if (node.NodeType == ExpressionType.Convert)
        {
            return Visit(node.Operand);
        }
        return base.VisitUnary(node);
    }
}

So what's LinqResolver? That's my own class to handle the expression tree rewrites.

public class LinqResolver
{
    public static Expression ResolveOpenPropertyValue(Expression entityExpression, Expression propertyExpression)
    {
        ConstantExpression propertyNameExpression = propertyExpression as ConstantExpression;
        string propertyName = propertyNameExpression.Value as string;

        // {it}.Properties
        Expression propertiesExpression = Expression.Property(entityExpression, typeof(Person), "Properties");

        // (pp => pp.Name == {name})
        ParameterExpression propertyParameter = Expression.Parameter(typeof(Property), "pp");
        LambdaExpression exp = Expression.Lambda(
            Expression.Equal(Expression.Property(propertyParameter, "Name"), propertyNameExpression),
            propertyParameter);

        // {it}.Properties.FirstOrDefault(pp => pp.Name == {name})
        Expression resultProperty = CallFirstOrDefault(propertiesExpression, exp);

        // {it}.Properties.FirstOrDefault(pp => pp.Name == {name}).Value
        Expression result = Expression.Property(resultProperty, "Value");

        return result;
    }

    private static Expression CallFirstOrDefault(Expression collection, Expression predicate)
    {
        Type cType = GetIEnumerableImpl(collection.Type);
        collection = Expression.Convert(collection, cType);

        Type elemType = cType.GetGenericArguments()[0];
        Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));

        // Enumerable.FirstOrDefault<T>(IEnumerable<T>, Func<T,bool>)
        MethodInfo anyMethod = (MethodInfo)
            GetGenericMethod(typeof(Enumerable), "FirstOrDefault", new[] { elemType },
                new[] { cType, predType }, BindingFlags.Static);

        return Expression.Call(anyMethod, collection, predicate);
    }
}

For me, the trick was recognizing that I could use IEnumberable methods in my tree, and that if I left a Call expression in my tree, that was OK, as long as I visited that node again, because Linq To Entities would then replace it for me. I had been thinking that I needed to get the expression down to the just the expressions supported directly by Linq-To-Entities. But in fact, you can leave in more complex expressions, as long as they can be translated

The implementation of CallFirstOrDefault is just like Barry Kelly's CallAny (again, his post, which includes the implementation of GetIEnumerableImpl.)

Community
  • 1
  • 1
object88
  • 720
  • 1
  • 7
  • 20

1 Answers1

0

The best way to figure this out (which I use a lot) is to try to write some sample code directly against EF which would give you the desired results. Once you get that working it is usually reasonably simple to create the matching expression (if nothing else, you can use the sample query and view it in the debugger to see the nodes and such).

In this particular case you will have to translate it to some kind of join. Maybe something like

it.Properties.Any(p => p.Name == "A" && p.Value == "1")

But I didn't try to see if EF can handle such condition.

Vitek Karas MSFT
  • 13,130
  • 1
  • 34
  • 30
  • Thanks for the tip! Marking this as the answer because it pointed me in the direction of using Expressions to directly invoke Any. – object88 Apr 04 '12 at 18:33