Skip to content

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:
    1. 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 atributo DtoGeneratorAttribute.
    2. Filtra Nodos: FilterItems se utiliza para determinar si un nodo de sintaxis es una clase o un registro que tiene atributos.
    3. Transforma Nodos: GetClassWithDtoGeneratorAttribute se aplica a los nodos de sintaxis que pasan el filtro, extrayendo el INamedTypeSymbol para clases o registros decorados con el atributo DtoGeneratorAttribute.
    4. 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 utilizando GenerateDto.

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 un ClassDeclarationSyntax y si tiene algún atributo.

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: El GeneratorSyntaxContext a analizar.
  • Devuelve:
    • El INamedTypeSymbol si la clase o registro está decorado con DtoGeneratorAttribute; de lo contrario, null.
  • Funcionalidad:
    1. Verifica Clase/Registro: Verifica si el nodo del contexto es un ClassDeclarationSyntax o un RecordDeclarationSyntax.
    2. Obtiene el Símbolo: Obtiene el INamedTypeSymbol de la declaración de clase o registro utilizando ctx.SemanticModel.GetDeclaredSymbol.
    3. Verifica el Atributo: Determina si el símbolo tiene el atributo DtoGeneratorAttribute examinando sus atributos.

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: El INamedTypeSymbol que representa la clase de comando.
  • Funcionalidad:
    1. Inicializa el Constructor de Código: Crea un StringBuilder para construir el código fuente de la clase DTO.
    2. 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”.
    3. Añade el Espacio de Nombres y la Clase: Añade las declaraciones necesarias de namespace y class al código.
    4. Añade Propiedades: Llama al método AddProperties para añadir las propiedades de la clase de comando al DTO.
    5. Añade el Código Fuente: Utiliza context.AddSource para añadir el código fuente generado a la compilación.

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: El StringBuilder para usar en la construcción de las declaraciones de propiedades.
    • command: El INamedTypeSymbol del cual extraer las propiedades.
  • Funcionalidad:
    1. 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).
    2. Añade Propiedades al Constructor: Añade las declaraciones de propiedades formateadas al `

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 information
using 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();
  1. 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
  2. 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>
  3. 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);
  4. Compilar el proyecto y verificar que se generó la clase DTO correspondiente al comando.

    Terminal window
    dotnet build
  5. Verificar que se generó la clase CreateUserDto en el proyecto. Este se encuentra disponible en el namespace CodeDesignPlus.Microservice.Api.Dtos.

    // See https://aka.ms/new-console-template for more information
    using 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();