使用实体框架,我们有许多实体,我们希望从调用代码的其余部分隐藏它们,以消除对数据库的直接依赖。我们是通过使用 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>>
我特别在寻找改进方法IsParseableMemberExpression
和IsDateTimeExpression
方法
IsParseableMemberExpression
用于Regex
查看表达式是否表示完整表达式中的变量。例如variable
在x => x.Value == variable
您可以使用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
我可以先说 - 这很好 - 无论是帖子还是提供的答案。
我的工具遵循类似的模式,通过我的控制器呈现 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); }
-
你的
PropertyMapping
课在哪里? —— 坦维尔·阿杰尔 19 年 12 月 28 日 16:20 -
您的
IExpressionConverter<TFrom, TTo>
接口及其实现在哪里? —— 坦维尔·阿杰尔 19 年 12 月 28 日 16:57 -
@TanvirArjel - PropertyMapping 只是原始 post Mapping 类的接口提取。IExpressionConverter 在 ExpressionConverter 具体类中实现。该接口只是公共 Convert 方法。 —— JDBennett 19 年 12 月 28 日 22:27
TEntity
会损害您在其他方面相当广泛的层分离。但是,仅使用的问题TDto
是不能保证其相关项TEntity
具有与该表达式可以使用的类似属性。我并不是说它在技术上无法完成,但是所需的努力值得付出吗?希望直接控制您强行抽象的实体属性是违反直觉的。您本质上要求过滤器的类型安全为零,这似乎可能成为未来问题的根源。 —— 扁平化 2018 年 5 月 2 日 11:26