Избавляемся от постоянного написания конструкторов для инжекта зависимостей с помощью C# Source Generators

от автора

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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *