新文章 网摘 文章 随笔 日记

Lambda表达式转换

使用实体框架,我们有许多实体,我们希望从调用代码的其余部分隐藏它们,以消除对数据库的直接依赖。我们是通过使用 DTO 来实现的,DTO 在大多数情况下与实体相同。我们还有一个抽象的通用提供者和持久化模式,用于处理 dtos 和实体之间的转换,以及所有数据库交互。基本抽象提供者的一般加载方法使用以下签名:

protected virtual IEnumerable<TDto> Load(Expression<Func<TEntity, bool>> filter)

所以任何扩展抽象的提供者都可以是这样的:

public IEnumerable<FooDto> GetSomeFilteredData(IEnumerable<string> identifiers)
{
  return base.Load(entity => identifiers.Contains(entity.SomeProperty));
}

因此,从调用代码中隐藏实体。

这种模式对我们很有效,但我一直在研究如何操作表达式,以便我可以将以下内容放入基本提供程序中:

public virtual IEnumerable<TDto> Load(Expression<Func<TDto, bool>> filter)

然后,基本提供程序会将 转换Expression<Func<TDto, bool>>为等效的实体表达式,并像以前一样加载数据,因此无需编写繁琐的方法,并为我们提供更多的调用代码控制权。

最终结果如下 ExpressionConverter

public class ExpressionConverter<Dto, Entity>
  where Dto : class, new() where Entity : class, new()
{
  private MappedConverter<Dto, Entity> converter;

  public ExpressionConverter(MappedConverter<Dto, Entity> converter)
  {
    this.converter = converter;
  }

  public Expression<Func<Entity, bool>> ConvertExpr(Expression<Func<Dto, bool>> expr)
  {
    ParameterExpression entityParam = Expression.Parameter(typeof(Entity), "e");
    Expression entityExpression = this.ConvertExpression(expr.Body, entityParam);

    var result = Expression.Lambda<Func<Entity, bool>>(entityExpression, entityParam);
    return result;
  }

  private Expression ConvertExpression(Expression expression, ParameterExpression entityParam)
  {
    if (expression is BinaryExpression binary)
    {
      return this.ConvertBinaryExpression(binary, entityParam);
    }
    if (expression is MemberExpression member)
    {
      return this.ConvertMemberExpression(member, entityParam);
    }
    if (expression is MethodCallExpression method)
    {
      return this.ConvertMethodCallExpression(method, entityParam);
    }
    return expression;
  }

  private Expression ConvertBinaryExpression(BinaryExpression binary, ParameterExpression param)
  {
    Expression left = this.ConvertExpression(binary.Left, param);
    Expression right = this.ConvertExpression(binary.Right, param);
    ExpressionType nodeType = binary.NodeType;
      return this.CombineExpressions(left, right, nodeType);
  }

  private Expression ConvertMemberExpression(Expression expression, ParameterExpression entityParam)
  {
    if (this.IsParseableMemberExpression(expression))
    {
      MemberExpression memberExpr = expression as MemberExpression;
      var value = Expression.Lambda(memberExpr).Compile().DynamicInvoke();
      return Expression.Constant(value);
    }
    else if (this.IsConvertibleMember(expression))
    {
      MemberInfo dtoMember = (expression as MemberExpression).Member;
      Mapping<Dto, Entity> mapping = this.converter.GetMappingFromMemberName<Dto>(dtoMember.Name);
      MemberExpression entityMemberExpr = Expression.MakeMemberAccess(entityParam, mapping.EntityProperty);
      return entityMemberExpr;
    }
    return expression;
  }

  private Expression ConvertMethodCallExpression(MethodCallExpression expression, ParameterExpression entityParam)
  {
    Expression objectExpr = this.ConvertMemberExpression(expression.Object, entityParam);
    IEnumerable<Expression> argumentExpressions = expression.Arguments.Select(x => this.ConvertMemberExpression(x, entityParam));
    MethodInfo method = expression.Method;

    Expression result = Expression.Call(objectExpr, method, argumentExpressions);
    return result;
  }

  private bool IsConvertibleMember(Expression expression)
  {
    return expression.NodeType == ExpressionType.MemberAccess;
  }

  private bool IsParseableMemberExpression(Expression expression)
  {
    if (expression is MemberExpression memberExpression)
    {
      return Regex.IsMatch(expression.ToString(), @"value[(].*[)]")
        || this.IsDateTimeExpression(memberExpression);
    }
    return false;
  }

  private bool IsDateTimeExpression(MemberExpression expression)
  {
    return Regex.IsMatch(expression.ToString(), @"DateTime\.(Today|Now)");
  }

  private Expression CombineExpressions(Expression left, Expression right, ExpressionType type)
  {
    switch (type)
    {
      case ExpressionType.And:                return Expression.And(left, right);
      case ExpressionType.AndAlso:            return Expression.AndAlso(left, right);
      case ExpressionType.Equal:              return Expression.Equal(left, right);
      case ExpressionType.ExclusiveOr:        return Expression.ExclusiveOr(left, right);
      case ExpressionType.GreaterThan:        return Expression.GreaterThan(left, right);
      case ExpressionType.GreaterThanOrEqual: return Expression.GreaterThanOrEqual(left, right);
      case ExpressionType.LessThan:           return Expression.LessThan(left, right);
      case ExpressionType.LessThanOrEqual:    return Expression.LessThanOrEqual(left, right);
      case ExpressionType.NotEqual:           return Expression.NotEqual(left, right);
      case ExpressionType.Or:                 return Expression.Or(left, right);
      case ExpressionType.OrElse:             return Expression.OrElse(left, right);
      default:
        throw new Exception($"Unsupported expression type: {type.ToString()}");
    }
  }
}

