В апреле 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
ссылка на оригинал статьи https://habr.com/ru/post/549394/
Добавить комментарий