Pure.DI — это генератор кода для внедрения зависимостей (Dependency Injection), который работает на этапе компиляции. Pure.DI развивает идею «чистого DI»: вместо контейнера и рефлексии вы получаете обычный C#‑код, который создаёт композиции объектов. В этой статье — новые возможности из релизов 2.3.5–2.5.1: от union types в роли DI‑контрактов до сценариев внедрения зависимостей без лишних аллокаций.
Ключевые преимущества Pure.DI:
-
Zero-Overhead: генерируемый код создания композиций объектов не отличается от ручного
-
Проверка на этапе компиляции: ошибки внедрения, циклические зависимости, недостающие привязки и все другие ошибки обнаруживаются на этапе компиляции
-
Работает везде: от .NET Framework 2.0 до последних версий .NET, Unity, Native AOT и других платформ
-
Прозрачность: вы всегда можете посмотреть сгенерированный код, отладить его и понять, как он работает
Со времени предыдущего поста вышло шесть релизов, и статья делится на две главные части.
Часть 1. Новые возможности:
-
union types как DI‑контракты
-
генерация интерфейсов из классов (
[GenerateInterface]) -
привязки атрибутами в реализациях (
[Bind],[Type],[Tag],[Lifetime]) -
[Export]: члены классов как источники зависимостей -
кастомные binding‑атрибуты
-
скоупы:
SetupScope, скоуп на сцену Unity,CreateScope()для Microsoft DI -
полная поддержка nullable reference types
-
TryBuildUp— безопасная «достройка» объектов -
готовность к C# 14: partial‑конструкторы и
OverloadResolutionPriority
Часть 2. Производительность — DI на hot paths:
-
Span<T>,ReadOnlySpan<T>и ref struct‑зависимости -
фабрики для stack-only значений (
allows ref struct) -
zero-copy парсинг и method injection на hot path
-
диагностики для stack-only
-
non-boxing union results
-
каталог high-performance примеров: ArrayPool, пулы объектов, фабрики без замыканий,
ValueTask<T>‑корни,ThreadSafe = Off -
оптимизация генерации кода
Другие статьи на тему Pure.DI:
-
Pure.DI помогает сделать DI чистым — рекомендуется для понимания идеи
Часть 1. Новые возможности
Union types как DI-контракты
В preview‑версиях C# появились union types — тип, значением которого может быть один из нескольких заранее известных case‑типов. Pure.DI научился использовать union как DI‑контракт: case‑реализация неявно конвертируется в union, и генератор строит весь граф зависимостей выбранного case за union‑контрактом. Главное отличие от интерфейса — case‑типам не нужна общая абстракция: это могут быть даже классы из разных сторонних SDK, которые вы не можете менять.
DI.Setup(nameof(Composition)) .Bind<IReceiptService>().To<ReceiptService>() // Композиция решает, какой платёжный шлюз // стоит за union-контрактом .Bind<PaymentGateway>().To<StripeGateway>() .Root<CheckoutService>("Checkout");// Case-типы — независимые классы без общего интерфейса,// у каждого свои зависимостиclass StripeGateway(StripeApiClient api){ public string Charge(int amountInCents) => api.Post($"charge {amountInCents}");}class BankGateway(BankAccount account){ public string Transfer(int amountInCents) => $"bank transfer {amountInCents} from {account.Iban}";}// Union-тип: case-типы неявно конвертируются в union,// и Pure.DI использует эту конверсиюunion PaymentGateway(StripeGateway, BankGateway);// Сервис зависит от union и обрабатывает каждый case явноclass CheckoutService(PaymentGateway gateway, IReceiptService receipts){ public string Pay(int amountInCents) { var confirmation = gateway switch { StripeGateway stripe => stripe.Charge(amountInCents), BankGateway bank => bank.Transfer(amountInCents), _ => throw new InvalidOperationException("Unknown payment gateway.") }; return receipts.Print(confirmation); }}
Смена провайдера — однострочное изменение: .Bind<PaymentGateway>().To<BankGateway>(). Что важно знать:
-
если явной union‑привязки нет, но ровно одна зарегистрированная привязка конвертируется в union — Pure.DI разрешит union через этот единственный case автоматически
-
если применимых case‑привязок несколько — вы получите ошибку
DIE050со списком кандидатов и указанием мест привязок -
union‑типы не участвуют в автопривязках — случайно создать «пустой» union не получится
-
case‑привязка остаётся владельцем своего lifetime и disposal:
Singleton,Scoped,PerResolveработают как обычно -
коллекции case‑ов собираются через
Tag.Unique: можно запроситьIEnumerable<PaymentGateway>, массив,ReadOnlySpan<>и т. д.
Union types: case-реализации за union-контрактом
Поддержка union types требует preview‑версии языка и .NET 11 Preview 5 или новее, но попробовать её можно уже сейчас.
Генерация интерфейсов из классов
Классическая пара «класс + зеркальный интерфейс» — самый скучный boilerplate в DI‑проектах. Теперь интерфейс можно сгенерировать из реализации: достаточно объявить пустой partial interface и пометить класс атрибутом [GenerateInterface]. Отдельное спасибо Adam Hathcock за дизайн и реализацию этой возможности!
DI.Setup(nameof(Composition)) .Bind().To<EmailSender>() .Root<App>(nameof(App));// Пустое объявление — тело интерфейса сгенерирует Pure.DIpublic partial interface IEmailSender;[GenerateInterface]public class EmailSender : IEmailSender{ public string Provider => "smtp"; public string Send(string address) => $"sent:{address}";}// Потребитель зависит от абстракции, которую никто не писал рукамиpublic class App(IEmailSender sender);
Возможности не ограничиваются простым зеркалированием:
-
имя, namespace и уровень доступа интерфейса настраиваются через именованные аргументы атрибута
-
из одного класса можно сгенерировать несколько интерфейсов, распределяя члены по контрактам атрибутами
-
атрибут
[IgnoreInterface]исключает члены из контракта -
generic-члены, nullable-аннотации и события сохраняются, как и XML‑комментарии документации
Генерация интерфейса из класса
Привязки атрибутами в реализациях
В версии 2.5.0 атрибутная модель привязок была переработана (это breaking change, о миграции — ниже). Теперь [Bind] можно объявлять прямо на типе реализации и указывать контракт, время жизни и тег. Реализации могут сами описывать свои привязки, и настройка композиции становится компактнее:
DI.Setup(nameof(Composition)) // В setup — только корень, привязка задана атрибутом .Root<IMessageWriter>("Writer", "console");[Bind(typeof(IMessageWriter), Lifetime.Singleton, "console")]class ConsoleMessageWriter : IMessageWriter{ public void Write(string message) => Console.WriteLine(message);}
Атрибуты внутри одной группы [...] сливаются в одну привязку, а отдельные группы создают независимые привязки. Это удобно, когда одна реализация играет несколько ролей с разными контрактами, lifetime и тегами:
// Группа привязки #1: участник пайплайна оплаты[Bind(typeof(IPaymentProcessor), Lifetime.Singleton, "audit")]// Группа привязки #2: тот же адаптер как sink аудита со своим lifetime[Bind(typeof(IPaymentAuditSink), Lifetime.Transient, "operations")]class PaymentAuditAdapter(IEventStore eventStore) : IPaymentProcessor, IPaymentAuditSink;
Кроме [Bind], работают и «точечные» атрибуты [Type], [Tag] и новый [Lifetime] — их можно комбинировать. Для generic‑реализаций поддерживаются маркерные контракты вида typeof(IBox<TT>).
Группы Bind-атрибутов: несколько ролей одного адаптера
[Export]: члены классов как источники зависимостей
Раньше источник зависимости задавал member-level атрибут [Bind]. Теперь эта роль у нового атрибута [Export] — семантика стала однозначной: [Bind] — про привязки, [Export] — про источники зависимостей:
DI.Setup(nameof(Composition)) .Bind().As(Lifetime.Singleton).To<DeviceFeatureProvider>() .Bind().To<PhotoService>() .Root<IPhotoService>("PhotoService");class DeviceFeatureProvider{ // Свойство — источник зависимости [Export] public IGps Gps { get; } = new Gps(); [Export] public ICamera Camera { get; } = new Camera();}class PhotoService(IGps gps, Func<ICamera> cameraFactory) : IPhotoService;
[Export] принимает необязательные lifetime и tags, так что экспортированный член ведёт себя как источник привязки:
class GraphicsAdapter{ [Export(lifetime: Lifetime.Singleton, tags: ["HighPerformance"])] public IGpu HighPerfGpu { get; } = new DiscreteGpu();}class RayTracer([Tag("HighPerformance")] IGpu gpu) : IRenderer;
Миграция с 2.4.x проста: member-level [Bind] заменяется на [Export], а RootKinds.Exposed — на RootKinds.Exported.
ExportAttribute: члены как источники зависимостей
Кастомные binding-атрибуты
Если не хочется зависеть от Pure.DI в сборках с реализациями — объявите собственный атрибут и зарегистрируйте его в setup. Методы TypeAttribute<T>(), TagAttribute<T>() и новый LifetimeAttribute<T>() объясняют генератору, из каких аргументов конструктора атрибута читать контракт, тег и время жизни:
DI.Setup(nameof(Composition)) // Регистрируем свой атрибут как источник binding-метаданных .TypeAttribute<ServiceAttribute<TT>>() .LifetimeAttribute<ServiceAttribute<TT>>() .TagAttribute<ServiceAttribute<TT>>(1) .Root<IMessageWriter>("Writer", "console");// Атрибут может жить в отдельной сборке без ссылки на Pure.DI[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]class ServiceAttribute<T> : Attribute{ public ServiceAttribute( Lifetime lifetime = Lifetime.Transient, object? tag = null) { }}[Service<IMessageWriter>(Lifetime.Singleton, "console")]class ConsoleMessageWriter : IMessageWriter;
Кастомные атрибуты участвуют в тех же правилах слияния групп, что и встроенные Bind, Type, Tag, Lifetime. Дублирование lifetime внутри одной группы атрибутов диагностируется как ошибка.
Скоупы: SetupScope
Классическая задача — время жизни «на запрос/операцию»: контекст запроса, unit of work, телеметрия. Новый генерируемый метод (имя задаётся хинтом ScopeMethodName) привязывает новый экземпляр композиции к родительскому скоупу: Scoped‑экземпляры уникальны внутри скоупа и освобождаются вместе с ним (обычный Singleton же остаётся общим для всех).
var composition = new Composition();// Запрос #1: свой scope, свой RequestContextusing (var request = Composition.SetupScope(composition, new Composition())){ var checkout = request.RequestRoot; // В пределах scope RequestContext один и тот же, // по окончании — будет вызван Dispose}partial class Composition{ static void Setup() => DI.Setup() .Hint(Hint.ScopeMethodName, "SetupScope") // Время жизни «на запрос» .Bind().As(Scoped).To<RequestContext>() .Bind().As(Singleton).To<IdGenerator>() .Bind().To<CheckoutService>() .Root<ICheckoutService>("RequestRoot");}
Имя метода подбирается под домен: CreateScope, BeginRequest, OpenSession — что улучшает читаемость кода. Скоупы могут создаваться и фабричными методами, а родительский и дочерний скоупы валидируются на различие — случайное «вложение в себя» исключено.
Scope setup method: скоуп на запрос без класса-обёртки
Скоупы на платформах: сцены Unity и Microsoft DI
Та же модель скоупов естественно ложится на платформенные сценарии. В Unity каждая загруженная сцена получает собственный скоуп: Scoped сервисы разделяются внутри сцены и изолированы между сценами, Singleton — общие на всё приложение. При этом MonoBehaviour-объекты создаёт сам Unity, а Pure.DI лишь достраивает их через builders:
public partial class Scope : MonoBehaviour{ [SerializeField] ClockConfig clockConfig; void Setup() => DI.Setup() .Hint(Hint.ScopeMethodName, "SetupScope") .Bind().To(() => clockConfig) .Bind<IClockService>().As(Singleton).To<ClockService>() .Bind<IClockSession>().As(Scoped).To<ClockSession>() // Unity создаёт MonoBehaviour, Pure.DI достраивает .Builders<MonoBehaviour>();}
Unity scene scopes: скоуп на каждую сцену
А для интеграции с Microsoft.Extensions.DependencyInjection композиция теперь поддерживает scoped времена жизни “из коробки”: класс композиции реализует IServiceScopeFactory/IServiceScope, и composition.CreateScope() работает в точной семантике MS DI — включая «тегирование» через keyed services.
Service provider со скоупами в семантике MS DI
Nullable reference types на всём пути
Версия 2.4.0 принесла полную поддержку nullable reference types — аннотации сохраняются при чтении контрактов, построении графа и генерации кода. Это breaking change: код, полагавшийся на автоматические null‑проверки для nullable‑аргументов, может потребовать корректировки.
DI.Setup(nameof(Composition)) .Bind<IDatabase>().To<Database>() .Bind<IReportService>().To<ReportService>() // Nullable-аргументы больше не получают null-проверок: // null проходит внутрь, как и задумано .Arg<string?>("defaultTitle", "title") .RootArg<string?>("connectionString", "connection") .Root<IReportService>("CreateReportService");var composition = new Composition(defaultTitle: null);var reportService = composition.CreateReportService(connectionString: null);class ReportService( [Tag("title")] string? defaultTitle, [Tag("connection")] string? connectionString, IDatabase? optionalDatabase) : IReportService;
На что это влияет:
-
non-null привязка может удовлетворить nullable‑зависимость — удобно для опциональных параметров конструктора, nullable‑результатов фабрик и элементов коллекций
-
T?означает «потребитель умеет обрабатывать null», но не отменяет ошибку графа при отсутствующей привязке — вся строгость проверок сохраняется -
для generic‑контрактов с nullable‑аргументами предпочитайте
where T : class?вместоwhere T : class, чтобы не получать предупреждений от компилятора -
новые предупреждения подсвечивают неоднозначные nullable‑корни в методах
Resolve
Nullable reference types: примеры и рекомендации
TryBuildUp: безопасная достройка объектов
Builders «достраивают» объекты, созданные внешней системой — UI‑фреймворком, сериализатором, Unity. Строгий BuildUp бросает ArgumentException, если runtime‑подтип неизвестен композиции. Сейчас вместе с BuildUp генерируется безопасный TryBuildUp, который не бросает исключения:
DI.Setup(nameof(Composition)) .Bind().To(Guid.NewGuid) .Bind().To<PlutoniumBattery>() // Builder для каждого типа, унаследованного от IRobot .Builders<IRobot>("BuildUp", filter: "*Bot");var composition = new Composition();// Известный тип — достройка выполняетсяvar cleaner = composition.BuildUp(new CleanerBot());// Неизвестный подтип — graceful fallback вместо исключенияvar externalRobot = new ExternalRobot();if (!composition.TryBuildUp(externalRobot)){ // объект остался нетронутым, обрабатываем ситуацию сами}
Это особенно удобно, когда объекты приходят извне и их фактический тип известен только в рантайме: десериализация, плагины, объекты сцены Unity.
Готовность к C# 14: partial-конструкторы и OverloadResolutionPriority
C# 14 позволяет разделить конструктор на объявление (контракт) и тело — например, чтобы контракт с DI‑атрибутами жил в одной части partial‑класса, а логика в другой (в том числе сгенерированной). Pure.DI видит объединённый конструктор и работает с ним как с обычным:
partial class AuditSink : AuditSinkBase{ // Определяющее объявление участвует в выборе конструктора, // атрибуты параметров объединяются с телом конструктора public partial AuditSink( [Tag("durable")] IAuditTransport transport, AuditSinkOptions options);}partial class AuditSink{ // Инициализатор base(...)/this(...) public partial AuditSink( IAuditTransport transport, AuditSinkOptions options) : base(transport) { BatchSize = options.BatchSize; }}
Кроме того, при выборе конструктора Pure.DI теперь учитывает OverloadResolutionPriorityAttribute — атрибут, которым вы указываете компилятору предпочтительную для DI перегрузку конструктора:
[method: OverloadResolutionPriority(1)]class BillingApiClient(ResilientHttpOptions options){ // Legacy-конструктор сохранён для старых вызывающих, // но новый код (включая сгенерированный) предпочтёт primary public BillingApiClient(LegacyHttpOptions options) : this(new ResilientHttpOptions()) { }}
Приоритеты работают как в C#: больше — предпочтительнее, неаннотированные конструкторы имеют приоритет 0, отрицательные значения понижают приоритет. [Ordinal] остаётся явным и более сильным механизмом Pure.DI, а предупреждение DIW014 подскажет, если возникает неоднозначность из‑за приоритетов.
OverloadResolutionPriority при выборе конструктора
Часть 2. Производительность: DI на hot paths
«Zero-overhead» в Pure.DI всегда означал «код, написанный так, как вы бы писали его сами». Релиз 2.5.1 идёт дальше: внедрение зависимостей теперь работает там, где классические DI были не у дел — в стек‑ориентированном коде с Span<T>, ref struct и без единой лишней аллокации. И, в духе Pure.DI, все опасные сценарии проверяются на этапе компиляции.
Span и ReadOnlySpan как зависимости
Span внедряется так же, как массивы T[] — для немедленного использования в конструкторе или методе. Для value‑типов Pure.DI генерирует stackalloc: коллекция зависимостей собирается вообще без heap‑аллокации.
DI.Setup(nameof(Composition)) .Bind<Point>('a').To(() => new Point(1, 1)) .Bind<Point>('b').To(() => new Point(2, 2)) .Bind<Point>('c').To(() => new Point(3, 3)) .Bind<IPath>().To<Path>() .Root<IPath>("Path");readonly struct Point(int x, int y);class Path(ReadOnlySpan<Point> points) : IPath{ // Спан размещён на стеке — очень дёшево. // Сохранить его в поле нельзя (ref struct), // но обработать в конструкторе — пожалуйста public int PointCount { get; } = points.Length;}
Внедрение ref struct в конструктор heap‑типа доступно для совместимости, но сопровождается предупреждением DIW012 — предпочтительный путь для stack-only значений — это method injection, о нём ниже.
Фабрики для stack-only значений
Как передать ReadOnlySpan<char> в фабрику? Func<TArg, T> тут не поможет — ref struct нельзя использовать как generic‑аргумент обычного делегата. Pure.DI закрывает это с двух сторон. Во‑первых, «из коробки» доступен безопасный default‑биндинг Func<ReadOnlySpan<char>, T>:
DI.Setup(nameof(Composition)) .Root<Func<ReadOnlySpan<char>, Parser>>("ParserFactory");var composition = new Composition();var parser = composition.ParserFactory("Hello".AsSpan());class Parser{ [Ordinal] public void Initialize(ReadOnlySpan<char> text) { /* ... */ }}
Во‑вторых, поддерживаются собственные generic‑делегаты с where T : allows ref struct — stack-only значение прокидывается через ctx.Override(...) и потребляется немедленно, а в потокобезопасном контексте override и внедрение держатся под lock (ctx.Lock):
delegate bool ParserFactory<in T>(T text) where T : allows ref struct;DI.Setup(nameof(Composition)) .Bind<ParserFactory<ReadOnlySpan<char>>>().To(ctx => new ParserFactory<ReadOnlySpan<char>>(text => { lock (ctx.Lock) { ctx.Override<ReadOnlySpan<char>>(text); ctx.Inject<Parser<ReadOnlySpan<char>>>(out var parser); return parser.Initialized; } })) .Root<ParserFactory<ReadOnlySpan<char>>>("ParserFactory");
allows ref struct работает также в generic‑корнях и root‑аргументах, а fluent API (Bind<T>(), Root<T>(), Transient<T>() и т. д.) принимает ref-like типы.
Zero-copy парсинг и method injection на hot path
Когда стабильные зависимости сервиса живут долго, но есть данные, которые меняются на каждый вызов, per-call значение передаётся root‑аргументом и потребляется сразу в методе внедрения. Бонус C# 14: root‑аргумент byte[] может удовлетворить контракту внедрения типа ReadOnlySpan<byte> — Pure.DI применит span‑конверсию типа, предоставляемую компилятором, и парсер прочитает caller-owned массив без единого копирования:
DI.Setup(nameof(Composition)) .Bind<IMessageRegistry>().To<MessageRegistry>() .Bind<IPacketHandler>().To<PacketHandler>() .RootArg<byte[]>("frame") .Root<IPacketHandler>("Handle");var composition = new Composition();// Массив остаётся во владении вызывающего:// после вызова его можно вернуть в пулvar packet = composition.Handle(frame);sealed class PacketHandler(IMessageRegistry registry) : IPacketHandler{ [Ordinal(0)] public void Decode(ReadOnlySpan<byte> frame) { var messageId = BinaryPrimitives.ReadUInt16BigEndian(frame); // zero-copy разбор пакета... }}
Zero-copy парсинг сетевых пакетов
Если аргумент сам stack-only (.RootArg<ReadOnlySpan<char>>("path")), сгенерированный корень (у нас метод) получает сигнатуру с модификатором scoped — компилятор гарантирует, что значение не переживёт текущий stack frame. Типичные сценарии: парсеры, роутеры, декодеры протоколов, конвейеры валидации.
Диагностики для stack-only
Работа со стеком — место, где легко получить хитрую ошибку. Pure.DI переносит эти ошибки на этап компиляции — добавлен целый пакет диагностик:
-
DIE046— stack-only зависимость нельзя использовать с «хранимыми» lifetime (Singleton, Scoped и т. п.) -
DIE047— stack-only зависимость нельзя внедрить в поле или свойство -
DIE048— stack-only реализацию нельзя внедрить через конверсию в интерфейс -
DIE049— stack-only зависимость нельзя захватить в сгенерированных делегатах и отложенных фабриках -
DIW012— предупреждение о внедрении stack-only зависимости в конструктор heap‑типа -
DIW013— предупреждение о несинхронизированном override stack-only значения в потокобезопасном контексте
Заметьте: там, где раньше вы получили бы малопонятную ошибку компилятора C# (вроде CS9244), Pure.DI выдаёт свою диагностику с объяснением и ссылкой на справку.
Non-boxing union results
Возвращаемся к union types — теперь со стороны производительности. Компактный union хранит значение как object, то есть боксит value‑типы. Для горячих путей поддерживаются кастомные union‑результаты, которые хранят value-type case в отдельном поле — без боксинга, рефлексии и обёрток:
DI.Setup(nameof(Composition)) // CacheHit — value-type case внутри CacheLookupResult .Bind<CacheLookupResult>().To<CacheHit>() .Root<CacheLookupResult>("CachedProduct");var result = new Composition().CachedProduct;// Allocation-free доступ к результатуif (result.TryGetValue(out CacheHit hit)) { /* ... */ }readonly record struct CacheHit(int ProductId, decimal Price);// Кастомный union хранит value-type case напрямую[System.Runtime.CompilerServices.Union]readonly struct CacheLookupResult : System.Runtime.CompilerServices.IUnion{ private readonly byte _kind; private readonly CacheHit _hit; private readonly CacheMiss _miss; public bool TryGetValue(out CacheHit value) { value = _hit; return _kind == 1; } // Value — обязательный общий fallback (боксит), // используйте его только для диагностики public object? Value => /* ... */;}
Каталог high-performance примеров
Вместе с новыми возможностями появилась целая серия примеров «DI на hot path»:
-
ArrayPool-буферы —
ArrayPool<T>поддерживается из коробки: запрашивайтеArrayPool<byte>как обычную зависимость, а возврат буфера в пул привязывайте к dispose владельца черезOwned<T> -
Пул объектов — Singleton‑пул «тёплых» объектов плюс короткоживущий lease на каждый вызов корня: переиспользование явное, утечек за пределы скоупа нет
-
Фабрики без захвата замыканий — зависимости объявляются параметрами лямбды (
.To((CurrencyFormatter currency, TaxPolicy tax) => ...)), а не захватываются из окружения — код фабрики детерминирован и allocation-friendly -
Struct-зависимости — композиция без heap‑аллокаций
-
ValueTask-корни — корень вида
Root<ValueTask<IFeatureSnapshot>>без аллокацииTask<T>для синхронного случая -
ThreadSafe = Off — если композиция создаётся и используется на одном потоке (CLI‑утилита, фаза инициализации game loop), хинт
.Hint(Hint.ThreadSafe, "Off")убирает сгенерированную синхронизацию полностью
Оптимизация генерации кода
-
поле
_lockтеперь создаётся только тогда, когда на него действительно ссылается хотя бы одинlock (...)— раньше оно создавалось «на всякий случай» для любой композиции, расходуя память в каждом экземпляре -
весь сгенерированный код помечается
[GeneratedCode]— анализаторы, coverage и code-style инструменты корректно распознают и пропускают его; в сгенерированный класс встраивается фактическая версия пакета Pure.DI -
построение графа и генерация серьёзно ускорены (кэширование, предпочтение синтаксического анализа семантическому, меньше повторных попыток построения графа) — это значительно влияет на время компиляции
Полезные мелочи
Короткой строкой о том, что не тянет на отдельный раздел, но пригодится:
Новые свойства контекста фабрик. К ctx.Tag, ctx.ConsumerType и ctx.Lock добавились ctx.RootName, ctx.RootType и ctx.IsLockRequired:
// Логгер знает, через какой корень его строят.Bind().To(ctx => new Logger(ctx.RootName))// Блокировка — только когда она действительно нужна.Bind().To(ctx =>{ if (ctx.IsLockRequired) { lock (ctx.Lock) { /* ... */ } } // ...})
Внедрение словарей. Зависимость IReadOnlyDictionary<TKey, TValue> собирается автоматически из привязок KeyValuePair<TKey, TValue> с Tag.Unique — удобно для выбора реализации по ключу в рантайме:
.Bind(Tag.Unique).To((EmailChannel c) => new KeyValuePair<Channel, INotificationChannel>(Channel.Email, c)).Bind(Tag.Unique).To((SmsChannel c) => new KeyValuePair<Channel, INotificationChannel>(Channel.Sms, c))class NotificationService(IReadOnlyDictionary<Channel, INotificationChannel> channels);
Dictionary: выбор зависимости по ключу
Больше BCL-типов из коробки. TimeProvider, TaskCompletionSource<T>, CultureInfo, IFormatProvider, StringComparer, IReadOnlySet<T>, RandomNumberGenerator и другие внедряются без дополнительных усилий — а при необходимости default‑привязку можно переопределить.
Фабрики до 16 параметров, обновление Roslyn до 5.6.
Пример на Uno Platform. Кроссплатформенное приложение на single-project Uno SDK: композиция как XAML‑ресурс, virtual‑корни для view model, DesignTimeComposition с override‑корнями для дизайнера — и всё это без runtime‑контейнера. Подробное описание примера.
Вместо заключения
Если есть желание попробовать новые возможности, можно начать с любого примера — они независимы и запускаются легко через dotnet run. А если чего-то не хватает — не стесняйтесь создать тикет в репозитории Pure.DI на GitHub.
Спасибо за интерес и что дочитали до конца!
ссылка на оригинал статьи https://habr.com/ru/articles/1060246/