Pure.DI — это генератор кода для внедрения зависимостей (Dependency Injection), который работает на этапе компиляции. Pure.DI развивает идею «чистого DI»: вместо контейнера и рефлексии вы получаете обычный C#‑код, который создаёт композиции объектов. В этой статье — новые возможности, которые упрощают настройку композиций, делают корни гибче, а диагностику — понятнее.
Ключевые преимущества Pure.DI:
Zero-Overhead: генерируемый код создания композиций объектов не отличается от ручного
Проверка на этапе компиляции: ошибки внедрения, циклические зависимости, недостающие привязки и все другие ошибки обнаруживаются на этапе компиляции
Работает везде: от .NET Framework 2.0 до последних версий .NET, Unity, Native AOT и других платформ
Прозрачность: вы всегда можете посмотреть сгенерированный код, отладить его и понять, как он работает
Со времени выхода предыдущего поста Pure.DI получил улучшения, которые делают настройку композиций ещё более гибкой и выразительной.
О чём статья:
контекст настройки для зависимых композиций (setup context)
управление глубиной переопределения зависимостей в фабриках
BuildUp/Builders для «достройки» существующих объектов
подсказки (hints) для контроля auto-binding и конструкторов по умолчанию
облегчённые корни (light roots) и оптимизированные анонимные корни
lifetime по умолчанию (включая auto-bindings), упрощённые lifetime-методы
SpecialType для упрощённых привязок (в т. ч. для Unity)
Tag.Any для гибких тегированных зависимостей
ref/out‑зависимости и сценарии с ref‑structконтекстно‑зависимые фабрики (
ctx.ConsumerType) и потокобезопасные фабрики (ctx.Lock)новые варианты корней (
virtual/override) для наследования композицийболее выразительная диагностика: ID ошибок/предупреждений, ссылки на справку, локализация
Другие статьи на тему Pure.DI:
Pure.DI помогает сделать DI чистым - рекомендуется для понимани идеи
Setup context для зависимых композиций
Когда одна настройка DI зависит от другой и ей могут быть нужны элементы базовой настройки (например, поля, инициализированные Unity или внешней инфраструктурой), можно передать контекст настройки явно. Это делает зависимость прозрачной и убирает «магические» ссылки на состояние.
// Создаём базовую композицию с настройками для продакшена var baseContext = new BaseComposition { Settings = new AppSettings("prod") }; // Передаём контекст базовой композиции в зависимую var composition = new Composition(baseContext); // Получаем сервис, который использует настройки из базовой композиции var service = composition.Service; internal partial class BaseComposition { internal AppSettings Settings { get; set; } = new("dev"); private void Setup() => DI.Setup(nameof(BaseComposition)) .Bind<IAppSettings>().To(_ => Settings); } internal partial class Composition { private void Setup() => DI.Setup(nameof(Composition)) // Указываем зависимость от базовой композиции с передачей аргумента .DependsOn(nameof(BaseComposition), SetupContextKind.Argument, "baseContext") .Bind<IService>().To<Service>() .Root<IService>("Service"); } record AppSettings(string Environment) : IAppSettings; interface IAppSettings { string Environment { get; } } interface IService { } class Service(IAppSettings settings) : IService;
Зависимые композиции с передачей контекста
Кроме передачи с помощью аргумента, контекст можно «подмешивать» через members / аксессоры и использовать root‑argument сценарии — это удобно, когда базовая настройка живёт как объект, а зависимая композиция должна оставаться чистой и тестируемой.
Наследование композиций: переиспользование инфраструктуры без дублирования
Если у вас есть общий слой инфраструктуры (БД, логирование, кэш), его удобно вынести в базовую композицию и наследовать в конкретных композициях. Так вы переиспользуете привязки и при этом оставляете «верхнюю» композицию маленькой и предметной.
// Базовый класс с общей инфраструктурой class Infrastructure { private static void Setup() => DI.Setup(kind: CompositionKind.Internal) .Bind<IDatabase>().To<SqlDatabase>(); } // Конкретная композиция наследует инфраструктуру partial class Composition : Infrastructure { private void Setup() => DI.Setup() .Bind<IUserManager>().To<UserManager>() .Root<App>(nameof(App)); }
Наследование композиций: общий Setup и привязки в базовом классе
В паре с virtual/override корнями композиции это даёт удобную модель «продакшен‑композиция + тестовая надстройка» без внедрения runtime‑контейнеров.
Управление глубиной переопределения зависимостей
В фабриках часто нужно временно переопределить зависимость (например, requestId/tenantId) — но не всегда хочется, чтобы override «протёк» во все вложенные зависимости. Для этого есть два режима:
ctx.Override(...)— переопределение распространяется максимально глубоко по графу зависимостейctx.Let(...)— переопределение действует только на текущий уровень внедрения
.Bind().To<Service>(ctx => { ctx.Let(42); // только для Service(...) ctx.Inject(out Service service); return service; })
Глубина override (Override vs Let) на реальном примере
Это особенно полезно в сервисах, где один и тот же примитив (например, int id) встречается и в корневом конструкторе, и внутри нескольких зависимостей: можно управлять тем, что именно вы хотите подменить.
BuildUp/Builders для достройки объектов
Иногда объект создаёт внешняя система (UI‑фреймворк, сериализатор, Unity, ORM, фабрика ...), а вам нужно лишь достроить его - довнедрить зависимости в поля/свойства/методы, помеченные атрибутами внедрения. Для этого есть BuildUp‑сценарии и генерация builders.
// Настраиваем генерацию builder для интерфейса IRobot DI.Setup(nameof(Composition)) .Bind().To(Guid.NewGuid) .Bind().To<PlutoniumBattery>() .Builders<IRobot>("BuildUp"); var composition = new Composition(); // Достраиваем уже созданный объект var bot = composition.BuildUp(new CleanerBot());
Builders: BuildUp для типов, известных на этапе компиляции
Builders удобны для сценариев, где DI дополняет/инициализирует уже имеющийся экземпляр, например, компоненты Unity, десериализованные объекты и др.
Контроль автопривязок (auto-binding)
Автопривязки часто ускоряют разработку, но в больших решениях иногда нужно запретить их полностью или точечно, чтобы все зависимости были под полным контролем - имели явные привязки и ревью изменений.
// Полностью отключаем автопривязки DI.Setup(nameof(Composition)) .Hint(Hint.DisableAutoBinding, "On") .Bind<IService>().To<Service>() .Root<IService>("Root");
DisableAutoBinding поддерживает фильтры по имени типа и по lifetime — удобно, если вы хотите оставить auto-binding только для «простых DTO», но запретить для сервисов/репозиториев:
DisableAutoBindingImplementationTypeNameRegularExpressionDisableAutoBindingImplementationTypeNameWildcardDisableAutoBindingLifetimeRegularExpressionDisableAutoBindingLifetimeWildcard
Пропуск конструктора по умолчанию
Иногда наличие параметрless‑конструктора мешает: объект формально можно создать, но реальная инициализация объекта должна идти через другой конструктор/фабрику. Подсказка SkipDefaultConstructor помогает направить генерацию в нужную сторону.
// Пропускаем конструктор по умолчанию для всех типов DI.Setup(nameof(Composition)) .Hint(Hint.SkipDefaultConstructor, "On") // Или только для конкретных типов по маске .Hint(Hint.SkipDefaultConstructorImplementationTypeNameWildcard, "*SchroedingersCat") .Bind<ICat>().To<SchroedingersCat>() .Root<Zoo>("Root");
Это полезно в интеграциях, где часть типов имеет «пустой» конструктор для фреймворка, но в реальной композиции должна создаваться с использованием рекомендуемого конструктора. Другие полезные подсказки:
SkipDefaultConstructorImplementationTypeNameRegularExpressionSkipDefaultConstructorImplementationTypeNameWildcardSkipDefaultConstructorLifetimeRegularExpressionSkipDefaultConstructorLifetimeWildcard
Light roots и оптимизированные анонимные корни
Для небольших корней (утилиты, «простые» сервисы) выгодно генерировать их как легковесные: корни используют общий lightweight‑композит и делегаты, значительно уменьшая объём сгенерированного кода и накладные расходы. Анонимные корни легковесны по умолчанию.
using static Pure.DI.RootKinds; DI.Setup(nameof(Composition)) .Bind().To<ConsoleLogger>() // Именованный легковесный корень .Root<ILogger>("Logger", kind: Light); var composition = new Composition(); var logger = composition.Logger;
Light roots и лёгкие анонимные корни
Если вам важно «дебажить» анонимные корни как полноценные графы, поведение можно контролировать подсказкой LightweightAnonymousRoot.
Lifetime по умолчанию — и для auto-bindings тоже
DefaultLifetime(...) позволяет убрать повторяющиеся .As(...) и сделать конфигурацию компактнее.
DefaultLifetime: как задать lifetime «по умолчанию»
При необходимости можно задавать lifetime «по умолчанию» точечно — для конкретного контракта и даже с учётом тега.
DefaultLifetime для конкретного контракта
Теперь время жизни можно определить и для зависимостей, которые Pure.DI создаёт автоматически (auto-bindings), в примере ниже это Cache:
// Задаём singleton для auto-binding типа Cache DI.Setup(nameof(Composition)) .DefaultLifetime<Cache>(Lifetime.Singleton) .Bind<IService>().To<Service>() .Root<IService>("Root"); class Cache; class Service(Cache cache) : IService; interface IService;
Упрощённые lifetime-методы: меньше шума в конфигурации
Когда конфигурация состоит в основном из однотипных привязок, удобнее писать не .Bind().As(...).To<...>(), а сразу lifetime-метод: Singleton<>(), PerResolve<>() и т.д. Это делает настройку композиции короче и заметно читабельнее.
// Краткая запись для однотипных привязок DI.Setup(nameof(Composition)) .PerBlock<OrderManager>() .Transient<Shop, OrderNameFormatter>() .Root<IShop>("MyShop");
Упрощённые lifetime-specific привязки
Этот же подход работает и для фабрик: когда нужно вручную создать объект, но при этом зафиксировать lifetime одним вызовом.
Упрощённые lifetime-specific фабрики
SpecialType «упрощённая привязка» для платформенных базовых типов
Упрощённая привязка, т.е. привязка без указания контрактов (.Bind().To<Implementation>()), умеет автоматически подхватывать «разумный» набор контрактов (интерфейсы/абстракции), но в платформах вроде Unity есть базовые типы (например, MonoBehaviour), которые вы не хотите превращать в контракты автоматически. Для этого можно объявить тип «специальным» и исключить его из автопривязок.
DI.Setup(nameof(Composition)) // Исключаем MonoBehaviour из автопривязок .SpecialType<MonoBehaviour>() .Bind().To<GameController>() .Root<GameController>("Controller");
Упрощённая привязка и список специальных типов
Это снижает риск неожиданных конфликтов в графе, когда базовый платформенный тип «вдруг» начинает участвовать как контракт привязки.
Маркеры generic type argument: удобнее для сложных generic-привязок
В сложных generic‑сценариях полезно иметь «маркерный» тип, который означает «любой T» в привязке (например, IBox<TT>). Pure.DI поддерживает такие маркеры через GenericTypeArgument<...>() и через вариант в виде атрибута.
// Объявляем TT как маркер для любого типа DI.Setup(nameof(Composition)) .GenericTypeArgument<MyTT>() .Bind<ISequence<MyTT>>().To<Sequence<MyTT>>() .Root<IProgram>("Root"); interface MyTT; interface ISequence<T>; class Sequence<T> : ISequence<T>; interface IProgram;
Custom generic argument: marker через GenericTypeArgument()
Custom generic argument attribute: marker через атрибут
В роли маркера может выступать не только интерфейс, но и ссылочный тип с публичным конструктором без параметров — это упрощает интеграцию с фреймворками.
Tag.Any: одна привязка для любых тегов
Когда нужно поддержать произвольные значения тега (включая null) и при этом «видеть» сам тег внутри фабрики, помогает Tag.Any.
// Одна привязка для любого значения тега DI.Setup(nameof(Composition)) .Bind<IQueue>(Tag.Any).To(ctx => new Queue(ctx.Tag)) .Root<IQueue>("AuditQueue", "Audit");
Tag.Any: привязка, которая "матчится" на любой тег
ref/out‑зависимости и ref‑struct: низкоуровневые сценарии без лишних аллокаций
Pure.DI поддерживает внедрения, где нужны ref/out, что открывает дверь в high‑perf сценарии (буферы, Span<T>, ref‑struct). Например: инициализировать сервис методом, который принимает ref‑структуру.
// Метод для внедрения с ref-параметром class Service { [Ordinal] public void Initialize(ref Data data) { /* ... */ } }
Ref dependencies: пример с ref‑struct и методом для внедрения
Контекстно‑зависимые фабрики: ctx.ConsumerType
Если фабрика должна вести себя по‑разному в зависимости от того, кому внедряют зависимость, используйте ctx.ConsumerType. Типичный пример — логирование: "обогащать" логгер типом класса‑потребителя.
// Фабрика, которая знает тип потребителя .Bind().To(ctx => { ctx.Inject<Serilog.ILogger>(out var logger); return logger.ForContext(ctx.ConsumerType); })
ConsumerType: контекстный логгер на примере Serilog
Потокобезопасные фабрики: ctx.Lock
Для фабрик, которые выполняют инициализацию «под Lock», и для параллельных override‑сценариев есть общий объект синхронизации — ctx.Lock.
// Потокобезопасная фабрика с синхронизацией .Bind<IMessageBus>().To(ctx => { lock (ctx.Lock) { ctx.Inject(out MessageBus bus); bus.Connect(); return bus; } })
Фабрика с синхронизацией через ctx.Lock
Thread-safe overrides: безопасные overrides при параллельной сборке
Virtual/Override корний: расширяемость через наследование
Если вы строите «базовую» композицию для продакшена и хотите переопределять корни в наследниках (например, в тестах или для разных окружений), корни можно делать virtual, а затем переопределять.
using static Pure.DI.RootKinds; // Продакшен-композиция с virtual корнем partial class ProdComposition { private void Setup() => DI.Setup(nameof(ProdComposition)) .Bind<ITime>().To<SystemTime>() .Root<ITime>("Time", kind: Public | Property | Virtual); } // Тестовая композиция переопределяет корень partial class TestComposition : ProdComposition { // Переопределяем root в наследнике public override ITime Time => new FakeTime(); }
Флаг Override полезен, когда вы хотит��, чтобы сам генератор создал override‑реализацию корня в производном классе (например, с другим набором привязок), оставаясь в compile‑time модели.
Множественные [Bind] атрибуты
BindAttribute позволяет объявлять «источники зависимостей» прямо на свойствах/полях/методах провайдера. Теперь можно задавать несколько привязок — удобно, когда один метод/свойство должно "обслуживать" несколько контрактов/тегов.
class GatewayProvider { // Один метод обслуживает несколько контрактов/тегов [Bind(typeof(IApiClient), Lifetime.Singleton, "Public")] [Bind(typeof(IApiClient), Lifetime.Singleton, "Internal")] public ApiClient Create() => new ApiClient(); }
BindAttribute: привязка зависимостей
Type/Tag атрибуты на members‑внедрении (методы/поля/свойства)
Когда вы делаете внедрение не через конструктор, а через свойства/поля/методы, иногда нужно явно указать тег или тип, чтобы точно указать что должно быть внедрено. Атрибуты Tag и Type теперь поддерживаются и для таких внедрений.
class JobRunner { [Tag("fast")] public IExecutor? Executor { get; set; } public void Init([Type(typeof(SystemClock))] IClock clock) { /* ... */ } }
Type attribute: явное указание внедряемого типа
Tag attribute: выбор реализации по тегу
Это хорошо сочетается с BuildUp/Builders, где объект уже существует, а вы хотите аккуратно «настроить» его зависимости через members‑внедрение.
Генерация конструкторов: только когда это нужно
Поведение генерации конструкторов для Composition стало более прагматичным: конструкторы появляются, когда в графе есть аргументы композиции и/или scoped‑lifetime привязки.
DI.Setup(nameof(Composition)) .Arg<string>("connectionString") .Bind<IDb>().To<Db>() .Root<IService>("Service"); // Конструктор генерируется с аргументом connectionString var composition = new Composition(connectionString: "Server=.;Database=App;");
Composition arguments: передача значений снаружи в композицию
Scope: lifetime Scoped и работа со scope‑иерархией
Смысл простой: если у композиции нет параметров и нет scoped‑зависимостей, лишние конструкторы не генерируются. А если они нужны — сигнатура строится по реально используемым аргументам. Это позволяет использовать Pure.DI, например, в таких сценариях как Unity, когда не всегда можно использовать конструкторы для инициализации объектов.
Циклические зависимости
Pure.DI улучшил работу с циклическими зависимостями: диагностика точнее указывает места в коде, построение графа старается показывать минимально возможное число ошибок, а в сложных случаях анализ старается аккуратнее «собирать» циклические участки, чтобы результат был предсказуемее. В сценариях, где цикл допустим по смыслу, его по‑прежнему можно разорвать через Func<T>, Lazy<T> и т. п.
interface IA; interface IB; // Разрыв цикла через фабрику Func<IB> class A(Func<IB> b) : IA; class B(IA a) : IB;
Lazy/Func: инъекции по требованию и разрыв циклов
Если же цикл недопустим — вы получите диагностику на этапе компиляции с ID и ссылкой на справку.
Диагностика стала проще: ID, справка, локализация
Pure.DI всё так же обнаруживает проблемы на этапе компиляции, но с ними стало проще работать: у ошибок/предупреждений появились стабильные ID, описания и ссылки на документацию, локализация сообщений. Плюс к этому улучшена привязка диагностик к месту в коде.
DI.Setup(nameof(Composition)) .Bind<IService>().To<Service>() .Root<IService>("Root"); interface IService; class Service(IDependency dep) : IService; interface IDependency;
Сейчас диагностические сообщения доступны для следующих языков:
Английский
Арабский
Бенгальский
Вьетнамский
Индонезийский
Испанский
Итальянский
Китайский
Корейский
Немецкий
Португальский
Русский
Тайский
Французский
Хинди
Японский
Если же кому не хватает например Санскрита, то не стесняйтесь создать тикет в репозитории Pure.DI на GitHub.
Если есть желание попробовать новые возможности, можно начать с любого примера — они независимы и запускаются легко через dotnet run.
Спасибо за интерес и что дочитали до конца!
