[Open source, .NET, C#] Избавляемся от постоянного написания конструкторов для инжекта зависимостей с помощью C# Source Generators
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В апреле 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, Софт] В OpenSSL нашли две опасные уязвимости
- [Open source, GitHub, Законодательство в IT, Социальные сети и сообщества, Биографии гиков] Multiple violations of policies in RMS open letter
- [Open source, Программирование, Системное программирование, Компиляторы, Rust] Rust 1.51.0: const generics MVP, новый распознаватель функциональности Cargo (перевод)
- [Open source, Программирование, Геоинформационные сервисы, Визуализация данных, Научно-популярное] Google Earth Engine (GEE) как общедоступный каталог больших геоданных
- [Open source] Нетехнические вызовы Open Source разработки
- [Open source, Учебный процесс в IT, Управление персоналом, Карьера в IT-индустрии] Ваша любовь к разработке в первую очередь выгодна работодателю (перевод)
- [.NET, C#, Программирование микроконтроллеров, Интернет вещей, DIY или Сделай сам] .NET nanoFramework — платформа для разработки приложений на C# для микроконтроллеров
- [Open source, Законодательство в IT, История IT, IT-компании] Фонд СПО обещает сделать процесс выбора новых членов прозрачным, а для начала испытает его на старых
- [Разработка мобильных приложений, Git, Big Data, Машинное обучение] DVC — Git для данных на примере ML-проекта
- [C#, Unity] Основы Unity + Mirror
Теги для поиска: #_open_source, #_.net, #_c#, #_sourcegenerator, #_open_source, #_.net, #_c#
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:46
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В апреле 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]
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; } [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; } 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} }}"; } =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:46
Часовой пояс: UTC + 5