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<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. Теперь для внедрения без дополнительных усилий готовы:

Набор типов 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 эти комментарии отображаются так:

Комментарий к корню композиции MyService

Комментарий к корню композиции MyService
Комментарий к классу Composition

Комментарий к классу Composition

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

Пример диаграммы классов

Пример диаграммы классов

Диаграмма класса может быть полезной для понимания композиции объектов.

Примеры

Для того, чтобы Pure.DI было быстрее и легче интегрировать в свои проекты, добавлены примеры его применения в разных видах .NET приложений. На данный момент вот их полный список:

Пример Avalonia

Пример Avalonia
Пример Blazor WebAssembly

Пример Blazor WebAssembly

Спасибо что дочитали до конца ))

На последок отмечу, что помимо упомянутых выше улучшений, есть еще много мелких, с которыми можно ознакомиться в истории релизов.

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


ссылка на оригинал статьи https://habr.com/ru/articles/795809/


Комментарии

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

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