В апреле 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.