В апреле 2020-го года разработчиками платформы .NET 5 был анонсирован новый способ генерации исходного кода на языке программирования C# — с помощью реализации интерфейса ISourceGenerator
. Данный способ позволяет разработчикам анализировать пользовательский код и создавать новые исходные файлы на этапе компиляции. При этом, API новых генераторов исходного кода схож с API анализаторов Roslyn. Генерировать код можно как с помощью Roslyn Compiler API, так и методом конкатенации обычных строк.
В данном материале рассмотрим библиотеку HarabaSourceGenerators.Generators и то, как она реализована
HarabaSourceGenerators.Generators
Все мы привыкли инжектить кучу зависимостей в класс и инициализировать их в конструкторе. На выходе обычно получаем что-то типа этого
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;
}
}
Пора с этим кончать!
Представляю вашему вниманию новый, удобный и элегантный способ:
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;
}
А что, если лень указывать для каждой зависимости атрибут Inject?
Не проблема, можно указать атрибут Inject для всего класса. В таком случае будут браться все приватные поля с модификатором readonly:
[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;
}
Отлично. Но что, если есть поле, которое нужно не для инжекта?
Указываем для такого поля атрибут InjectIgnore:
[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;
}
Ну окей, а что, если я хочу указать последовательность для зависимостей?
Угадайте что? Правильно, не проблема. Есть два способа:
1) Расставить поля в нужной последовательности в самом классе.
2) В атрибут 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;
}
Как видим, последовательность успешно сохранена.
Взглянем на реализацию
У нас есть класс 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
Evengard
Я пока что не опробовал, но выглядит очень многообещающе! Уже немного задолбало писать эти инжекты по несколько раз =) Спасибо, попробую применить в ближайшее же время.