Новое в Pure.DI: union types, генерация интерфейсов и DI без аллокаций

от автора

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:


Часть 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);

Возможности не ограничиваются простым зеркалированием:

Генерация интерфейса из класса


Привязки атрибутами в реализациях

В версии 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>).

BindAttribute на реализации

Группы 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: члены как источники зависимостей

Export с lifetime и тегом


Кастомные 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 внутри одной группы атрибутов диагностируется как ошибка.

Кастомный binding-атрибут


Скоупы: 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.

Builders и TryBuildUp


Готовность к 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;    }}

Partial constructor injection

Кроме того, при выборе конструктора 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, о нём ниже.

Span и ReadOnlySpan


Фабрики для 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) { /* ... */ }}

Default Func с ReadOnlySpan

Во‑вторых, поддерживаются собственные 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 типы.

Allows ref struct factory


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. Типичные сценарии: парсеры, роутеры, декодеры протоколов, конвейеры валидации.

Method injection для hot path


Диагностики для 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 выдаёт свою диагностику с объяснением и ссылкой на справку.

Справочник диагностик 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 => /* ... */;}

Non-boxing union result


Каталог 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/