MappedConverter<Dto, Entity>以下是哪里

public class MappedConverter<Dto, Entity> where Dto : class, new() where Entity : class, new()
{
  public List<Mapping<Dto, Entity>> Mappings { get; set; }

  public MappedConverter(params Mapping<Dto, Entity>[] maps)
  {
    this.Mappings = maps.ToList();
  }

  public Mapping<Dto, Entity> GetMappingFromMemberName<T>(string name)
  {
    if (typeof(T) == typeof(Dto))
    {
      return this.Mappings.SingleOrDefault(x => x.DtoProperty.Name == name);
    }
    else if (typeof(T) == typeof(Entity))
    {
      return this.Mappings.SingleOrDefault(x => x.EntityProperty.Name == name);
    }
    throw new Exception($"Cannot get mapping for {typeof(T).Name} from MappedConverter<{typeof(Dto).Name}, {typeof(Entity).Name}>");
  }
}

并且Mapping<Dto, Entity>是以下内容:

public class Mapping<Dto, Entity>
{
  public PropertyInfo DtoProperty { get; }
  public PropertyInfo EntityProperty { get; }
}

笔记:

转换只需要处理表单的表达式 Expression<Func<Dto, bool>>

我特别在寻找改进方法IsParseableMemberExpressionIsDateTimeExpression方法

IsParseableMemberExpression用于Regex查看表达式是否表示完整表达式中的变量。例如variablex => x.Value == variable

  • 1
    我同意必须参考TEntity会损害您在其他方面相当广泛的层分离。但是,仅使用的问题TDto是不能保证其相关项TEntity具有与该表达式可以使用的类似属性。我并不是说它在技术上无法完成,但是所需的努力值得付出吗?希望直接控制您强行抽象的实体属性是违反直觉的。您本质上要求过滤器的类型安全为零,这似乎可能成为未来问题的根源。 —— 扁平化 2018 年 5 月 2 日 11:26 
  •  
    @Flater 我理解对类型安全的担忧,但这是我们在创建映射转换器时在数据层管理的东西。根据我们的经验,由于我们必须添加大量新的 DTO 和实体,这些都是从 csv 文件中解析出来的简单对象,并且任何查询都相对简单,因此创建它的努力已经是值得的。对于更大或更复杂的物体,我们仍然使用旧模式来帮助安全 —— 克里斯汀 2018 年 5 月 2 日 12:43
  • 1
    现在您正在构建中间层,这是有道理的。但是假设您已经完成了,另一个开发人员将编写使用您的层的层。何时使用哪种模式对他们来说很明显?它们是否需要保持在您的层中尚未(未)实现的循环中?因为他们大概必须根据底层实体是否与 dto 相同的知识在模式之间做出决定。这意味着他们需要了解实体。这意味着它实际上并没有被你的层分开。 —— 扁平化 2018 年 5 月 2 日 12:51 
  • 1
    我的意思是,当您的分离不完整时(正如您的“对于复杂对象,我们使用旧模式”所证明的那样),那么您就需要消费开发人员来了解您的中间系统,同时仍然了解基本的基础知识。(对此有一个相当相关的 XKCD)。一半的分离实际上比完全不分离更糟糕,因为一半的分离需要分离部分和未分离部分的了解。 —— 扁平化 2018 年 5 月 2 日 12:54
  • 1
    虽然我完全理解您的想法,但有关我们系统整体架构的问题不属于此特定代码审查的范围。毫无疑问,它值得在不同的问题上单独讨论,但它与手头的代码审查无关。 —— 克里斯汀 2018 年 5 月 3 日 6:27
