Dto Generator
La clase DtoGenerator
implementa la interfaz IIncrementalGenerator
, lo que le permite procesar código fuente de manera incremental y generar nuevo código durante el proceso de compilación. El objetivo principal de este generador es reducir el código repetitivo creando automáticamente DTOs que reflejen las propiedades de las clases o registros de comandos.
Características Principales
- Generación Automática de DTO: Crea clases DTO basadas en clases o registros de comandos que están decorados con el atributo
DtoGeneratorAttribute
. - Personalización del Espacio de Nombres: Genera las clases DTO dentro del espacio de nombres
CodeDesignPlus.Microservice.Api.Dtos
. - Mapeo de Propiedades: Copia las propiedades públicas de la clase de origen al DTO generado.
- Convención de Nombres: El nombre del DTO se deriva del nombre de la clase de origen, reemplazando el sufijo “Command”, “Commands”, “Commandd” o “Commandds” con “Dto”. Si la clase de origen tiene un tipo contenedor, el nombre será el nombre del tipo contenedor seguido de Dto.
- Procesamiento Incremental: Utiliza la generación de código fuente incremental para mejorar el rendimiento y reducir el tiempo de compilación.
using CodeDesignPlus.Net.Generator.Attributes;using Microsoft.CodeAnalysis;using Microsoft.CodeAnalysis.CSharp;using Microsoft.CodeAnalysis.CSharp.Syntax;using Microsoft.CodeAnalysis.Text;using System.Linq;using System.Text;using System.Text.RegularExpressions;
namespace CodeDesignPlus.Net.Generator{ /// <summary> /// Source generator for creating DTO classes. /// </summary> /// <remarks>https://andrewlock.net/series/creating-a-source-generator/</remarks> [Generator(LanguageNames.CSharp)] public class DtoGenerator : IIncrementalGenerator { private const string PATTERN = @"(Comman?d?s?)$";
/// <summary> /// Initializes the generator. /// </summary> /// <param name="context">The generator initialization context.</param> public void Initialize(IncrementalGeneratorInitializationContext context) { IncrementalValuesProvider<INamedTypeSymbol> commands = context.SyntaxProvider .CreateSyntaxProvider( predicate: static (syntaxNode, _) => FilterItems(syntaxNode), transform: static (ctx, _) => GetClassWithDtoGeneratorAttribute(ctx) ) .Where(static m => m is not null) .Select(static (m, _) => m!);
context.RegisterSourceOutput(commands, static (context, command) => GenerateDto(context, command)); }
/// <summary> /// Filter the items that will be processed by the generator /// </summary> /// <param name="syntaxNode">The syntax node to filter</param> /// <returns>True if the syntax node is a class or record decorated with the DtoGenerator attribute, false otherwise</returns> public static bool FilterItems(SyntaxNode syntaxNode) { if(syntaxNode is RecordDeclarationSyntax recordDeclaration && recordDeclaration.AttributeLists.Count > 0) return true;
if (syntaxNode is ClassDeclarationSyntax classDeclaration && classDeclaration.AttributeLists.Count > 0) return true;
return false; }
/// <summary> /// Obtains the class if it is decorated with the DtoGenerator Attribute /// </summary> /// <param name="ctx">The GeneratorSyntaxContext to use</param> /// <returns>The INamedTypeSymbol if the class is decorated with the DtoGenerator attribute, null otherwise</returns> private static INamedTypeSymbol GetClassWithDtoGeneratorAttribute(GeneratorSyntaxContext ctx) { if (ctx.Node is ClassDeclarationSyntax classDeclarationSyntax) { if (ctx.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol namedTypeSymbol) return null;
if (!namedTypeSymbol.GetAttributes().Any(attr => attr.AttributeClass?.Name == nameof(DtoGeneratorAttribute))) return null;
return namedTypeSymbol; }
if (ctx.Node is RecordDeclarationSyntax recordDeclarationSyntax) { if (ctx.SemanticModel.GetDeclaredSymbol(recordDeclarationSyntax) is not INamedTypeSymbol namedTypeSymbol) return null;
if (!namedTypeSymbol.GetAttributes().Any(attr => attr.AttributeClass?.Name == nameof(DtoGeneratorAttribute))) return null;
return namedTypeSymbol; }
return null; }
/// <summary> /// Generates the DTO class from the command class /// </summary> /// <param name="context">Context for the source generation</param> /// <param name="command">The command class to generate the DTO</param> private static void GenerateDto(SourceProductionContext context, INamedTypeSymbol command) { var codeBuilder = new StringBuilder(); var dtoName = command.ContainingType != null ? $"{command.ContainingType.Name}Dto" : Regex.Replace(command.Name, PATTERN, "Dto", RegexOptions.None, System.TimeSpan.FromSeconds(1)); codeBuilder.AppendLine($"namespace CodeDesignPlus.Microservice.Api.Dtos"); codeBuilder.AppendLine("{"); codeBuilder.AppendLine($"public class {dtoName}"); codeBuilder.AppendLine("{");
AddProperties(codeBuilder, command);
codeBuilder.AppendLine("}"); codeBuilder.AppendLine("}");
context.AddSource($"{dtoName}.g.cs", SourceText.From(codeBuilder.ToString(), Encoding.UTF8)); }
/// <summary> /// Add the properties to the DTO class /// </summary> /// <param name="codeBuilder">StringBuilder to add the properties</param> /// <param name="command">The command class to generate the DTO</param> private static void AddProperties(StringBuilder codeBuilder, INamedTypeSymbol command) { var properties = command.GetMembers() .OfType<IPropertySymbol>() .Where(prop => prop.DeclaredAccessibility == Accessibility.Public && !prop.IsStatic && !prop.IsReadOnly && prop.Name != "EqualityContract") .Select(prop => $"\t\t public {prop.Type.ToDisplayString()} {prop.Name} {{ get; set; }}");
foreach (var property in properties) { codeBuilder.AppendLine(property); } } }}
Métodos
Initialize
Type: Initialize(IncrementalGeneratorInitializationContext context)
Este método es el punto de entrada del generador de código fuente y es llamado por el compilador cuando se inicializa el generador.
- Parámetros:
context
: Proporciona acceso a la información y las herramientas necesarias para realizar la generación de código fuente.
- Funcionalidad:
- Crea el Proveedor de Sintaxis: Utiliza
context.SyntaxProvider.CreateSyntaxProvider
para establecer una tubería para procesar nodos de sintaxis. Esta tubería filtra y transforma nodos para identificar clases o registros decorados con el atributoDtoGeneratorAttribute
. - Filtra Nodos:
FilterItems
se utiliza para determinar si un nodo de sintaxis es una clase o un registro que tiene atributos. - Transforma Nodos:
GetClassWithDtoGeneratorAttribute
se aplica a los nodos de sintaxis que pasan el filtro, extrayendo elINamedTypeSymbol
para clases o registros decorados con el atributoDtoGeneratorAttribute
. - Registra la Salida de Código Fuente:
context.RegisterSourceOutput
registra una acción para producir archivos de código fuente procesando las clases/registros de comandos identificados utilizandoGenerateDto
.
- Crea el Proveedor de Sintaxis: Utiliza
FilterItems
Type: FilterItems(SyntaxNode syntaxNode)
Este método filtra los nodos de sintaxis para determinar si son clases o registros que deberían ser procesados por el generador.
- Parámetros:
syntaxNode
: El nodo de sintaxis a evaluar.
- Devuelve:
true
si el nodo es una clase o registro que tiene al menos un atributo; de lo contrario,false
.
- Funcionalidad:
- Verifica si el nodo de sintaxis es un
RecordDeclarationSyntax
o unClassDeclarationSyntax
y si tiene algún atributo.
- Verifica si el nodo de sintaxis es un
GetClassWithDtoGeneratorAttribute
Type: GetClassWithDtoGeneratorAttribute(GeneratorSyntaxContext ctx)
Este método extrae el INamedTypeSymbol
de un GeneratorSyntaxContext
si la clase o registro está decorado con el atributo DtoGeneratorAttribute
.
- Parámetros:
ctx
: ElGeneratorSyntaxContext
a analizar.
- Devuelve:
- El
INamedTypeSymbol
si la clase o registro está decorado conDtoGeneratorAttribute
; de lo contrario,null
.
- El
- Funcionalidad:
- Verifica Clase/Registro: Verifica si el nodo del contexto es un
ClassDeclarationSyntax
o unRecordDeclarationSyntax
. - Obtiene el Símbolo: Obtiene el
INamedTypeSymbol
de la declaración de clase o registro utilizandoctx.SemanticModel.GetDeclaredSymbol
. - Verifica el Atributo: Determina si el símbolo tiene el atributo
DtoGeneratorAttribute
examinando sus atributos.
- Verifica Clase/Registro: Verifica si el nodo del contexto es un
GenerateDto
Type: GenerateDto(SourceProductionContext context, INamedTypeSymbol command)
Este método genera el código fuente de la clase DTO a partir de un INamedTypeSymbol
dado.
- Parámetros:
context
: Proporciona herramientas para emitir código fuente.command
: ElINamedTypeSymbol
que representa la clase de comando.
- Funcionalidad:
- Inicializa el Constructor de Código: Crea un
StringBuilder
para construir el código fuente de la clase DTO. - Determina el Nombre del DTO: Genera el nombre del DTO basado en el nombre de la clase de comando (o su nombre de tipo contenedor si está disponible), reemplaza cualquier coincidencia con “Dto”.
- Añade el Espacio de Nombres y la Clase: Añade las declaraciones necesarias de
namespace
yclass
al código. - Añade Propiedades: Llama al método
AddProperties
para añadir las propiedades de la clase de comando al DTO. - Añade el Código Fuente: Utiliza
context.AddSource
para añadir el código fuente generado a la compilación.
- Inicializa el Constructor de Código: Crea un
AddProperties
Type: AddProperties(StringBuilder codeBuilder, INamedTypeSymbol command)
Este método añade las propiedades de la clase de comando a la clase DTO utilizando un StringBuilder
.
- Parámetros:
codeBuilder
: ElStringBuilder
para usar en la construcción de las declaraciones de propiedades.command
: ElINamedTypeSymbol
del cual extraer las propiedades.
- Funcionalidad:
- Extrae las Propiedades Públicas: Obtiene todas las propiedades públicas, no estáticas, no de solo lectura de la clase de comando (excluyendo
EqualityContract
). - Añade Propiedades al Constructor: Añade las declaraciones de propiedades formateadas al `
- Extrae las Propiedades Públicas: Obtiene todas las propiedades públicas, no estáticas, no de solo lectura de la clase de comando (excluyendo
DtoGeneratorAttribute
El atributo DtoGeneratorAttribute
se utiliza para marcar las clases que deben tener un DTO generado. Es un atributo personalizado que se aplica a las clases para indicar que se debe generar un DTO para ellas.
using System;
namespace CodeDesignPlus.Net.Generator.Attributes{ /// <summary> /// Attribute to indicate that a DTO (Data Transfer Object) should be generated for the class. /// </summary> [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public class DtoGeneratorAttribute : Attribute { }}
Ejemplo de Uso
A continuación, se muestra un ejemplo de cómo utilizar el generador de DTOs en una aplicación .NET Core:
// See https://aka.ms/new-console-template for more informationusing CodeDesignPlus.Microservice.Api.Dtos;
var createUserDto = new CreateUserDto(){ Name = "John", Email = "john.doe@codedesignplus.com", LastName = "Doe", Birthdate = new DateTime(1990, 10, 10), Password = ""};
var updateUserDto = new UpdateUserDto() { Id = Guid.NewGuid(), Name = "John", Email = "john.doe@codedesignplus.com", LastName = "Doe", Birthdate = new DateTime(1990, 10, 10), Password = "" };
Console.WriteLine(createUserDto.Name);Console.WriteLine(updateUserDto.Id);
Console.ReadLine();
-
Crear la librería que corresponde a la capa de aplicación con respecto a la arquitectura hexagonal.
- CodeDesignPlus.Net.Generator.Sample.sln
Directorysrc
DirectoryCodeDesignPlus.Net.Generator.Application
- CodeDesignPlus.Net.Generator.Application.csproj
- CreateUserCommand.cs
- CreateUserCommandHandler.cs
DirectoryCodeDesignPlus.Net.Generator.Sample
- CodeDesignPlus.Net.Generator.Sample.csproj
- Program.cs
-
Instalar la ultima versión del paquete
CodeDesignPlus.Net.Generator
en la capa de aplicación.Terminal window dotnet add package CodeDesignPlus.Net.Generator<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net9.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup><ItemGroup><PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.12.0" /><PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" /><ProjectReference Include="..\..\..\..\packages\CodeDesignPlus.Net.Generator\src\CodeDesignPlus.Net.Generator\CodeDesignPlus.Net.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /></ItemGroup><ItemGroup><Compile Include="..\..\..\..\packages\CodeDesignPlus.Net.Generator\src\CodeDesignPlus.Net.Generator\Attributes\DtoGeneratorAttribute.cs" Link="DtoGeneratorAttribute.cs" /></ItemGroup></Project> -
Crear un comando que representa la acción de crear un usuario en la aplicación y decorarlo con el atributo
DtoGeneratorAttribute
.using CodeDesignPlus.Net.Generator.Attributes;namespace CodeDesignPlus.Net.Generator.Application.Users.Commands.CreateUser;[DtoGenerator]public class CreateUserCommand{public required string Name { get; set; }public required string LastName { get; set; }public required string Email { get; set; }public Instant Birthdate { get; set; }public required string Password { get; set; }public required string Other { get; set; }}using CodeDesignPlus.Net.Generator.Attributes;namespace CodeDesignPlus.Net.Generator.Application.Users.Commands.UpdateUser;[DtoGenerator]public record UpdateUserCommand(Guid Id, string Name, string LastName, string Email, DateTime Birthdate, string Password, string Other); -
Compilar el proyecto y verificar que se generó la clase DTO correspondiente al comando.
Terminal window dotnet build -
Verificar que se generó la clase
CreateUserDto
en el proyecto. Este se encuentra disponible en el namespaceCodeDesignPlus.Microservice.Api.Dtos
.// See https://aka.ms/new-console-template for more informationusing CodeDesignPlus.Microservice.Api.Dtos;var createUserDto = new CreateUserDto(){Name = "John",Email = "john.doe@codedesignplus.com",LastName = "Doe",Birthdate = new DateTime(1990, 10, 10),Password = ""};var updateUserDto = new UpdateUserDto() {Id = Guid.NewGuid(),Name = "John",Email = "john.doe@codedesignplus.com",LastName = "Doe",Birthdate = new DateTime(1990, 10, 10),Password = ""};Console.WriteLine(createUserDto.Name);Console.WriteLine(updateUserDto.Id);Console.ReadLine();