如何动态创建 lambda 表达式

8

假设我有以下这个类:

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")

我知道表达式树的存在,但我对这个主题还很陌生,所以我甚至不知道该如何入手。

这个问题能够解决吗?我该如何动态创建这样的表达式?


我猜实际上问题比你描述的更复杂,对吗?如果您提前知道要测试的属性,难道不能编写一个返回函数的函数吗? - kaveman
@kaveman 实际上是这样的,你说得对。但这对我来说太复杂了,可能会把你和我都搞糊涂,我有一个存储库接口,可以使用表达式查询我的实体。问题是该表达式必须采用上述格式:由“&&”分隔的键条件。 - Matias Cicero
@kaveman 我正在接收类型(在我的示例中为Genre)作为通用参数,以及关键值,因此我需要通过反射获取我的关键属性(我认为我知道如何做到这一点),并创建表达式,以便我可以查询Genre实体。 - Matias Cicero
当你说“接收”时,你的意思是什么?你在哪里接收?接收的方法签名是什么?你是否同时拥有键和值,或者只是键值作为函数参数? - kaveman
@kaveman 我只有键值,但是我可以使用一些自定义属性通过反射获取键属性。在这个例子中,“Language”和“Name”将被装饰为一个属性,该属性将它们定义为键属性。 - Matias Cicero
4个回答

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

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 我只有关键值,但是我可以通过使用一些自定义属性来反射获取关键属性。在这个例子中,Language和Name将被装饰为一个定义它们为关键属性的属性。

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


这是一个不错的解决方案,但是LanguageName属性的存在取决于当前实体的类型,因此我需要为每个不同的实体类型创建不同的接口(它们可能具有完全不同的属性)。 - Matias Cicero
这只是一个示例,你可以随意命名属性,甚至可以叫“ExpressionArgument1”和“ExpressionArgument2”。请查看编辑。 - C Bauer
我可以看一下 CreateExpression 的主体吗? - Matias Cicero
我的建议是不要动态创建表达式,而是定义一个接口,强制实现者维护一些属性,以便您可以静态地处理它们。 - C Bauer

0

假设您在处理此处感兴趣的每个实体的每个属性上都有属性(请参见comments),并且您具有类型参数(称其为T),则CreateExpression的可能签名如下:

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

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

您可以通过使用where的通用类型约束添加对T的其他约束(例如,T必须实现某个接口)

另请参见检查属性是否具有属性

Reflection-获取属性上的属性名称和值


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 https://dev59.com/010Z5IYBdhLWcg3wnBVS#31262225

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 => language]等。
要评估它:
var matches = query.Compile()(genre);

或者,你可以将它传递给 EF 作为一个例子:
dtx.Genre.Where(query);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接