Elimine la molestia de escribir constructores para la inyección de dependencias con C # Source Generators

En abril de 2020, los desarrolladores de la plataforma .NET 5   anunciaron una  nueva forma de generar código fuente en el lenguaje de programación C #, utilizando una implementación de interfaz  ISourceGenerator



. Este método permite a los desarrolladores analizar código personalizado y  crear nuevos archivos fuente  en tiempo de compilación. Al mismo tiempo, la API de los nuevos generadores de código fuente es similar a la API  de los analizadores Roslyn . Puede generar código utilizando la  API del compilador de Roslyn y concatenando cadenas ordinarias.





En este artículo, consideraremos la biblioteca HarabaSourceGenerators.Generators y cómo se implementa





HarabaSourceGenerators.Generators

Todos estamos acostumbrados a inyectar un montón de dependencias en una clase e inicializarlas en el constructor. La salida suele ser algo como esto





public partial class HomeController : Controller
{
     private readonly TestService _testService;
        
     private readonly WorkService _workService;
        
     private readonly ExcelService _excelService;
        
     private readonly MrNService _mrNService;
        
     private readonly DotNetTalksService _dotNetTalksService;
       
     private readonly ILogger<HomeController> _logger;

     public HomeController(
         TestService testService,
         WorkService workService,
         ExcelService excelService,
         MrNService mrNService,
         DotNetTalksService dotNetTalksService,
         ILogger<HomeController> logger)
     {
         _testService = testService;
         _workService = workService;
         _excelService = excelService;
         _mrNService = mrNService;
         _dotNetTalksService = dotNetTalksService;
         _logger = logger;
     }
}
      
      



¡Es hora de acabar con esto!





Les presento a su atención una forma nueva, cómoda y elegante:





public partial class HomeController : Controller
{
    [Inject]
    private readonly TestService _testService;
        
    [Inject]
    private readonly WorkService _workService;
        
    [Inject]
    private readonly ExcelService _excelService;
        
    [Inject]
    private readonly MrNService _mrNService;
        
    [Inject]
    private readonly DotNetTalksService _dotNetTalksService;
        
    [Inject]
    private readonly ILogger<HomeController> _logger;
 }
      
      



Pero, ¿qué pasa si es demasiado vago para especificar el atributo Inject para cada dependencia?





No hay problema, puede especificar el atributo Inject para toda la clase. En este caso, se tomarán todos los campos privados con el modificador de solo lectura:





[Inject]
public partial class HomeController : Controller
{
    private readonly TestService _testService;
        
    private readonly WorkService _workService;
        
    private readonly ExcelService _excelService;
        
    private readonly MrNService _mrNService;
        
    private readonly DotNetTalksService _dotNetTalksService;
        
    private readonly ILogger<HomeController> _logger;
}
      
      



Excelente. Pero, ¿qué pasa si hay un campo que no es necesario para la inyección?





Especificamos el atributo InjectIgnore para dicho campo:





[Inject]
public partial class HomeController : Controller
{
    [InjectIgnore]
    private readonly TestService _testService;
        
    private readonly WorkService _workService;
        
    private readonly ExcelService _excelService;
        
    private readonly MrNService _mrNService;
        
    private readonly DotNetTalksService _dotNetTalksService;
        
    private readonly ILogger<HomeController> _logger;
}
      
      



Bien, ¿y qué pasa si quiero secuenciar las dependencias?





¿Adivina qué? Eso es correcto, no hay problema. Hay dos maneras:





1) Organice los campos en la secuencia deseada en la propia clase.

2) Pase el número de serie de la dependencia al atributo Inject





public partial class HomeController : Controller
{
    [Inject(2)]
    private readonly TestService _testService;

    [Inject(1)]
    private readonly WorkService _workService;

    [Inject(3)]
    private readonly ExcelService _excelService;

    [Inject(4)]
    private readonly MrNService _mrNService;

    [Inject(5)]
    private readonly DotNetTalksService _dotNetTalksService;

    [Inject(6)]
    private readonly ILogger<HomeController> _logger;
}
      
      



Como puede ver, la secuencia se ha guardado correctamente.





InjectSourceGenerator, ISourceGenerator.

. , , Inject. - partial , .

"{className}.Constructor.cs"





public void Execute(GeneratorExecutionContext context)
{
	var compilation = context.Compilation;
	var attributeName = nameof(InjectAttribute).Replace("Attribute", string.Empty);
	foreach (var syntaxTree in compilation.SyntaxTrees)
	{
		var semanticModel = compilation.GetSemanticModel(syntaxTree);
		var targetTypes = syntaxTree.GetRoot().DescendantNodes()
			.OfType<ClassDeclarationSyntax>()
			.Where(x => x.ContainsClassAttribute(attributeName) || x.ContainsFieldAttribute(attributeName))
			.Select(x => semanticModel.GetDeclaredSymbol(x))
			.OfType<ITypeSymbol>();

		foreach (var targetType in targetTypes)
		{
			string source = GenerateInjects(targetType);
			context.AddSource($"{targetType.Name}.Constructor.cs", SourceText.From(source, Encoding.UTF8));
		}
	}
}
      
      



. , , . , , .





private string GenerateInjects(ITypeSymbol targetType)
{
            return $@" 
using System;
namespace {targetType.ContainingNamespace}
{{
    public partial class {targetType.Name}
    {{
        {GenerateConstructor(targetType)}
    }}
}}";
}
      
      



( ).

, . Inject , , readonly InjectIgnore. , Inject. , .





private string GenerateConstructor(ITypeSymbol targetType)
{
	var parameters = new StringBuilder();
	var fieldsInitializing = new StringBuilder();
	var fields = targetType.GetAttributes().Any(x => x.AttributeClass.Name == nameof(InjectAttribute)) 
					? targetType.GetMembers()
						.OfType<IFieldSymbol>()
						.Where(x => x.IsReadOnly && !x.GetAttributes().Any(y => y.AttributeClass.Name == nameof(InjectIgnoreAttribute)))
					: targetType.GetMembers()
						.OfType<IFieldSymbol>()
						.Where(x => x.GetAttributes().Any(y => y.AttributeClass.Name == nameof(InjectAttribute)));

	var orderedFields = fields.OrderBy(x => x.GetAttributes()
											 .First(e => e.AttributeClass.Name == nameof(InjectAttribute))
											 .ConstructorArguments.FirstOrDefault().Value ?? default(int)).ToList();
	foreach (var field in orderedFields)
	{
		var parameterName = field.Name.TrimStart('_');
		parameters.Append($"{field.Type} {parameterName},");
		fieldsInitializing.AppendLine($"this.{field.Name} = {parameterName};");
	}

	return $@"public {targetType.Name}({parameters.ToString().TrimEnd(',')})
			  {{
				  {fieldsInitializing}
			  }}";
}
      
      



partial, . , !





   GitHub.

Nuget HarabaSourceGenerators








All Articles