如何动态创建lambda表达式

时间:2015-07-08 01:36:53

标签: c# .net lambda

假设我有以下课程:

public class Show
{
    public string Language { get; set; }
    public string Name { get; set; }
}

根据这些信息,我的目标是创建一个像这样的lambda表达式:

g => g.Language == lang && g.Name == name

langname是我想在创建表达式时添加为常量值的局部变量。

如您所见,编译函数的类型为Func<Genre, bool>

为了帮助您更清楚地理解,我想实现类似的目标:

string lang = "en";
string name = "comedy";
Genre genre = new Genre { Language = "en", Name = "comedy" };
Expression<Func<Genre, bool>> expression = CreateExpression(genre, lang, name);
// expression = (g => g.Language == "en" && g.Name == "comedy")

我知道表达树的存在,但我对这个主题很新,所以我甚至不知道如何开始。

这个问题可以解决吗?如何动态创建这样的表达式?

4 个答案:

答案 0 :(得分:1)

public Expression<Func<TValue, bool>> CreateExpression<TValue, TCompare>(TValue value, TCompare compare)
{
    var pv = Expression.Parameter(typeof(TValue), "data");
    var compareProps = typeof(TCompare).GetProperties();

    // First statement of the expression
    Expression exp = Expression.Constant(true);

    foreach (var prop in typeof(TValue).GetProperties())
    {
        // Check if the compare type has the same property
        if (!compareProps.Any(i => i.Name == prop.Name))
            continue;

        // Build the expression: value.PropertyA == "A" 
        // which "A" come from compare.PropertyA
        var eq = Expression.Equal(
            Expression.Property(pv, prop.Name), 
            Expression.Constant(compareProps
                .Single(i => i.Name == prop.Name)
                .GetValue(compare)));

        // Append with the first (previous) statement
        exp = Expression.AndAlso(exp, eq);
    }

    return Expression.Lambda<Func<TValue, bool>>(exp, pv);
}

用法:

var value = new { Lang = "en", Name = "comedy"};

// The compareValue should have the same property name as the value, 
// or the expression will just ignore the property
var compareValue = new { Lang = "en", Name = "comedy", Other = "xyz" };

// The create expression content is
// {data => ((True AndAlso (data.Lang == "en")) AndAlso (data.Name == "comedy"))}
bool isMatch = CreateExpression(value, compareValue).Compile()(value); // true

答案 1 :(得分:0)

您可以使用接口来为已定义必要属性的接口创建Func。

示例:

interface IExpressionable {
    string Language { get;set; }
    string Name { get;set; }
}
class Genre : IExpressionable {
    string Language {get;set;}
    string Name {get;set;}
}
Genre genre = new Genre();
Expression<Func<IExpressionable, bool>> expression = CreateExpression(genre, lang, name);
expression = (g => g.Language == "en" && g.Name == "comedy")

可以通过在接口上提供GetLanguage和GetName方法来实现此概念的替代实现,这将强制订阅者实现基础方法,以便根据自己的内部方法返回“语言”和“名称”

interface IExpressionable {
    string GetExpressionOne();
    string GetExpressionTwo();
}
class Genre : IExpressionable {
    string Language {get;set;}
    string Name {get;set;}
    public string GetExpressionOne() {
        return Language;
    }
    public string GetExpressionOne() {
        return Name;
    }
}
class SomethingElse {

    string Orange {get;set;}
    string BananaPeel {get;set;}
    public string GetExpressionOne() {
        return Orange;
    }
    public string GetExpressionOne() {
        return BananaPeel;
    }
}
Genre genre = new Genre();
SomethingElse else = new SomethingElse();
Expression<Func<IExpressionable, bool>> expression = CreateExpression(genre, lang, name);
Expression<Func<IExpressionable, bool>> expression2 = CreateExpression(else, lang, name);

expression = (g => g.GetExpressionOne() == "en" && g.GetExpressionTwo() == "comedy");

根据上面的评论进行修改:@kaveman I only have the key values, but I can fetch the key properties via reflection using some custom attributes that I defined. In this example, Language and Name would be decorated with an attribute that defines them as key properties

我的回答是完全避免这种想法。创建一个接口,定义执行所需表达式所需的方法,并让实体从中继承。您可以让接口上的方法为“GetKeyOne”和“GetKeyTwo”。然后你的表达式不必是动态的,你只需要定义表达式在与每个实现者中定义的KeyOne和KeyTwo交互时的作用。

答案 2 :(得分:0)

假设您有兴趣在此处理的每个实体的每个属性都有属性(请参阅comments),并且您有类型参数(称之为T)a CreateExpression的可能签名是:

Expression<Func<T, bool>> CreateExpression(T entity, IOrderedDictionary<string, string> propertyTests);

通过这种方式,您的CreateExpression可以使用已知的泛型类型,并且调用者可以指定针对实体属性进行的测试。这里我们假设key是要反映的属性名称(确保它具有已知属性),而value是所述属性的必需值。 IOrderedDictionary是一种强制&&测试短路的方法,如果这是你的事情。

您可以T添加其他约束(例如T必须实现某些界面)generic type constraints using where

另见Check if property has attribute

Reflection - get attribute name and value on property

答案 3 :(得分:0)

这样做,并且应该使它对用户友好构建

private Expression<Func<Genre,bool>> CreateExpression(params Expression<Func<Object>>[] selectors)
{
    //We are working on a Genre type, and make a parameter of it
    //Query so far looks like g =>
    var param = Expression.Parameter(typeof(Genre),"g");

    //Set the base expression to make it easy to build
    //Query so far looks like g => true
    Expression expression = Expression.Constant(true);

    foreach(var selector in selectors) {
        //Find out the name of the variable was passed
        var selectorname = TestExtension.nameof(selector);
        //Get the value
        var selectorValue = selector.Compile()();

        //Create an accessor to the genre (g.Language for example)
        var accessMethod = Expression.PropertyOrField(param, selectorname);

        //Check if it equals the value (g.Language == "en")
        var equalExpr = Expression.Equal(accessMethod, Expression.Constant(selectorValue));

        //Make it an And expression
        //Query so far looks like g => true && g.Language == "en"
        expression = Expression.AndAlso(expression, equalExpr);

        //Second pass through the loop would build:
        //g => true && g.Language == "en" && g.Name == "comedy"
    }

    //Turn it into a lambda func and cast it
    var result = Expression.Lambda(expression, param) as Expression<Func<Genre, bool>>;
    return result;
}

public class Genre
{
    public string Language { get; set; }
    public string Name { get; set; }
}

//Taken from my answer at http://stackoverflow.com/a/31262225/563532

public static class TestExtension
{
    public static String nameof<T>(Expression<Func<T>> accessor)
    {
        return nameof(accessor.Body);
    }

    public static String nameof<T, TT>(this T obj, Expression<Func<T, TT>> propertyAccessor)
    {
        return nameof(propertyAccessor.Body);
    }

    private static String nameof(Expression expression)
    {
        if (expression.NodeType == ExpressionType.MemberAccess)
        {
            var memberExpression = expression as MemberExpression;
            if (memberExpression == null)
                return null;
            return memberExpression.Member.Name;
        }
        return null;
    }
}

然后你就这样使用它:

string language = "en";
string name = "comedy";
var genre = new Genre { Language = "en", Name="comedy" };

var query = CreateExpression(() => language, () => name);

请注意,变量必须与属性名称匹配。否则,您将需要某种映射[lang =&gt;语言]等。

评估它:

var matches = query.Compile()(genre);

或者,您可以将其传递给EF,例如:

dtx.Genre.Where(query);