En un art铆culo anterior , describ铆 una forma de organizar la generaci贸n de c贸digo con Roslyn. La tarea de entonces era demostrar un enfoque com煤n. Ahora quiero darme cuenta de algo que tendr谩 una aplicaci贸n real.
Entonces, cualquier persona interesada en ver c贸mo puede hacer que una biblioteca como AutoMapper pida un gato.
Introduccion
En primer lugar, creo que vale la pena describir c贸mo funcionar谩 mi Ahead of Time Mapper (AOTMapper). El punto de entrada de nuestro mapeador ser谩 el m茅todo de extensi贸n gen茅rico MapTo<>
. El analizador lo buscar谩 y ofrecer谩 implementar el MapToUser
extensi贸n MapToUser
, donde User
es el tipo que se pasa a MapTo<>
.
Como ejemplo, tome las siguientes clases:
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; } } }
El MapToUser
generado se ver谩 as铆:
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 = ;
Como puede ver en este ejemplo, todas las propiedades con los mismos nombres y tipos se asignan autom谩ticamente. A su vez, aquellos para los que no se encontraron coincidencias contin煤an "colg谩ndose" creando un error de compilaci贸n y el desarrollador de alguna manera debe manejarlos.
Por ejemplo, as铆:
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; }
Durante la generaci贸n de MapToUser
, la MapTo<User>
llamada MapTo<User>
ser谩 reemplazada por MapToUser
.
C贸mo funciona en movimiento se puede ver aqu铆:
AOTMapper tambi茅n se puede instalar a trav茅s de nuget:
Install-Package AOTMapper
El c贸digo completo del proyecto se puede encontrar aqu铆 .
Implementaci贸n
Pens茅 durante mucho tiempo c贸mo se puede hacer esto de manera diferente y al final llegu茅 a la conclusi贸n de que esto no es tan malo, ya que esto resuelve algunos de los inconvenientes que me atormentaban al usar AutoMapper
.
En primer lugar, obtenemos diferentes m茅todos de extensi贸n para diferentes tipos, como resultado de lo cual, para algunos tipos de User
abstractos, podemos usar IntelliSense muy f谩cilmente para averiguar qu茅 mapas ya est谩n implementados sin tener que buscar el mismo archivo donde est谩n registrados nuestros mapas. Solo mira qu茅 m茅todos de extensi贸n ya tienes.
En segundo lugar, en tiempo de ejecuci贸n es solo un m茅todo de extensi贸n y, por lo tanto, evitamos cualquier sobrecarga asociada con la llamada a nuestro mapeador. Entiendo que los desarrolladores de AutoMapper
dedicaron mucho esfuerzo a optimizar la llamada, pero todav铆a hay algunos costos adicionales. Mi peque帽o punto de referencia mostr贸 que, en promedio, es de 140-150ns por llamada, excluyendo el tiempo de inicializaci贸n. El punto de referencia en s铆 se puede ver en el repositorio, y los resultados de la medici贸n son m谩s bajos.
Adem谩s, las ventajas de este mapeador incluyen el hecho de que generalmente no requiere tiempo para inicializarse cuando se inicia la aplicaci贸n, lo que puede ser 煤til en aplicaciones grandes.
El analizador tiene la siguiente forma (falta el c贸digo de enlace):
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)); } }
Todo lo que hace es verificar si es el m茅todo que necesitamos, extrae el tipo de la entidad en la que se llama MapTo<>
desde el primer par谩metro del m茅todo generalizado y genera un mensaje de diagn贸stico.
A su vez, se procesar谩 dentro del AOTMapperCodeFixProvider
. Aqu铆 obtenemos informaci贸n sobre los tipos sobre los que ejecutaremos la generaci贸n de c贸digo. Luego reemplazamos la llamada a MapTo<>
con una implementaci贸n espec铆fica. Luego llamamos a AOTMapperGenerator
que generar谩 un archivo con el m茅todo de extensi贸n.
En el c贸digo, se ve as铆:
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
modifica el proyecto entrante creando archivos con mapeos entre los tipos.
Esto se hace de la siguiente manera:
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; }} }} "; }
Conclusiones
En total, tenemos un mapeador que funciona justo al momento de escribir el c贸digo, y luego no queda nada de su tiempo de ejecuci贸n. Los planes presentan una forma de agregar capacidades de configuraci贸n. Por ejemplo, configure las plantillas para los nombres de los m茅todos generados y especifique el directorio donde guardar. Tambi茅n agregue la capacidad de rastrear cambios en los tipos. Tengo una idea de c贸mo se puede organizar esto, pero sospecho que esto puede ser notable en t茅rminos de consumo de recursos, y hasta ahora se ha decidido retrasar esto.