5
 

您可以使用ExpressionVisitor类给自己更多的灵活性,而不必自己处理所有条件。

public class ExpressionConverter<TFrom, TTo> : ExpressionVisitor
    where TFrom : class, new() 
    where TTo : class, new()
{
    private readonly MappedConverter<TFrom, TTo> _converter;
    private ParameterExpression _fromParameter;
    private ParameterExpression _toParameter;

    public ExpressionConverter(MappedConverter<TFrom, TTo> converter)
    {
        _converter = converter;
    }

    public override Expression Visit(Expression node)
    {
        if (_fromParameter == null)
        {
            if (node.NodeType != ExpressionType.Lambda)
            {
                throw new ArgumentException("Expression must be a lambda");
            }

            var lambda = (LambdaExpression)node;
            if (lambda.ReturnType != typeof(bool) || lambda.Parameters.Count != 1 ||
                lambda.Parameters[0].Type != typeof(TFrom))
            {
                throw new ArgumentException("Expression must be a Func<TFrom, bool>");
            }

            _fromParameter = lambda.Parameters[0];
            _toParameter = Expression.Parameter(typeof(TTo), _fromParameter.Name);
        }
        return base.Visit(node);
    }

我们将让 ExpressionConverter 从 ExpressionVisitor 继承,并且第一次调用 Visit 确保它是一个 Func lambda(在 ExpressionTree 中通过递归调用了很多)。如果是这样,我们保存我们要转换的参数表达式,并为我们要转换的参数创建一个新的表达式。

然后覆盖 VisitParameter 并换出参数

protected override Expression VisitParameter(ParameterExpression node)
{
    if (_fromParameter == node)
    {
        return  _toParameter;
    }
    return base.VisitParameter(node);
}

然后我们还需要覆盖 VisitMembers 来交换成员分配

protected override Expression VisitMember(MemberExpression node)
{
    if (node.Expression == _fromParameter)
    {
        var member = _converter.GetMappingFromMemberName<TFrom>(node.Member.Name);
        return Expression.Property(_toParameter, member);
    }

    return base.VisitMember(node);
}

我确实必须更改 GetMappingFromMemberName 以返回正确的 PropertyInfo 而不是映射以使其更容易工作。

public PropertyInfo GetMappingFromMemberName<T>(string name)
{
    if (typeof(T) == typeof(Dto))
    {
        return this.Mappings.SingleOrDefault(x => x.DtoProperty.Name == name).EntityProperty;
    }
    else if (typeof(T) == typeof(Entity))
    {
        return this.Mappings.SingleOrDefault(x => x.EntityProperty.Name == name).DtoProperty;
    }
    throw new Exception($"Cannot get mapping for {typeof(T).Name} from MappedConverter<{typeof(Dto).Name}, {typeof(Entity).Name}>");
}

我们需要做的最后一件事是创建一个新的 Expression<Func<TFrom, bool>>

protected override Expression VisitLambda<T>(Expression<T> node)
{
    if (typeof(T) == typeof(Func<TFrom, bool>))
    {
        return Expression.Lambda<Func<TTo, bool>>(Visit(node.Body), new[] { _toParameter }); 
    }
    return base.VisitLambda(node);
}

你可以这样称呼它

Expression<Func<Model, bool>> lambda = x => x.LastName == x.FirstName  && x.FirstName == "John";
var newLambda = expressionConverter.Visit(lambda);

如果您不喜欢 Visit 调用,您可以随时将其设为另一个类中的私有类,以赋予它不同的方法签名。但是现在您不必创建常量或运算符。只需更新正在使用的参数和属性。我没有做 CallMethod 但如果你需要,你可以override Expression VisitMethodCall(MethodCallExpression node)使用与上面相同的技术。

  •  
    不错,没想到用ExpressionVisitor。我遇到的唯一问题是使用转换器的相同实例转换多个表达式,因为_fromParameter_toParameter字段引用了先前表达式中的 Func 变量,因此InvalidOperationException在换出参数表达式时会得到这可以通过将Visit方法包装在ConvertExpression像上面这样帮助器中并在调用时重置字段来解决 克里斯汀 2019-06-06 13:21
  • 1
    这确实意味着它不是线程安全的,而是通过将访问逻辑提取到一个单独的子类中,您每次转换表达式时都会实例化该子类,您会得到它,并且代码仍然保持良好和干净 – 克里斯汀 2019-06-06 13:23
  •  
    正确,您每次需要使用它时都需要更新一个类,因为它具有需要保留的属性。 —— 查尔斯·恩里斯 2019-06-06 13:26
  •  
    @JChristen 刚刚意识到你一年多前问了这个问题,当时我得到了答案的徽章。 —— 查尔斯·恩里斯 2019-06-06 22:07
  •  
    @CharlesNRice 在将Mapping<From, To>类传递给MappedConverter构造函数如何实例化 —— 坦维尔·阿杰尔 19 年 12 月 28 日 16:22
 
3

我可以先说 - 这很好 - 无论是帖子还是提供的答案。

我的工具遵循类似的模式,通过我的控制器呈现 DTO 并抽象出我的数据访问。

我一直在寻找这样的东西一段时间了,这对我有帮助。

使用 @CharlesNRice 提供的 ExpressionVisitor - 我对 MappedConverter 的实现添加了另一个小改动。

使用 Automapper 似乎

public class Mapping<Dto, Entity>
{
  public PropertyInfo DtoProperty { get; }
  public PropertyInfo EntityProperty { get; }
}

可以通过使用 Automapper 并引用已注册的映射配置来替换。

我将 Mapped Converter 更改为 IPropertyMappingProvider

    public interface IPropertyMappingProvider
    {
        IPropertyMapping GetPropertyMapping<TSource, TDestination>(string sourcePropertyName);
    }

使用 IPropertyMapping 与您实现的 Mapping 完全相同。

    public interface IPropertyMapping
    {
        PropertyInfo SourceProperty { get; }
        PropertyInfo DestinationProperty { get; }
    }

PropertyMappingProvider 的实现只是注入了向应用程序注册的 IMapper 实例。然后我们可以使用 IMapper 来获取属性映射配置。

 public class PropertyMappingProvider : IPropertyMappingProvider
{
    private readonly IMapper _mapper;

    public PropertyMappingProvider(IMapper mapper)
    {
        _mapper = mapper;
    }

    /// <summary>
    /// Returns a <see cref="IPropertyMapping"/> mapped properties from the IMapper configuration based on the source property name
    /// </summary>
    /// <typeparam name="TSource">Source mapping class</typeparam>
    /// <typeparam name="TDestination">Destination mapping class</typeparam>
    /// <param name="sourcePropertyName">The property name on the source class</param>
    /// <returns><see cref="IPropertyMapping"/> Contains the <see cref="PropertyInfo"/> classes for <typeparam name="TSource"></typeparam> property and <typeparam name="TDestination"></typeparam></returns>
    public virtual IPropertyMapping GetPropertyMapping<TSource, TDestination>(string sourcePropertyName)
    {
        var configurationProvider = _mapper.ConfigurationProvider;

        var mapping = configurationProvider.FindTypeMapFor<TSource, TDestination>();

        var propertyMap = mapping.PropertyMaps.FirstOrDefault(pm => pm.SourceMember.Name == sourcePropertyName);

        if(propertyMap == null)
            throw new ArgumentException($"No mappings found for {sourcePropertyName}");

        if(propertyMap.SourceMember.MemberType != MemberTypes.Property)
            throw new ArgumentException($"{sourcePropertyName} is not a property {nameof(TSource)}");

        if(propertyMap.DestinationMember.MemberType != MemberTypes.Property)
            throw new ArgumentException($"{propertyMap.DestinationMember.Name} is not a property of {nameof(TDestination)}");

        var sourcePropertyInfo = (PropertyInfo) propertyMap.SourceMember;

        var destinationPropertyInfo = (PropertyInfo) propertyMap.DestinationMember;

        return new PropertyMapping(sourcePropertyInfo, destinationPropertyInfo);
    }
}

我们可以访问映射属性上的 PropertyInfo 对象。

使用它就像您在 ExpressionVisitor 实现中所做的一样:

 public sealed class ExpressionConverter<TFrom, TTo> : ExpressionVisitor, IExpressionConverter<TFrom, TTo>
{
    private readonly IPropertyMappingProvider _propertyMappingProvider;
    private ParameterExpression _fromParameter;
    private ParameterExpression _toParameter;

    public ExpressionConverter(IPropertyMappingProvider propertyMappingProvider)
    {
        _propertyMappingProvider = propertyMappingProvider;
    }

    public Expression<Func<TTo, bool>> Convert(Expression<Func<TFrom, bool>> expression)
    {
        var expr =  Visit(expression);

        return expr.Convert<TTo>();
    }

...

 protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Expression != _fromParameter) return base.VisitMember(node);

        var member = _propertyMappingProvider.GetPropertyMapping<TFrom, TTo>(node.Member.Name);

        return Expression.Property(_toParameter, member.DestinationProperty);
    }
posted @ 2021-07-12 13:30  岭南春  阅读(912)  评论(0)    收藏  举报