Source Generators实现简版AutoMapper
問題
在業務開發中,我們常常需要將一個對象映射成另一個對象。例如將領域實體(UserEntity)映射成暴露給服務外部使用的數據傳輸對象(UserDto)。
而AutoMapper則是目前主流的解決方案,實現類似如下代碼:
var?configuration?=?new?MapperConfiguration(cfg?=>? {cfg.CreateMap<UserEntity,?UserDto>(); });var?mapper?=?configuration.CreateMapper();var?userEntity?=?GetFromDB(); var?userDto?=?mapper.Map<UserDto>(userEntity);相對于使用AutoMapper,我更傾向于顯式映射,類似如下代碼:
public?UserDto?MapToUserDto(UserEntity?entity) {return?new?UserDto?{Id?=?entity.Id,Name?=?entity.Name}; }var?userEntity?=?GetFromDB(); var?userDto?=?MapToUserDto(userEntity);顯式映射有以下一些好處:
不依賴第三方框架,性能有保障
設計時支持,例如"查找所有引用"
運行時支持,例如"斷點調試"
但是缺點也很明顯,手工編寫顯式映射是一項耗時并且枯燥的工作。
雖然可以使用工具(例如代碼生成器)自動生成這些映射代碼,但是今天我們介紹一種更方便的方式。
Source Generators
上次我們已經介紹過Source Generators,它可以在編譯時創建并添加到編譯中的代碼,而無需像代碼生成器那樣顯式生成大量冗余代碼。
因此,我們這次嘗試用Source Generators來自動生成顯式映射代碼。
實現代碼如下:
[Generator] public?class?AutoMapperGenerator?:?ISourceGenerator {private?const?string?MappingAttributeText?=?@" using?System; namespace?AutoMapperGenerator { public?class?AutoMappingAttribute?:?Attribute {public?AutoMappingAttribute(Type?fromType,Type?toType){this.FromType?=?fromType;this.ToType?=?toType;}public?Type?FromType?{?get;?set;?}public?Type?ToType?{?get;?set;?} } }";public?void?Initialize(GeneratorInitializationContext?context){}public?void?Execute(GeneratorExecutionContext?context){context.AddSource("AutoMappingAttribute",?SourceText.From(MappingAttributeText,?Encoding.UTF8));var?options?=?(context.Compilation?as?CSharpCompilation).SyntaxTrees[0].Options?as?CSharpParseOptions;var?compilation?=?context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(MappingAttributeText,?Encoding.UTF8),?options));var?allNodes?=?compilation.SyntaxTrees.SelectMany(s?=>?s.GetRoot().DescendantNodes());var?allAttributes?=?allNodes.Where((d)?=>?d.IsKind(SyntaxKind.Attribute)).OfType<AttributeSyntax>();var?attributes?=?allAttributes.Where(d?=>?d.Name.ToString()?==?"AutoMapping").ToList();var?allClasses?=?compilation.SyntaxTrees.SelectMany(x?=>?x.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>());var?sourceBuilder?=?new?StringBuilder(@" //<auto-generated> namespace?AutoMapperGenerator { public?static?class?Mapper {");foreach?(AttributeSyntax?attr?in?attributes){var?fromTypeArgSyntax?=?attr.ArgumentList.Arguments.First();var?fromTypeArgSyntaxExpr?=?fromTypeArgSyntax.Expression.NormalizeWhitespace().ToFullString();var?toTypeArgSyntax?=?attr.ArgumentList.Arguments.ElementAt(1);var?toTypeArgSyntaxExpr?=?toTypeArgSyntax.Expression.NormalizeWhitespace().ToFullString();var?fromClassName?=?GetContentInParentheses(fromTypeArgSyntaxExpr);var?fromClassSyntax?=?allClasses.First(x?=>?x.Identifier.ToString()?==?fromClassName);var?fromClassModel?=?compilation.GetSemanticModel(fromClassSyntax.SyntaxTree);var?fromClassNamedTypeSymbol?=?ModelExtensions.GetDeclaredSymbol(fromClassModel,?fromClassSyntax);var?fromClassFullName?=?fromClassNamedTypeSymbol.OriginalDefinition.ToString();var?toClassName?=?GetContentInParentheses(toTypeArgSyntaxExpr);var?toClassSyntax?=?allClasses.First(x?=>?x.Identifier.ToString()?==?toClassName);var?toClassModel?=?compilation.GetSemanticModel(toClassSyntax.SyntaxTree);var?toClassNamedTypeSymbol?=?ModelExtensions.GetDeclaredSymbol(toClassModel,?toClassSyntax);var?toClassFullName?=?toClassNamedTypeSymbol.OriginalDefinition.ToString();???????????sourceBuilder.Append($@"public?static?{toClassFullName}?To{toClassName}(this?{fromClassFullName}?source){{var?target?=?new?{toClassFullName}();");var?propertySyntaxes?=?toClassSyntax.SyntaxTree.GetRoot().DescendantNodes().OfType<PropertyDeclarationSyntax>();foreach?(var?propertySyntaxe?in?propertySyntaxes){var?symbol?=?toClassModel.GetDeclaredSymbol(propertySyntaxe);var?propertyName?=?symbol.Name;sourceBuilder.Append($@"target.{propertyName}?=?source.{propertyName};");}sourceBuilder.Append(@"return?target;} ");???????????????}sourceBuilder.Append(@" } }");context.AddSource("Mapper",?SourceText.From(sourceBuilder.ToString(),?Encoding.UTF8));}private?string?GetContentInParentheses(string?value){var?match?=?Regex.Match(value,?@"\(([^)]*)\)");return?match.Groups[1].Value;} }我們定義了AutoMappingAttribute,可以在任意類上聲明此Attribute。
AutoMappingAttribute包含FromType和ToType參數,Source Generators為FromType生成ToXXX的擴展方法,遍歷ToType對應類的所有屬性并顯示映射。
使用示例
示例代碼如下:
[ApiController] [Route("[controller]")] [AutoMapping(typeof(UserEntity),?typeof(UserDto))] public?class?UserController?:?ControllerBase {?[HttpGet]public?UserDto?Get(int?id){var?userEntity?=?GetFromDB(id);var?userDto?=?userEntity.ToUserDto();return?userDto;} }在UserController上聲明了AutoMappingAttribute,編譯后可以看到,自動生成了ToUserDto方法:
運行后測試,工作正常,成功!
結論
當然,目前的功能與真正的AutoMapper還相差很遠。
但是,如果你也希望在代碼中使用顯式映射,本文將是一個很好的起點。
如果你覺得這篇文章對你有所啟發,請關注我的個人公眾號”My IO“,記住我!
總結
以上是生活随笔為你收集整理的Source Generators实现简版AutoMapper的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 祝贺|合肥.NET俱乐部第二期技术沙龙活
- 下一篇: .NET 6 中的 LINQ 更新