Pure.DI v2.1
С момента выхода генератора исходного кода 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().As(Lifetime.PerResolve).To()
.Root("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
DI.Setup(nameof(Composition))
// PerResolve -> PerBlock
.Bind().As(Lifetime.PerBlock).To()
.Root("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 sessionFactory)
{
public Session CreateSession() => sessionFactory();
}
partial class Composition
{
private static void Setup() =>
DI.Setup(nameof(Composition))
.Bind().As(Scoped).To()
.Bind().To()
// Session composition root
.Root("SessionRoot")
// Program composition root
.Root("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
Сгенерированный код выглядит примерно так:
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];
}
///
/// Program composition root
///
public Program ProgramRoot
{
get
{
var sessionFactory = new Func(() => new Session(this));
return new Program(sessionFactory);
}
}
///
/// Session composition root
///
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().To()
.Bind().To()
.Root("Root")
// Some kind of identifier
.Arg("id")
// An argument can be tagged (e.g., tag "name")
// to be injectable by type and this tag
.Arg("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
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
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().To()
.Bind().To().Root("CreateService")
// Some argument
.RootArg("id")
.RootArg("dependencyName")
// An argument can be tagged (e.g., tag "forService")
// to be injectable by type and this tag
.RootArg("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
Набор типов BCL, готовых для внедрения, зависит от версии .NET проекта. Например, тип ValueTask
.Bind>()
.To(ctx =>
{
ctx.Inject>(ctx.Tag, out var factory);
return new Lazy(factory, true);
})
Здесь предписывается использовать специальный конструктор, чтобы определить аргумент isThreadSafe значением true для безопасного одновременного использования несколькими потоками. Теперь каждый раз при создании объекта типа Lazy
Комментарии
В новой версии 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("MyService");
var composition = new Composition();
var service = composition.MyService;
сгенерированный код будет содержать комментарии, созданные автоматически по комментариям пользователя из кода выше. Вот как это может выглядеть:
///
///
/// Specifies to create a partial class "Composition"
///
///
/// Composition roots:
///
///
/// Root
/// Description
///
/// -
///
/// MyService
///
///
/// Specifies to create a property "MyService"
///
///
///
///
///
/// This shows how to get an instance of type using the composition root :
///
/// var composition = new Composition();
/// var instance = composition.MyService;
///
///
/// Class diagram
/// This class was created by Pure.DI source code generator.
///
///
partial class Composition
{
///
/// Specifies to create a property "MyService"
///
public Service MyService
{
get
{
return new Service(new Dependency());
}
}
}
В IDE Rider эти комментарии отображаются так:
Комментарий к корню композиции MyService
Комментарий к классу Composition
Комментарий для класса содержит список корней композиции с их типом и описанием, а также базовый пример использования этого класса. Помимо этого, в комментарии есть ссылка на диаграмму классов. Если перейти по ней, можно увидеть, что-то похожее:
Пример диаграммы классов
Диаграмма класса может быть полезной для понимания композиции объектов.
Примеры
Для того, чтобы Pure.DI было быстрее и легче интегрировать в свои проекты, добавлены примеры его применения в разных видах .NET приложений. На данный момент вот их полный список:
Console
UI
Web
Git repo with examples
Спасибо что дочитали до конца))
На последок отмечу, что помимо упомянутых выше улучшений, есть еще много мелких, с которыми можно ознакомиться в истории релизов.
Пополнился и набор подсказок, позволяющих тонко подстроить генерацию кода под свои нужды. Их полный список можно найти в разделе Генерация кода.