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