views:

411

answers:

5

Is there anyone here with experience writing custom Linq proviers?

What I'm trying to do is tell whether a MemberExpression that is a property on a business object should be included in the sql, or treated as a constant, because its from a local variable that just happens to be a business object.

So for example, if you have this:

Customer c = LoadCustomerFromDatabase();

var orders = from o in db.Orders() where o.CustomerID == c.CustomerID select o;

At the moment, my query translator will try to execute "SELECT * FROM orders o where o.CustomerID = c.CustomerID", which of course doesn't work.

What I would like to do is examine the MemberExpression on the "c.CustomerID" and try to work out if its a local variable, or just something that is being used as part of the linq expression.

I have managed to do it as a 2nd pass over the query, looking for fields that SQL Server won't be able to bind, and injecting their values instead, but if possible I'd like to get it all happening at the same time. I tried looking at the expression Type property, and IsAutoClass, but that was just a guess because it contained the word Auto. And it didnt work :)

A: 

Well, I don't know if you can cut it down to one pass, but you can get information on the member, and if it coincides with another variable you have declared as being part of the query (in this case "o"), you use it to generate your query.

Otherwise, you would assume it is a constant, and then plug that value in.

Unfortunately, because you can have from statements (in addition to the let statement) in multiple places in the query, it doesn't seem like you can do it in just one pass, since you need to know all of the query variables up front.

casperOne
A: 

Yeah, I agree you can't know everything up front if you only know the contents of the query, but I was hoping it was possible to somehow discover the scoping of the object. So far I haven't seen anything different about the types.

I can try to get the value of the member, and catch the "Lambda parameter is not in scope" exception, but that's a terrible use of exceptions if you ask me

Ch00k
A: 

Okay, after some quick statistical analysis (ie, comparing individual properties manually), DeclaringType, ReflectedType and Namespace are when Lambda parameter is not in scope" fires.

So unless someone comes up with a better answer, that might be all I have to go on.

Ch00k
A: 

In a Where expression, you are looking at an Expression<Func<T,bool>> - which means that the outermost lambda should have a single ParameterExpression with type T.

If a comparison relates to the row, it will have (as an ancestor) this ParameterExpression; if it is local variable, it will have (as an ancestor) a ConstantExpression - however, the type of this constant expression will be compiler generated to cope with all of the captured variables used in the expression.

Like so:

using System;
using System.Linq.Expressions;
class Foo
{
    public string Name { get; set; }
    static void Main()
    {
        var exp = (LambdaExpression) GetExpression();
        WalkTree(0, exp.Body, exp.Parameters[0]);

    }
    static void WriteLine(int offset, string message)
    {
        Console.WriteLine(new string('>',offset) + message);
    }
    static void WalkTree(int offset, Expression current,
        ParameterExpression param)
    {
        WriteLine(offset, "Node: " + current.NodeType.ToString());
        switch (current.NodeType)
        {
            case ExpressionType.Constant:
                WriteLine(offset, "Constant (non-db)"
                    + current.Type.FullName);
                break;
            case ExpressionType.Parameter:
                if (!ReferenceEquals(param, current))
                {
                    throw new InvalidOperationException(
                        "Unexpected parameter: " + param.Name);
                }
                WriteLine(offset, "db row: " + param.Name);
                break;
            case ExpressionType.Equal:
                BinaryExpression be = (BinaryExpression)current;
                WriteLine(offset, "Left:");
                WalkTree(offset + 1, be.Left, param);
                WriteLine(offset, "Right:");
                WalkTree(offset + 1, be.Right, param);
                break;
            case ExpressionType.MemberAccess:
                MemberExpression me = (MemberExpression)current;
                WriteLine(offset, "Member: " + me.Member.Name);
                WalkTree(offset + 1, me.Expression, param);
                break;
            default:
                throw new NotSupportedException(
                    current.NodeType.ToString());
        }
    }

    static Expression<Func<Foo, bool>> GetExpression()
    {
        Foo foo = new Foo { Name = "abc" };

        return row => row.Name == foo.Name;
    }    
}
Marc Gravell
A: 

I'll have to have a play with that. Unfortunately my example is fair bit more complicated, because the parameter is not necessarily the business object. It could be a compiler generated type (eg, when comparing two fields from two different tables, or when used in a Select), which means its a member of a member. I guess at some point it comes back to a constant expression and parameter probably, but the depth is indeterminate as far as I can tell. Plus there is no Parent property on Expression :)

Ch00k