在上一篇文章中,我描述了一种使用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 = ;
从该示例中可以看到,具有相同名称和类型的所有属性都是自动分配的。 反过来,没有找到匹配项的对象继续“挂起”,从而产生编译错误,开发人员必须以某种方式处理它们。
例如,像这样:
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(不包括初始化时间)。 可以在存储库中查看基准本身,并且测量结果较低。
另外,此映射器的优点包括以下事实:启动应用程序时通常不需要时间来初始化,这在大型应用程序中可能很有用。
分析器本身具有以下形式(缺少绑定代码):
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; }} }} "; }
结论
总的来说,我们有一个可以在编写代码时正常工作的映射器,因此它的运行时没有任何剩余。 计划提出了一种添加配置功能的方法。 例如,为生成的方法的名称配置模板,并指定保存位置。 还可以跟踪类型的更改。 我有一个如何组织的想法,但是我怀疑这在资源消耗方面可能是很明显的,到目前为止,已经决定将其推迟。