Как динамически создать лямбда-выражение

предположим, у меня есть следующий класс:

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

основываясь на этой информации, моя цель-создать такое лямбда-выражение:

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

lang и name локальные переменные я хотел бы добавить в качестве постоянных значений при создании выражения.

как вы можете видеть, скомпилированная функция будет иметь тип 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 ответов


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

вы можете использовать интерфейсы для создания 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, который определен в каждом реализаторе.


предполагая, что у вас есть атрибуты для каждого из свойств каждого из объектов, с которыми вы заинтересованы в работе здесь (см. комментарии), и у вас есть параметр type (назовите его T) подписи для CreateExpression - это:

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

таким образом CreateExpression работает с известным универсальным типом, и вызывающий объект может указать, какие тесты необходимо выполнить для свойств сущности. Здесь мы предполагаем key имя свойства, котор нужно отразить дальше (обеспечьте он имеет известный атрибут) в то время как value - требуемое значение указанного имущества. IOrderedDictionary это способ обеспечить короткое замыкание && тесты, Если это ваше дело.

вы можете добавить дополнительные ограничения на T (например,T должен реализовать некоторый интерфейс) через ограничения универсального типа с использованием where

см. также проверьте, имеет ли свойство атрибут

и отражение-получить имя атрибута и значение по свойству


это сделает это, и должно сделать его довольно удобным для пользователя, чтобы построить

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 => language] и т. д.

оценить это:

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

или вы можете передать его в EF, например:

dtx.Genre.Where(query);