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

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
28-Мар-2021 22:32

В апреле 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.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_open_source, #_.net, #_c#, #_sourcegenerator, #_open_source, #_.net, #_c#
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 14:46
Часовой пояс: UTC + 5