С момента выхода генератора исходного кода Pure.DI версии 2.0 прошло уже больше, чем полгода. За это время появились отзывы по использованию, удалось добавить несколько полезных фич, улучшить производительность анализа и качество генерируемого кода, а также исправить ошибки и мелкие недочеты. В этой статье разберем несколько новых возможностей версии 2.1, которые могут быть особенно полезны.

Времена жизни PerBlock и Scoped
"Время жизни объектов в Dependency Injection - это как срок годности продуктов в холодильнике: если выбирать их спустя рукава, то объекты могут стать испорченными" (с) YaGPT 2. В переводе на человеческий - неверно выбранное время жизни объекта может стать причиной излишней траты ресурсов: памяти и процессора или даже ошибок в приложении. Изначально Pure.DI поддерживал следующие времена жизни:
Transient - объект создается каждый раз при его внедрении (по умолчанию)
Singleton - объект создается один раз для всех корней композиции
PerResolve - объект создается один раз для каждого из корней композиции
С первыми двумя все относительно просто. Остановимся на последнем в этом списке - PerResolve. Это время жизни позволяет использовать один и тот же объект внутри одного графа объектов - "Singleton на минималках", для одной композиции объектов, например:
interface IDependency; class Dependency : IDependency; class Service { public Service( IDependency dep1, IDependency dep2, Lazy<(IDependency dep3, IDependency dep4)> deps) { Dep1 = dep1; Dep2 = dep2; Dep3 = deps.Value.dep3; Dep4 = deps.Value.dep4; } public IDependency Dep1 { get; } public IDependency Dep2 { get; } public IDependency Dep3 { get; } public IDependency Dep4 { get; } } DI.Setup(nameof(Composition)) .Bind<IDependency>().As(Lifetime.PerResolve).To<Dependency>() .Root<Service>("Root"); var composition = new Composition(); var service1 = composition.Root; service1.Dep1.ShouldBe(service1.Dep2); service1.Dep3.ShouldBe(service1.Dep4); service1.Dep1.ShouldBe(service1.Dep3); var service2 = composition.Root; service2.Dep1.ShouldNotBe(service1.Dep1);
В примере выше, при создании композиции объектов с корнем service1, его зависимости в свойствах Dependency1, Dependency2, Dependency3, Dependency4 будут содержать один и тот же объект. Для композиции с корнем service2 для этих свойств будет создан другой объект. Генерируемый код - чуть сложнее, чем можно было ожидать:
partial class Composition { public IService Root { var dependency = default(Dependency); var funcDeps = new Func<(IDependency dep3, IDependency dep4)>( () => { if (ReferenceEquals(dependency, null)) { lock (_lock) { if (ReferenceEquals(dependency, null)) { dependency = new Dependency(); } } } return (dependency, dependency); }); var lazyDeps = new Lazy<(IDependency dep3, IDependency dep4)>(funcDeps, true); if (ReferenceEquals(dependency, null)) { lock (_lock) { if (ReferenceEquals(dependency, null)) { dependency = new Dependency(); } } } return new Service(dependency, dependency, lazyDeps); } }
Этот код учитывает сценарии когда PerResolve зависимости, такие как Dependency, будут создаваться лениво в лямбда функциях, при работе с IEnumerable<T>, Lazy<T> и т.д. Очевидно, что генерируемый код тратит ресурсы на проверку "единственности". В некоторых сценариях такое жесткое требование чрезмерно, и достаточно просто уменьшить количество объектов за счет их повторного использования, но без фанатизма, т.е. без гарантии их "единственности". Время жизни PerBlock - своего рода компромисс между количеством объектов типа внутри композиции и тратами на проверку "единственности", например:
DI.Setup(nameof(Composition)) // PerResolve -> PerBlock .Bind<IDependency>().As(Lifetime.PerBlock).To<Dependency>() .Root<Service>("Root"); var composition = new Composition(); var service1 = composition.Root; service1.Dep1.ShouldBe(service1.Dep2); service1.Dep3.ShouldBe(service1.Dep4); service1.Dep1.ShouldNotBe(service1.Dep3); var service2 = composition.Root; service2.Dep1.ShouldNotBe(service1.Dep1);
Для одного корня композиции с корнем service1 будет создано два объекта типа Dependency:
первый - для свойств Dependecy1 и Dependecy2
второй - при обращении к deps, для свойств Dependecy3 и Dependecy4
Сгенерированный код выглядит примерно так:
partial class Composition { public Service Root { var funcDeps = new Func<(IDependency dep3, IDependency dep4)>( () => { // второй var dependency34 = new Dependency(); return (dependency34, dependency34); }); var lazyDeps = new Lazy<(IDependency dep3, IDependency dep4)>(funcDeps, true); // первый var dependency12 = new Dependency(); return new Service(dependency12, dependency12, lazyDeps); } }
В созданном коде нет лишних проверок на "единственность" объектов типа Dependency и не нужна синхронизация потоков. Таким образом PerBlock улучшает производительность создания композиции объектов, но потенциально увеличивает количество объектов. Если создание новых объектов и их утилизация выгоднее чем проверка "единственности", то время жизни PerBlock будет полезным в конкретной ситуации.
Другое нововведение в Pure.DI версии 2.1 - это время жизни Scoped. Большинству пользователей библиотеки Microsoft Dependency Injection хорошо известно назначение времени жизни Scoped:
обеспечить "единственность" объекта в рамках некоторой сессии (области применения)
обеспечить утилизацию объекта при завершении этой сессии
В Pure.DI время жизни Scoped работает похоже, например:
interface IDependency { bool IsDisposed { get; } } class Dependency : IDependency, IDisposable { public bool IsDisposed { get; private set; } public void Dispose() => IsDisposed = true; } interface IService { IDependency Dependency { get; } } class Service(IDependency dependency) : IService { public IDependency Dependency => dependency; } // Implements a session class Session(Composition composition) : Composition(composition); class Program(Func<Session> sessionFactory) { public Session CreateSession() => sessionFactory(); } partial class Composition { private static void Setup() => DI.Setup(nameof(Composition)) .Bind<IDependency>().As(Scoped).To<Dependency>() .Bind<IService>().To<Service>() // Session composition root .Root<IService>("SessionRoot") // Program composition root .Root<Program>("ProgramRoot"); } var composition = new Composition(); var program = composition.ProgramRoot; // Creates session #1 var session1 = program.CreateSession(); var dependency1 = session1.SessionRoot.Dependency; var dependency12 = session1.SessionRoot.Dependency; // Checks the identity of scoped instances in the same session dependency1.ShouldBe(dependency12); // Creates session #2 var session2 = program.CreateSession(); var dependency2 = session2.SessionRoot.Dependency; // Checks that the scoped instances are not identical in different sessions dependency1.ShouldNotBe(dependency2); // Disposes of session #1 session1.Dispose(); // Checks that the scoped instance is finalized dependency1.IsDisposed.ShouldBeTrue(); // Disposes of session #2 session2.Dispose(); // Checks that the scoped instance is finalized dependency2.IsDisposed.ShouldBeTrue();
В примере выше создается две сессии session1 и session2. Каждая сессия внутри себя будет использовать единственный объект типа Dependency. Вместе с утилизацией сессии то же самое произойдет и со всеми её Scoped зависимостями.
Всё, что необходимо для этого:
определиться с типами, которые будут использоваться как одиночки внутри сессии - указать им время жизни Scoped, как в строке кода #35
выделить некий тип (или несколько) для организации сессии, в примере выше это тип Session в строчке кода #24
организовать возможность создания сессий лениво, как в строке кода #26, где для этого используется зависимость типа Func<Session>
Сгенерированный код выглядит примерно так:
partial class Composition: IDisposable { private readonly Composition _root; private readonly object _lock; readonly IDisposable[] _disposables; private int _disposeIndex; private Dependency _scopedDependency; public Composition() { _root = this; _lock = new object(); _disposables = new IDisposable[1]; } internal Composition(Composition baseComposition) { _root = baseComposition._root; _lock = _root._lock; _disposables = new IDisposable[1]; } /// <summary> /// Program composition root /// </summary> public Program ProgramRoot { get { var sessionFactory = new Func<Session>(() => new Session(this)); return new Program(sessionFactory); } } /// <summary> /// Session composition root /// </summary> public IService SessionRoot { get { if (ReferenceEquals(_scopedDependency, null)) { lock (_lock) { if (ReferenceEquals(_scopedDependency, null)) { _scopedDependency = new Dependency(); _disposables[_disposeIndex++] = _scopedDependency; } } } return new Service(_scopedDependency); } } public void Dispose() { lock (_lock) { while (_disposeIndex > 0) { var disposableInstance = _disposables[--_disposeIndex]; try { disposableInstance.Dispose(); } catch(Exception exception) { OnDisposeException(disposableInstance, exception); } } _scopedDependency = null; } } }
Для создания новой сессии используется специальный сгенерированный конструктор композиции в строке кода #16. Утилизация выполняется вызовом метода Dispose() для сессии. Обратите внимание, что сейчас в случае проблем с утилизацией вызывается частичный метод OnDisposeException. Реализовав его тело в частичном классе композиции, можно определить реакцию на исключения, возникающие при утилизации объектов.
Аргументы корня композиции
В версии 2.0 Pure.DI для передачи некоторого состояния объектам внутри композиций можно использовать аргументы конструктора класса, например:
interface IDependency { int Id { get; } } class Dependency(int id) : IDependency { public int Id { get; } = id; } interface IService { string Name { get; } IDependency Dependency { get; } } class Service( [Tag("name")] string name, IDependency dependency) : IService { public string Name { get; } = name; public IDependency Dependency { get; } = dependency; } DI.Setup(nameof(Composition)) .Bind<IDependency>().To<Dependency>() .Bind<IService>().To<Service>() .Root<IService>("Root") // Some kind of identifier .Arg<int>("id") // An argument can be tagged (e.g., tag "name") // to be injectable by type and this tag .Arg<string>("serviceName", "name"); var composition = new Composition(serviceName: "Abc", id: 123); // service = new Service("Abc", new Dependency(123)); var service = composition.Root; service.Name.ShouldBe("Abc"); service.Dependency.Id.ShouldBe(123);
Аргументы конструктора определяются вызовами метода Arg<TArg>(argName) как в строке кода #32. По одному вызову для каждого аргумента. В строке кода #37 при создании объекта композиции его конструктор принимает ранее объявленные аргументы serviceName и id. Их значения могут быть внедрены любым объектам в любой композиции объектов. Они хранятся в приватных неизменяемых полях класса:
partial class Composition { private readonly int _id; private readonly string _serviceName; public Composition(int id, string serviceName) { _id = id; _serviceName = serviceName; } public IService Root { get { return new Service(_serviceName, new Dependency(_id)); } } }
У некоторых пользователей Pure.DI возникла необходимость определять набор аргументов отдельно для каждого корня композиции - аргументы корня композиции. Для этого в новой версии генератора был добавлен метод RootArg<TArg>(string argName), например:
interface IDependency { int Id { get; } public string DependencyName { get; } } class Dependency(int id, string dependencyName) : IDependency { public int Id { get; } = id; public string DependencyName { get; } = dependencyName; } interface IService { string Name { get; } IDependency Dependency { get; } } class Service( [Tag("forService")] string name, IDependency dependency) : IService { public string Name { get; } = name; public IDependency Dependency { get; } = dependency; } DI.Setup(nameof(Composition)) .Bind<IDependency>().To<Dependency>() .Bind<IService>().To<Service>().Root<IService>("CreateService") // Some argument .RootArg<int>("id") .RootArg<string>("dependencyName") // An argument can be tagged (e.g., tag "forService") // to be injectable by type and this tag .RootArg<string>("serviceName", "forService"); var composition = new Composition(); var service = composition.CreateService( serviceName: "Abc", id: 123, dependencyName: "dependency 123"); service.Name.ShouldBe("Abc"); service.Dependency.Id.ShouldBe(123); service.Dependency.DependencyName.ShouldBe("dependency 123");
Обратите внимание, что параметры serviceName и dependencyName имеют одинаковый тип string. Поскольку оба они одинакового типа, для точного определения куда будет внедрен каждый, используются тэги. В реальных задачах это частый случай. В этом примере тег имеет значение "forService" как в строках #40 и #23. Теги позволяют точно определить композицию объектов. Генератор создает примерно такой код:
partial class Composition { public IService CreateService( int id, string dependencyName, string serviceName) { return new Service( serviceName, new Dependency(id, dependencyName)); } }
Важно, что корень композиции под названием CreateService "превратился" из свойства в метод, который содержит минимальный набор аргументов, достаточный для создания композиции объектов.
Новые BCL типы из коробки
В дополнение к таким типам как ICollection<T>, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ISet<T>, Queue<T>, ImmutableArray<T>, Lazy<T>, и многим другим, добавилась поддержка других типов BCL. Теперь для внедрения без дополнительных усилий готовы:
По умолчанию задачи запускаются автоматически, как в этом примере
В этом примере задачи будут стартовать вручную
Типы из пространства имен System.Collections.Concurrent:
Набор типов BCL, готовых для внедрения, зависит от версии .NET проекта. Например, тип ValueTask<T> готов для проектов .NET Core 1+, NET 5+, и для .NET Standard 2.1+. Текущий набор подготовленных для внедрения BCL типов и .NET версий определен в этом файле. Без дополнительных усилий можно внедрять множество или даже большинство других НЕ абстрактных типов, как в этом примере. Если же требуется вручную настроить внедрение для типа: изменить время жизни с Transient на другое или использовать особенный конструктор, или выполнить предварительную инициализацию, то можно следовать примеру ниже для типа Lazy<T>:
.Bind<Lazy<TT>>() .To(ctx => { ctx.Inject<Func<TT>>(ctx.Tag, out var factory); return new Lazy<TT>(factory, true); })
Здесь предписывается использовать специальный конструктор, чтобы определить аргумент isThreadSafe значением true для безопасного одновременного использования несколькими потоками. Теперь каждый раз при создании объекта типа Lazy<T> будет использован специальный конструктор.
Комментарии
В новой версии Pure.DI генерируемый код теперь поддерживает комментарии. Для примера:
class Dependency; class Service(Dependency dependency); // Specifies to create a partial class "Composition" DI.Setup("Composition") // Specifies to create a property "MyService" .Root<Service>("MyService"); var composition = new Composition(); var service = composition.MyService;
сгенерированный код будет содержать комментарии, созданные автоматически по комментариям пользователя из кода выше. Вот как это может выглядеть:
/// <summary> /// <para> /// Specifies to create a partial class "Composition" /// </para> /// <para> /// Composition roots:<br/> /// <list type="table"> /// <listheader> /// <term>Root</term> /// <description>Description</description> /// </listheader> /// <item> /// <term> /// <see cref="Service"/> MyService /// </term> /// <description> /// Specifies to create a property "MyService" /// </description> /// </item> /// </list> /// </para> /// <example> /// This shows how to get an instance of type <see cref="Service"/> using the composition root <see cref="MyService"/>: /// <code> /// var composition = new Composition(); /// var instance = composition.MyService; /// </code> /// </example> /// <a href="https://mermaid.live/view#pako:eNp1UMsOgjAQ_JVmzx4IHFBuYDl68tpL02600bakRRNC-HehSHiIl8nuTmZ2Mi0IKxEyEE_uPVX85rhmjpmwk7PVlfWqVtYQ9oqitBi4YYqLK7q3EkguzXeaqJTOBhQrNBKNaH70MxX2Y8DTnsn06U-CUb74JNfOG8-lW5wHpCOSbdwkX1wG7baPuJwxKVdBe-1uQ3AAjU5zJfvWWwb1HTUyyBhI7h4MOug-cCKKWQ">Class diagram</a><br/> /// This class was created by <a href="https://github.com/DevTeam/Pure.DI">Pure.DI</a> source code generator. /// </summary> /// <seealso cref="Pure.DI.DI.Setup"/> partial class Composition { /// <summary> /// Specifies to create a property "MyService" /// </summary> public Service MyService { get { return new Service(new Dependency()); } } }
В IDE Rider эти комментарии отображаются так:


Комментарий для класса содержит список корней композиции с их типом и описанием, а также базовый пример использования этого класса. Помимо этого, в комментарии есть ссылка на диаграмму классов. Если перейти по ней, можно увидеть, что-то похожее:

Диаграмма класса может быть полезной для понимания композиции объектов.
Примеры
Для того, чтобы Pure.DI было быстрее и легче интегрировать в свои проекты, добавлены примеры его применения в разных видах .NET приложений. На данный момент вот их полный список:
Console
UI
Web
Git repo with examples


Спасибо что дочитали до конца ))
На последок отмечу, что помимо упомянутых выше улучшений, есть еще много мелких, с которыми можно ознакомиться в истории релизов.
Пополнился и набор подсказок, позволяющих тонко подстроить генерацию кода под свои нужды. Их полный список можно найти в разделе Генерация кода.
