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

2 個回答

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM