我们使用Roslyn和代码生成来实现AutoMapper

在上一篇文章中,我描述了一种使用Roslyn组织代码生成的方法。 当时的任务是演示一种通用方法。 现在,我想实现一些可以真正应用的东西。


因此,任何有兴趣研究如何使像AutoMapper这样的库的人都可以要求cat。


引言


首先,我认为有必要描述我的《时间提前者》(AOTMapper)的工作方式。 我们的映射器的入口点将是通用扩展方法MapTo<> 。 分析器将搜索它并提供实现MapToUser扩展MapToUser ,其中User是传递给MapTo<>的类型。


例如,采用以下类:


 namespace AOTMapper.Benchmark.Data { public class UserEntity { public UserEntity() { } public UserEntity(Guid id, string firstName, string lastName) { this.Id = id; this.FirstName = firstName; this.LastName = lastName; } public Guid Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } public class User { public string FirstName { get; set; } public string LastName { get; set; } public string Name { get; set; } } } 

生成的MapToUser将如下所示:


 public static AOTMapper.Benchmark.Data.User MapToUser(this AOTMapper.Benchmark.Data.UserEntity input) { var output = new AOTMapper.Benchmark.Data.User(); output.FirstName = input.FirstName; output.LastName = input.LastName; output.Name = ; // missing property return output; } 

从该示例中可以看到,具有相同名称和类型的所有属性都是自动分配的。 反过来,没有找到匹配项的对象继续“挂起”,从而产生编译错误,开发人员必须以某种方式处理它们。


例如,像这样:


 public static AOTMapper.Benchmark.Data.User MapToUser(this AOTMapper.Benchmark.Data.UserEntity input) { var output = new AOTMapper.Benchmark.Data.User(); output.FirstName = input.FirstName; output.LastName = input.LastName; output.Name = $"{input.FirstName} {input.LastName}"; return output; } 

MapToUser生成期间, MapTo<User>调用MapTo<User>将被MapToUser替换。


它的运动原理如下:



AOTMapper也可以通过nuget安装:


 Install-Package AOTMapper 

完整的项目代码可以在这里找到。


实作


我思考了很长时间后,可以得出不同的结论,但这并没有那么糟糕,因为这解决了使用AutoMapper时给我带来的不便。


首先,对于不同的类型,我们获得了不同的扩展方法,因此,对于某些抽象的User类型,我们可以非常轻松地使用IntelliSense来查找已经实现了哪些映射,而不必查找用于注册映射的文件。 只要看看您已有的扩展方法即可。


其次,在运行时它只是扩展方法,因此我们避免了与调用映射器相关的任何开销。 我知道AutoMapper开发人员在优化调用上花费了很多精力,但是仍然有一些额外的费用。 我的小型基准测试表明,平均每次调用为140-150ns(不包括初始化时间)。 可以在存储库中查看基准本身,并且测量结果较低。


方法均值失误标准差Gen 0第一代第二代已分配
AutoMapperToUserEntity151.84 ns1.9952纳秒1.8663 ns0.0253----80 B
AOTMapperToUserEntity10.41 ns0.2009 ns0.1879纳秒0.0152----48 B
AutoMapperToUser197.51 ns2.9225 ns2.5907 ns0.0787----248 B
AOTMapperToUser46.46 ns0.3530 ns0.3129 ns0.0686----216 B

另外,此映射器的优点包括以下事实:启动应用程序时通常不需要时间来初始化,这在大型应用程序中可能很有用。


分析器本身具有以下形式(缺少绑定代码):


 private void Handle(OperationAnalysisContext context) { var syntax = context.Operation.Syntax; if (syntax is InvocationExpressionSyntax invocationSytax && invocationSytax.Expression is MemberAccessExpressionSyntax memberAccessSyntax && syntax.DescendantNodes().OfType<GenericNameSyntax>().FirstOrDefault() is GenericNameSyntax genericNameSyntax && genericNameSyntax.Identifier.ValueText == "MapTo") { var semanticModel = context.Compilation.GetSemanticModel(syntax.SyntaxTree); var methodInformation = semanticModel.GetSymbolInfo(genericNameSyntax); if (methodInformation.Symbol.ContainingAssembly.Name != CoreAssemblyName) { return; } var fromTypeInfo = semanticModel.GetTypeInfo(memberAccessSyntax.Expression); var fromTypeName = fromTypeInfo.Type.ToDisplayString(); var typeSyntax = genericNameSyntax.TypeArgumentList.Arguments.First(); var toTypeInfo = semanticModel.GetTypeInfo(typeSyntax); var toTypeName = toTypeInfo.Type.ToDisplayString(); var properties = ImmutableDictionary<string, string>.Empty .Add("fromType", fromTypeName) .Add("toType", toTypeName); context.ReportDiagnostic(Diagnostic.Create(AOTMapperIsNotReadyDescriptor, genericNameSyntax.GetLocation(), properties)); } } 

他所做的只是检查是否是我们需要的方法,从广义方法的第一个参数中一起调用MapTo<>的实体中提取类型,并生成诊断消息。


反过来,它将在AOTMapperCodeFixProvider内部进行处理。 在这里,我们获得有关将运行代码生成的类型的信息。 然后,用特定的实现替换对MapTo<>的调用。 然后,我们调用AOTMapperGenerator ,它将使用扩展方法生成一个文件。


在代码中,它看起来像这样:


 private async Task<Document> Handle(Diagnostic diagnostic, CodeFixContext context) { var fromTypeName = diagnostic.Properties["fromType"]; var toTypeName = diagnostic.Properties["toType"]; var document = context.Document; var semanticModel = await document.GetSemanticModelAsync(); var root = await diagnostic.Location.SourceTree.GetRootAsync(); var call = root.FindNode(diagnostic.Location.SourceSpan); root = root.ReplaceNode(call, SyntaxFactory.IdentifierName($"MapTo{toTypeName.Split('.').Last()}")); var pairs = ImmutableDictionary<string, string>.Empty .Add(fromTypeName, toTypeName); var generator = new AOTMapperGenerator(document.Project, semanticModel.Compilation); generator.GenerateMappers(pairs, new[] { "AOTMapper", "Mappers" }); var newProject = generator.Project; var documentInNewProject = newProject.GetDocument(document.Id); return documentInNewProject.WithSyntaxRoot(root); } 

AOTMapperGenerator本身通过创建类型之间具有映射关系的文件来修改传入的项目。
这样做如下:


 public void GenerateMappers(ImmutableDictionary<string, string> values, string[] outputNamespace) { foreach (var value in values) { var fromSymbol = this.Compilation.GetTypeByMetadataName(value.Key); var toSymbol = this.Compilation.GetTypeByMetadataName(value.Value); var fromSymbolName = fromSymbol.ToDisplayString().Replace(".", ""); var toSymbolName = toSymbol.ToDisplayString().Replace(".", ""); var fileName = $"{fromSymbolName}_To_{toSymbolName}"; var source = this.GenerateMapper(fromSymbol, toSymbol, fileName); this.Project = this.Project .AddDocument($"{fileName}.cs", source) .WithFolders(outputNamespace) .Project; } } private string GenerateMapper(INamedTypeSymbol fromSymbol, INamedTypeSymbol toSymbol, string fileName) { var fromProperties = fromSymbol.GetAllMembers() .OfType<IPropertySymbol>() .Where(o => (o.DeclaredAccessibility & Accessibility.Public) > 0) .ToDictionary(o => o.Name, o => o.Type); var toProperties = toSymbol.GetAllMembers() .OfType<IPropertySymbol>() .Where(o => (o.DeclaredAccessibility & Accessibility.Public) > 0) .ToDictionary(o => o.Name, o => o.Type); return $@" public static class {fileName}Extentions {{ public static {toSymbol.ToDisplayString()} MapTo{toSymbol.ToDisplayString().Split('.').Last()}(this {fromSymbol.ToDisplayString()} input) {{ var output = new {toSymbol.ToDisplayString()}(); { toProperties .Where(o => fromProperties.TryGetValue(o.Key, out var type) && type == o.Value) .Select(o => $" output.{o.Key} = input.{o.Key};" ) .JoinWithNewLine() } { toProperties .Where(o => !fromProperties.TryGetValue(o.Key, out var type) || type != o.Value) .Select(o => $" output.{o.Key} = ; // missing property") .JoinWithNewLine() } return output; }} }} "; } 

结论


总的来说,我们有一个可以在编写代码时正常工作的映射器,因此它的运行时没有任何剩余。 计划提出了一种添加配置功能的方法。 例如,为生成的方法的名称配置模板,并指定保存位置。 还可以跟踪类型的更改。 我有一个如何组织的想法,但是我怀疑这在资源消耗方面可能是很明显的,到目前为止,已经决定将其推迟。

Source: https://habr.com/ru/post/zh-CN459771/


All Articles