Динамическое дерево выражений для фильтрации по вложенным свойствам коллекции

Я использую Entity Framework и строю запросы, используя свойства навигации динамически.

Для большинства моих сценариев отлично работает следующее:

private static MethodCallExpression GetNavigationPropertyExpression<T>(string propertyName, int test,
        ParameterExpression parameter, string subParameter)
    {
        var navigationPropertyCollection = Expression.Property(parameter, propertyName);

        var childType = navigationPropertyCollection.Type.GetGenericArguments()[0];

        var anyMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" && m.GetParameters().Length == 2).MakeGenericMethod(childType);

        var aclAttribute = GetAclAttribute(typeof(T), propertyName);
        var childProperty = aclAttribute.ChildProperty;

        var propertyCollectionGenericArg = childType;
        var serviceLocationsParam = Expression.Parameter(propertyCollectionGenericArg, subParameter);

        var left = Expression.Property(serviceLocationsParam, childProperty);
        var right = Expression.Constant(test, typeof(int));

        var isEqual = Expression.Equal(left, right);
        var subLambda = Expression.Lambda(isEqual, serviceLocationsParam);

        var resultExpression = Expression.Call(anyMethod, navigationPropertyCollection, subLambda);

        return resultExpression;
    }

Я использую пользовательский класс AclAttribute, назначенный свойствам через тип метаданных и частичные классы. Для свойств навигации предоставляется ChildProperty, так что построитель выражений знает, как искать нужное свойство глубже.

Например: таблица Services ссылается на другую таблицу с именем ServiceLocations. Мне нужны значения LocationId из ссылки ServiceLocations. Эта часть отлично работает.

Моя проблема заключается в том, что существует более 1 вложенного свойства. Другой пример: таблица ServiceCategories ссылается на Services, которая, опять же, ссылается на ServiceLocations. Как и прежде, мне нужны значения LocationId из ServiceLocations.

Я сделал это вручную, используя два метода "Любой", комбинируя и возвращая полученное выражение, но будут времена, когда свойства навигации не будут коллекциями, или дочерний элемент свойства навигации может быть коллекцией. Для этих случаев мне нужна какая-то рекурсивная опция. Я уже некоторое время пытаюсь и не могу.

Поскольку я упомянул об этом, вот метод испытания, который я собрал для моего второго примера. Это работает, но я нарушаю СУХОЙ. (Примечание: я не назвал таблицы):

private static MethodCallExpression GetNestedNavigationPropertyExpression(int test, ParameterExpression rootParameter)
    {
        var servicesProperty = Expression.Property(rootParameter, "tblServices");
        var servicesParameter = Expression.Parameter(servicesProperty.Type.GetGenericArguments()[0], "ss");

        var serviceLocationsProperty = Expression.Property(servicesParameter, "tblServiceLocations");
        var serviceLocationsParameter = Expression.Parameter(serviceLocationsProperty.Type.GetGenericArguments()[0], "s");

        var servicesAnyMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" && m.GetParameters().Length == 2).MakeGenericMethod(servicesProperty.Type.GetGenericArguments()[0]);
        var serviceLocationsAnyMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" && m.GetParameters().Length == 2).MakeGenericMethod(serviceLocationsProperty.Type.GetGenericArguments()[0]);

        var aclAttribute = GetAclAttribute(typeof(tblService), "tblServiceLocations");

        var left = Expression.Property(serviceLocationsParameter, aclAttribute.ChildProperty);
        var right = Expression.Constant(test, typeof(int));

        var isEqual = Expression.Equal(left, right);
        var subLambda = Expression.Lambda(isEqual, serviceLocationsParameter);

        var endExpression = Expression.Call(serviceLocationsAnyMethod, serviceLocationsProperty, subLambda);
        var intermediaryLamba = Expression.Lambda(endExpression, servicesParameter);
        var resultExpression = Expression.Call(servicesAnyMethod, servicesProperty, intermediaryLamba);

        return resultExpression;
    }

1 ответ

Решение

С этим должно быть возможно построить ваши запросы:

public static Expression GetNavigationPropertyExpression(Expression parameter, int test, params string[] properties)
{
    Expression resultExpression = null;
    Expression childParameter, navigationPropertyPredicate;
    Type childType = null;

    if (properties.Count() > 1)
    {
        //build path
        parameter = Expression.Property(parameter, properties[0]);
        var isCollection = typeof(IEnumerable).IsAssignableFrom(parameter.Type);
        //if it´s a collection we later need to use the predicate in the methodexpressioncall
        if (isCollection)
        {
            childType = parameter.Type.GetGenericArguments()[0];
            childParameter = Expression.Parameter(childType, childType.Name);
        }
        else
        {
            childParameter = parameter;
        }
        //skip current property and get navigation property expression recursivly
        var innerProperties = properties.Skip(1).ToArray();
        navigationPropertyPredicate = GetNavigationPropertyExpression(childParameter, test, innerProperties);
        if (isCollection)
        {
            //build methodexpressioncall
            var anyMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" && m.GetParameters().Length == 2);
            anyMethod = anyMethod.MakeGenericMethod(childType);
            navigationPropertyPredicate = Expression.Call(anyMethod, parameter, navigationPropertyPredicate);
            resultExpression = MakeLambda(parameter, navigationPropertyPredicate);
        }
        else
        {
            resultExpression = navigationPropertyPredicate;
        }
    }
    else
    {
        //Formerly from ACLAttribute
        var childProperty = parameter.Type.GetProperty(properties[0]);
        var left = Expression.Property(parameter, childProperty);
        var right = Expression.Constant(test, typeof(int));
        navigationPropertyPredicate = Expression.Equal(left, right);
        resultExpression = MakeLambda(parameter, navigationPropertyPredicate);
    }
    return resultExpression;
} 

private static Expression MakeLambda(Expression parameter, Expression predicate)
{
    var resultParameterVisitor = new ParameterVisitor();
    resultParameterVisitor.Visit(parameter);
    var resultParameter = resultParameterVisitor.Parameter;
    return Expression.Lambda(predicate, (ParameterExpression)resultParameter);
}

private class ParameterVisitor : ExpressionVisitor
{
    public Expression Parameter
    {
        get;
        private set;
    }
    protected override Expression VisitParameter(ParameterExpression node)
    {
        Parameter = node;
        return node;
    }
}

Назовите это как:

var parameter = Expression.Parameter(typeof(A), "A");
var expression = ExpressionBuilder.GetNavigationPropertyExpression(parameter, 8,"CollectionOfB", "CollectionOfC", "ID");
Другие вопросы по тегам