Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
219 views
in Technique[技术] by (71.8m points)

c# - Dynamic expression tree to filter on nested collection properties

I'm using Entity Framework and building queries using navigation properties dynamically.

For most of my use cases, the following works fine:

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;
    }

I use a custom AclAttribute class assigned to properties via metadata type and partial classes. For navigation properties, a ChildProperty is provided so that the expression builder knows to look deeper for the desired property.

For example: the table Services references another table called ServiceLocations. I need the LocationId value(s) from the ServiceLocations reference. This part works fine.

My problem is when there is more than 1 nested property to go through. Another example: the table ServiceCategories references Services which, again, references ServiceLocations. Like before, I need the LocationId value(s) from ServiceLocations.

I've done this manually by using two "Any" methods, combining, and returning the resulting expression, but there will be times where navigation properties won't be collections, or a child of a navigation property may be a collection. For those cases, I need some kind of recursive option. I've been trying for a while now, and coming up short.

Since I mentioned it, here's the test method I put together for my second example. This works, but I'm violating DRY. (Note: I didn't name the tables):

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;
    }
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

With this it should be possible to build your queries:

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;
    }
}

Call it like:

var parameter = Expression.Parameter(typeof(A), "A");
var expression = ExpressionBuilder.GetNavigationPropertyExpression(parameter, 8,"CollectionOfB", "CollectionOfC", "ID");

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...