【问题标题】:Replace parameter to point to nested parameter in lambda expression替换参数以指向 lambda 表达式中的嵌套参数
【发布时间】:2016-11-24 18:04:19
【问题描述】:

感谢previous question 上的一些答案,我可以成功替换 lambda 表达式中的简单参数类型,但我不知道如何将传入 lambda 中的参数替换为嵌套参数。

考虑以下对象:

public class DtoColour {

    public DtoColour(string name)
    {
        Name = name;
    }

    public string Name { get; set; }

    public ICollection<DtoFavouriteColour> FavouriteColours { get; set; }
}

public class DtoPerson
{
    public DtoPerson(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
        FavouriteColours = new Collection<DtoFavouriteColour>();
    }

    public string FirstName { get; private set; }

    public string LastName { get; private set; }

    public ICollection<DtoFavouriteColour> FavouriteColours { get; set; }
}

public class DtoFavouriteColour
{
    public DtoColour Colour { get; set; }

    public DtoPerson Person { get; set; }
}

public class DomainColour {

    public DomainColour(string name)
    {
        Name = name;
    }

    public string Name { get; set; }

    public ICollection<DomainPerson> People { get; set; }
}

public class DomainPerson {

    public DomainPerson(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
        Colours = new Collection<DomainColour>();
    }

    public string FirstName { get; private set; }

    public string LastName { get; private set; }

    public ICollection<DomainColour> Colours { get; set; }
}

和一个存储库:

public class ColourRepository {

    private IList<DtoColour> Colours { get; set; } 

    public ColourRepository()
    {
        var favColours = new Collection<DtoFavouriteColour>
        {
            new DtoFavouriteColour() { Person = new DtoPerson("Peter", "Parker") },
            new DtoFavouriteColour() { Person = new DtoPerson("John", "Smith") },
            new DtoFavouriteColour() { Person = new DtoPerson("Joe", "Blogs") }
        };
        Colours = new List<DtoColour>
        {
            new DtoColour("Red") { FavouriteColours = favColours },
            new DtoColour("Blue"),
            new DtoColour("Yellow")
        };
    }

    public IEnumerable<DomainColour> GetWhere(Expression<Func<DomainColour, bool>> predicate)
    {
        var coonvertedPred = MyExpressionVisitor.Convert(predicate);
        return Colours.Where(coonvertedPred).Select(c => new DomainColour(c.Name)).ToList();
    }
}

最后是一个表达式访问者,它应该将谓词转换为 Dto 模型的正确谓词

public class MyExpressionVisitor : ExpressionVisitor
{
    private ReadOnlyCollection<ParameterExpression> _parameters;

    public static Func<DtoColour, bool> Convert<T>(Expression<T> root)
    {
        var visitor = new MyExpressionVisitor();
        var expression = (Expression<Func<DtoColour, bool>>)visitor.Visit(root);
        return expression.Compile();
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        var param = _parameters?.FirstOrDefault(p => p.Name == node.Name);

        if (param != null)
        {
            return param;
        }

        if(node.Type == typeof(DomainColour))
        {
            return Expression.Parameter(typeof(DtoColour), node.Name);
        }

        if (node.Type == typeof(DomainPerson))
        {
            return Expression.Parameter(typeof(DtoFavouriteColour), node.Name);
        }

        return node;
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        _parameters = VisitAndConvert<ParameterExpression>(node.Parameters, "VisitLambda");
        return Expression.Lambda(Visit(node.Body), _parameters);
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        var exp = Visit(node.Expression);

        if (node.Member.DeclaringType == typeof(DomainColour))
        {
            if (node.Type == typeof(ICollection<DomainPerson>))
            {
                return Expression.MakeMemberAccess(exp, typeof(DtoColour).GetProperty("FavouriteColours"));
            }

            return Expression.MakeMemberAccess(exp, typeof(DtoColour).GetProperty(node.Member.Name));
        }

        if (node.Member.DeclaringType == typeof(DomainPerson))
        {
            var nested = Expression.MakeMemberAccess(exp, typeof(DtoFavouriteColour).GetProperty("Person"));
            return Expression.MakeMemberAccess(nested, typeof(DtoPerson).GetProperty(node.Member.Name));
        }

        return base.VisitMember(node);
    }
}

