Define part of an Expression as a variable in c#

2020-02-12 07:59发布

问题:

I have following code:

public class MyClass<T>
{
   Expression<Func<T,bool>> Criteria {get; set;}
}
public class Customer
{
   //..
   public string Name {get; set;}
} 

and use it as following:

var c = new MyClass<Customer>();
c.Criteria = x.Name.StartWith("SomeTexts");

Is there any way to define something like this:

? p = x=>x.Customer.Name;
var c = new MyClass<Customer>();
c.Criteria = p => p.StartWith("SomeTexts");

I used Expression<Func<T,bool>> to use it as where clause in my linq to entities query (EF code first).

回答1:

You can use the following helper functions (one could probably give them a better names, but that's not essential):

public static class ExpressionUtils
{
    public static Expression<Func<TOuter, TResult>> Bind<TOuter, TInner, TResult>(this Expression<Func<TOuter, TInner>> source, Expression<Func<TInner, TResult>> resultSelector)
    {
        var body = new ParameterExpressionReplacer { source = resultSelector.Parameters[0], target = source.Body }.Visit(resultSelector.Body);
        var lambda = Expression.Lambda<Func<TOuter, TResult>>(body, source.Parameters);
        return lambda;
    }

    public static Expression<Func<TOuter, TResult>> ApplyTo<TInner, TResult, TOuter>(this Expression<Func<TInner, TResult>> source, Expression<Func<TOuter, TInner>> innerSelector)
    {
        return innerSelector.Bind(source);
    }

    class ParameterExpressionReplacer : ExpressionVisitor
    {
        public ParameterExpression source;
        public Expression target;
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node == source ? target : base.VisitParameter(node);
        }
    }
}

Let see how the sample expression

c.Criteria = x => x.Name.StartsWith("SomeTexts");

can be built from the two different parts.

If you have

Expression<Func<Customer, string>> e = x => x.Name;

then

c.Criteria = e.Bind(x => x.StartsWith("SomeTexts"));

or if you have this instead

Expression<Func<string, bool>> e = x => x.StartsWith("SomeTexts");

then

c.Criteria = e.ApplyTo((Customer x) => x.Name);

If you have both expressions, then you can use any of the two functions, since a.Bind(b) is equivalent to b.ApplyTo(a).



回答2:

You must define type variable explicit but next code will help you to solve your scenario:

// define new expression that get an Order object and returns string value
Expression<Func<Order, string>> p = x => x.Customer.Name;
var c = new MyClass<Order>();

// Compile the expression to the Func then invoke it and call extra criteria
c.Criteria = o => p.Compile().Invoke(o).StartsWith("SomeText");

There is little bit simpler solution without expressions:

Func<Order, string> p = x => x.Customer.Name;
var c = new MyClass<Order>();
c.Criteria = o => p(o).StartsWith("SomeText");

You can also use Func<> instead of Expression<> in MyClass:

public MyClass<T>
{
   Func<T,bool> Criteria {get; set;}
}


回答3:

I don't see the benefit of using an Expression here. How about a straight Func?

public class MyClass<T> 
{
    public Func<T, string, bool> Criteria { get; set; }
}

And then...

var myCustomer = new MyClass<Customer>
{
    Criteria = (c, s) => c.Name.StartsWith(s)
};

var customer = new Customer { Name = "Bob" };

var x = myCustomer.Criteria(customer, "B");


回答4:

If you want an expression, then you can use LinqKit to do the following:

Expression<Func<Customer, string>> p = x => x.Name;

var c = new MyClass<Customer>();

c.Criteria = x => p.Invoke(x).StartsWith("asd"); //Reuse p expression

c.Criteria = c.Criteria.Expand();

Invoke is an extension method provided by LinqKit that helps you to compose expressions easily.

After invoking the Expand method, c.Criteria would contain an expression that is exactly the same as if you have done this:

c.Criteria = x => x.Name.StartsWith("asd");


标签: c# expression