ket
ket

Reputation: 758

How to consume a delegate within an expression

I'm struggling a bit to understand how to construct an expression that consumes a delegate. I'm new to expressions and embarrassingly wasn't able to create a unit test that replicated the problem I'm seeing, so hopefully the information below will be sufficient to explain the problem.

Consider the following classes:

public class Instance
{
    internal Instance(string value)
    {
        Value = value;
    }

    public string Value { get; }
    public Expression<Func<Container, bool>> IsContainerMatch => c => Selector(c.Item) == Value;
    public Func<Item, string> Selector => i => i.Value;

}

public class Container
{
    internal Container(Item item)
    {
        Item = item;
    }
    public Item Item { get; }
}

public class Item
{
    internal Item(string value)
    {
        Value = value;
    }
    public string Value { get; }
}

The IsContainerMatch expression is used as an argument on a per-Instance basis for a third party method and is compiled/used at a later time. When the expression is actually called, however, I get an error that states that the variablecis referenced from scope '' but is not defined. If I'm not mistaken, this problem can be solved if I can incorporate the Selector delegate into the expression, so that the two have the same scope. (Please do correct me if I'm wrong!)

I came across this issue, but one fundamental difference I see is that the argument to my delegate is not a constant; it's determined at compile time. I haven't had much luck figuring out how to construct the expression for my scenario. Could someone provide me with a little guidance?

EDIT: This is the simplest test I can construct that fails - sorry it's not readily repeatable. The issue only occurs when I attempt to use the expression in conjunction with NHibernate; calling the func works fine when I use the method from @juharr. When I inject the expression into the .And statement, I get the error message variable c of type Container referenced from scope '', but it is not defined.

        [Fact]
        public void Test()
        {
            var instanceList = new Collection<Instance>{new Instance("test")};
            var dbConfig = OracleDataClientConfiguration.Oracle10
                .ConnectionString(c => c.Is("Data Source=(DESCRIPTION =(ADDRESS = (PROTOCOL = TCP)(HOST = 10.11.12.13)(PORT = 1521))(CONNECT_DATA = (SERVER = DEDICATED)(SERVICE_NAME = orcl.test.com)));Password=test;User ID=test;"))
                .Driver<NHibernate.Driver.OracleManagedDataClientDriver>();

            FluentConfiguration configuration = Fluently.Configure().Database(dbConfig)
                .Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly()));

            var sessionFactory = configuration.BuildSessionFactory();
            var session = sessionFactory.OpenSession();
            var query = session.QueryOver<Container>();
            foreach (var element in instanceList) query.And(c => element.Selector(c.Item) == element.Value);

            var query2 = session.QueryOver<Container>();
            foreach (var element in instanceList) query.And(element.IsContainerMatch);
        }

EDIT #2: Note the query2 above; it is a second use case I have. The first query is the one that throws the exception, but my intent is to reuse the delegate for both the first query as well as in the expression for the second query.

Upvotes: 1

Views: 137

Answers (1)

Jeremy Lakeman
Jeremy Lakeman

Reputation: 11120

Ok, lets unpack this;

public Expression<Func<Container, bool>> IsContainerMatch => c => Selector(c.Item) == Value;

IsContainerMatch is a property, returning a new instance of an expression on each get. The expression contains constant references to the Instance to access Value and Selector. This is roughly equivalent to;

public Expression<Func<Container, bool>> IsContainerMatch { get {
    var inst = Expression.Constant(this);
    var container = Expression.Parameter(typeof(Container), "c");
    var selector = typeof(Instance).GetProperty("Selector");
    var value = typeof(Instance).GetProperty("Value");
    var item = typeof(Container).GetProperty("Item");
    return Expression.Lambda<Func<Container, bool>>(
        Expression.Equal(
            Expression.Invoke(
                Expression.MakeMemberAccess(inst, selector),
                Expression.MakeMemberAccess(container, item)
                ),
            Expression.MakeMemberAccess(inst, value)
            ),
        container
    );
} }

This is unlikely to be the true source of your exception. Somewhere else a new LambdaExpression has been constructed, perhaps from pieces of this Expression, with a reference to this ParameterExpression 'C'. But with a different parameter.

For example something like this might cause that Exception;

    ...
    return Expression.Lambda<Func<Container, bool>>(
        Expression.Equal(
            Expression.Invoke(
                Expression.MakeMemberAccess(inst, selector),
                Expression.MakeMemberAccess(Expression.Parameter(typeof(Container), "X"), item)
                ),
            Expression.MakeMemberAccess(inst, value)
            ),
        Expression.Parameter(typeof(Container), "Y")
    );

Clearly you are attempting to use a type of Expression that your 3rd party library doesn't support.

Now that you've updated the question to include NHibernate, it seems like what you're trying to achieve is;

foreach (var element in instanceList) query.And(c => c.Item.[Dynamic member] == element.Value);

So that the criteria can be evaluated efficiently by NHibernate. But since your Selector is a compiled Func, there's no way for NHibernate to look inside that method and translate this into an efficient query.

Upvotes: 1

Related Questions