目前我得到以下异常

[System.ArgumentException: 类型表达式 'System.Collections.Generic.ICollection1[ExpressionVisitorTests.DtoFavouriteColour]' cannot be used for parameter of type 'System.Collections.Generic.IEnumerable1[ExpressionVisitorTests.DomainPerson]' 方法的布尔值 Any[DomainPerson](System.Collections.Generic.IEnumerable1[ExpressionVisitorTests.DomainPerson], System.Func2[ExpressionVisitorTests.DomainPerson,System.Boolean])']

这里有一个dotnetfiddle,它不起作用。

提前感谢您的帮助。

【问题讨论】:

  • DtoColourDtoPerson 都有一个名为 FavoriteColours 的属性,这有点令人困惑。这些集合应该同步吗?无论如何,对象具有实际上不是对象属性的属性是没有意义的。喜欢特定颜色的人的集合不是该颜色的属性。一个人喜欢的颜色的集合可能是一个人的属性,但我也会删除它,并且只有一个所有“人喜欢颜色”关系的公共集合。
  • dto 对象代表数据库的模式,最喜欢的颜色 dto 是为域模型提供关系的链接表。我知道这是一个奇怪的例子,但不要太担心。

标签: c# lambda expression visitor-pattern


【解决方案1】:

经过更多搜索后,我通过John Skeet 找到了this answer,这让我想出了一个可行的解决方案,其中包括在ExpressionVisitor 上添加VisitMethodCall 方法的覆盖以替换原来的MethodInfo为正确的集合类型添加一个新的。

protected override Expression VisitMethodCall(MethodCallExpression node)
{
    if (node.Method.DeclaringType == typeof(Enumerable) && node.Arguments[0].Type == typeof(ICollection<DomainPerson>))
    {
        Expression obj = Visit(node.Object);
        IEnumerable<Expression> args = Visit(node.Arguments);
        if (obj != node.Object || args != node.Arguments)
        {
            var generic = typeof(Enumerable).GetMethods()
                            .Where(m => m.Name == node.Method.Name)
                            .Where(m => m.GetParameters().Length == node.Arguments.Count)
                            .Single();
            var constructed = generic.MakeGenericMethod(typeof(DtoFavouriteColour));
            return Expression.Call(obj, constructed, args);
        }
    }
    return node;
}

我还需要确保我对_parameters 集合的引用没有被对VisitLambda&lt;T&gt; 的嵌套调用所取代,这可能在访问node.Body 时发生。

protected override Expression VisitLambda<T>(Expression<T> node)
{
    var parameters = VisitAndConvert(node.Parameters, "VisitLambda");

    // ensure parameters set but dont let original reference 
    // be overidden by nested calls
    _parameters = parameters;

    return Expression.Lambda(Visit(node.Body), parameters);
}

请参阅dotnetfiddle 以获得完全有效的解决方案。

如果有人有更好/更优雅的解决方案,请添加答案让我标记。

【讨论】:

    【解决方案2】:

    您已经解决了具体问题,所以我不能说我要向您提出的建议是否更好/更优雅,但肯定更通用(删除了具体类型/属性/假设),因此可以重用于翻译来自不同模型类型的相似表达式。

    代码如下:

    public class ExpressionMap
    {
        private Dictionary<Type, Type> typeMap = new Dictionary<Type, Type>();
        private Dictionary<MemberInfo, Expression> memberMap = new Dictionary<MemberInfo, Expression>();
        public ExpressionMap Add<TFrom, TTo>()
        {
            typeMap.Add(typeof(TFrom), typeof(TTo));
            return this;
        }
        public ExpressionMap Add<TFrom, TFromMember, TTo, TToMember>(Expression<Func<TFrom, TFromMember>> from, Expression<Func<TTo, TToMember>> to)
        {
            memberMap.Add(((MemberExpression)from.Body).Member, to.Body);
            return this;
        }
        public Expression Map(Expression source) => new MapVisitor { map = this }.Visit(source);
    
        private class MapVisitor : ExpressionVisitor
        {
            public ExpressionMap map;
            private Dictionary<Type, ParameterExpression> parameterMap = new Dictionary<Type, ParameterExpression>();
            protected override Expression VisitLambda<T>(Expression<T> node)
            {
                return Expression.Lambda(Visit(node.Body), node.Parameters.Select(Map));
            }
            protected override Expression VisitParameter(ParameterExpression node) => Map(node);
            protected override Expression VisitMember(MemberExpression node)
            {
                var expression = Visit(node.Expression);
                if (expression == node.Expression)
                    return node;
                Expression mappedMember;
                if (map.memberMap.TryGetValue(node.Member, out mappedMember))
                    return Visit(mappedMember);
                return Expression.PropertyOrField(expression, node.Member.Name);
            }
            protected override Expression VisitMethodCall(MethodCallExpression node)
            {
                if (node.Object == null && node.Method.IsGenericMethod)
                {
                    // Static generic method
                    var arguments = Visit(node.Arguments);
                    var genericArguments = node.Method.GetGenericArguments().Select(Map).ToArray();
                    var method = node.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArguments);
                    return Expression.Call(method, arguments);
                }
                return base.VisitMethodCall(node);
            }
            private Type Map(Type type)
            {
                Type mappedType;
                return map.typeMap.TryGetValue(type, out mappedType) ? mappedType : type;
            }
            private ParameterExpression Map(ParameterExpression parameter)
            {
                var mappedType = Map(parameter.Type);
                ParameterExpression mappedParameter;
                if (!parameterMap.TryGetValue(mappedType, out mappedParameter))
                    parameterMap.Add(mappedType, mappedParameter = Expression.Parameter(mappedType, parameter.Name));
                return mappedParameter;
            }
        }
    }
    

    以及具体示例的用法:

    public IEnumerable<DomainColour> GetWhere(Expression<Func<DomainColour, bool>> predicate)
    {
        var map = new ExpressionMap()
            .Add<DomainColour, DtoColour>()
            .Add((DomainColour c) => c.People, (DtoColour c) => c.FavouriteColours.Select(fc => fc.Person))
            .Add<DomainPerson, DtoPerson>();
        var mappedPredicate = ((Expression<Func<DtoColour, bool>>)map.Map(predicate));
        return Colours.Where(mappedPredicate.Compile()).Select(c => new DomainColour(c.Name)).ToList();
    }
    

    如您所见,它允许您定义从一种类型到另一种类型的简单映射,并且可以选择从一种类型的成员到另一种类型的成员/表达式(只要它们兼容),使用“流利”语法拉姆达表达式。没有指定映射的成员按照原代码中的名称进行映射。

    一旦定义了映射,实际的处理当然是由自定义的ExpressionVisitor 完成的,类似于您的。不同之处在于它通过 type 映射和合并 ParameterExpressions,并且还翻译每个 静态泛型方法,因此应该也适用于 Queryable 和类似的。 p>

    【讨论】:

    • 感谢您的回答,它更加灵活/可重用,而且优雅,通过一些调整和一些构建器模式,它可以返回编译后的表达式。
    • 能否请您告诉我如何修改此代码以处理强制转换,例如此表达式 predicate = x =&gt; (x as SubEmployeePM).Division.Name == ""; 我想将 SubEmployeePM 类转换为 SubEmployee
    • @Sisyphus 将以下内容添加到 MapVisitor 类:protected override Expression VisitUnary(UnaryExpression node) { if (node.NodeType == ExpressionType.TypeAs) return Expression.TypeAs(Visit(node.Operand), Map(node.Type)); return base.VisitUnary(node); }
    • @IvanStoev 非常感谢朋友,这对我帮助很大。
    猜你喜欢
    • 2011-01-07
    • 2023-04-05
    • 1970-01-01
    • 2013-05-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多