Elimine o incômodo de escrever construtores para injeção de dependência com geradores de código-fonte C #

Em abril de 2020, os desenvolvedores da plataforma .NET 5   anunciaram uma  nova maneira de gerar código-fonte na linguagem de programação C # - usando uma implementação de interface  ISourceGenerator



. Este método permite que os desenvolvedores analisem o código personalizado e  criem novos arquivos de origem  em tempo de compilação. Ao mesmo tempo, a API dos novos geradores de código-fonte é semelhante à API  dos analisadores Roslyn . Você pode gerar código usando a  API do compilador Roslyn e concatenando strings comuns.





Neste artigo, consideraremos a biblioteca HarabaSourceGenerators.Generators e como ela é implementada





HarabaSourceGenerators.Generators

Estamos todos acostumados a injetar um monte de dependências em uma classe e inicializá-las no construtor. A saída geralmente é algo assim





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;
     }
}
      
      



É hora de acabar com isso!





Apresento a sua atenção uma maneira nova, conveniente e 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;
 }
      
      



Mas e se você estiver com preguiça de especificar o atributo Inject para cada dependência?





Sem problemas, você pode especificar o atributo Inject para toda a classe. Nesse caso, todos os campos privados com o modificador somente leitura serão usados:





[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. Mas e se houver um campo que não seja necessário para injeção?





Especificamos o atributo InjectIgnore para esse 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;
}
      
      



Ok, e se eu quiser sequenciar as dependências?





Adivinha? Isso mesmo, não é um problema. Existem duas maneiras:





1) Organize os campos na sequência desejada na própria classe.

2) Passe o número de série da dependência para o atributo Injetar





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 você pode ver, a sequência foi salva com sucesso.





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