Usage scenarios.

July 17, 2026 · View on GitHub

This Markdown-formatted document contains information about working with Pure.DI

Usage scenarios.

Auto-bindings

Pure.DI can create non-abstract types without explicit bindings, which makes quick prototypes and small demos concise. The generator still validates the graph at compile time and produces regular C# object creation code.

using Pure.DI;

// Specifies to create a partial class named "Composition"
DI.Setup("Composition")
    // with the root "Orders"
    .Root<OrderService>("Orders");

var composition = new Composition();

// service = new OrderService(new Database())
var orders = composition.Orders;

class Database;

class OrderService(Database database);

To run the above code, the following NuGet package must be added:

Auto-bindings are convenient for utilities and sample code where dependency choices are obvious. In larger applications they can hide architectural intent, because consumers start depending on concrete classes. If you need interchangeable implementations and explicit lifetime control, prefer bindings of abstractions to implementations.

Warning

This approach is not recommended if you follow the dependency inversion principle or need precise lifetime control.

Prefer injecting abstractions (for example, interfaces) and map them to implementations as in most other examples. Limitations: auto-bindings scale poorly when several implementations, decorators, or strict lifetime rules are required. Common pitfalls:

Injections of abstractions

This is the recommended model for production code: depend on abstractions and bind them to implementations in composition. It keeps business code independent from infrastructure details and makes replacements predictable.

using Pure.DI;

DI.Setup(nameof(Composition))
    // Binding abstractions to their implementations:
    // The interface IGpsSensor is bound to the implementation GpsSensor
    .Bind<IGpsSensor>().To<GpsSensor>()
    // The interface INavigationSystem is bound to the implementation NavigationSystem
    .Bind<INavigationSystem>().To<NavigationSystem>()

    // Specifies to create a composition root
    // of type "VehicleComputer" with the name "VehicleComputer"
    .Root<VehicleComputer>("VehicleComputer");

var composition = new Composition();

// Usage:
// var vehicleComputer = new VehicleComputer(new NavigationSystem(new GpsSensor()));
var vehicleComputer = composition.VehicleComputer;

vehicleComputer.StartTrip();

// The sensor abstraction
interface IGpsSensor;

// The sensor implementation
class GpsSensor : IGpsSensor;

// The service abstraction
interface INavigationSystem
{
    void Navigate();
}

// The service implementation
class NavigationSystem(IGpsSensor sensor) : INavigationSystem
{
    public void Navigate()
    {
        // Navigation logic using the sensor...
    }
}

// The consumer of the abstraction
partial class VehicleComputer(INavigationSystem navigationSystem)
{
    public void StartTrip() => navigationSystem.Navigate();
}

To run the above code, the following NuGet package must be added:

The binding chain maps abstractions to concrete types so the generator can build a fully concrete object graph. This keeps consumers decoupled and allows swapping implementations. A single implementation can satisfy multiple abstractions.

Tip

If a binding is missing, injection still works when the consumer requests a concrete type (not an abstraction).

Limitations: explicit bindings add configuration lines, but the trade-off is clearer architecture and safer evolution. Common pitfalls:

  • Mixing abstraction-first and concrete-only styles in one module without clear boundaries.
  • Forgetting to bind alternate implementations for tagged use cases. See also: Auto-bindings, Tags.

Simplified binding

You can call Bind() without type parameters to infer contracts from the implementation type. This reduces boilerplate while preserving compile-time graph validation.

using System.Collections;
using Pure.DI;

// Specifies to create a partial class "Composition"
DI.Setup(nameof(Composition))
    // Begins the binding definition for the implementation type itself,
    // and if the implementation is not an abstract class or structure,
    // for all abstract but NOT special types that are directly implemented.
    // Equivalent to:
    // .Bind<IOrderRepository, IOrderNotification, OrderManager>()
    //   .As(Lifetime.PerBlock)
    //   .To<OrderManager>()
    .Bind().As(Lifetime.PerBlock).To<OrderManager>()
    .Bind().To<Shop>()

    // Specifies to create a property "MyShop"
    .Root<IShop>("MyShop");

var composition = new Composition();
var shop = composition.MyShop;

interface IManager;

class ManagerBase : IManager;

interface IOrderRepository;

interface IOrderNotification;

class OrderManager :
    ManagerBase,
    IOrderRepository,
    IOrderNotification,
    IDisposable,
    IEnumerable<string>
{
    public void Dispose() {}

    public IEnumerator<string> GetEnumerator() =>
        new List<string> { "Order #1", "Order #2" }.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

interface IShop;

class Shop(
    OrderManager manager,
    IOrderRepository repository,
    IOrderNotification notification)
    : IShop;

To run the above code, the following NuGet package must be added:

In practice, most abstraction types can be inferred. The parameterless Bind() binds:

  • the implementation type itself
  • and, if it is NOT abstract,
    • all abstract types it directly implements
    • except special types

Special types will not be added to bindings:

  • System.Object
  • System.Enum
  • System.MulticastDelegate
  • System.Delegate
  • System.Collections.IEnumerable
  • System.Collections.Generic.IEnumerable<T>
  • System.Collections.Generic.IList<T>
  • System.Collections.Generic.ICollection<T>
  • System.Collections.IEnumerator
  • System.Collections.Generic.IEnumerator<T>
  • System.Collections.Generic.IReadOnlyList<T>
  • System.Collections.Generic.IReadOnlyCollection<T>
  • System.IDisposable
  • System.IAsyncResult
  • System.AsyncCallback

If you want to add your own special type, use the SpecialType<T>() call.

For class OrderManager, Bind().To<OrderManager>() is equivalent to Bind<IOrderRepository, IOrderNotification, OrderManager>().To<OrderManager>(). The types IDisposable and IEnumerable<string> are excluded because they are special. ManagerBase is excluded because it is not abstract. IManager is excluded because it is not implemented directly by OrderManager.

OrderManagerimplementation type itself
IOrderRepositorydirectly implements
IOrderNotificationdirectly implements
IDisposablespecial type
IEnumerable<string>special type
ManagerBasenon-abstract
IManageris not directly implemented by class OrderManager
Limitations: inferred bindings include only directly implemented abstractions and exclude special types.
Common pitfalls:

Composition roots

This example shows several ways to define composition roots as explicit entry points into the graph.

Tip

There is no hard limit on roots, but prefer a small number. Ideally, an application has a single composition root.

In classic DI containers, the composition is resolved dynamically via calls like T Resolve<T>() or object GetService(Type type). In Pure.DI, each root generates a property or method at compile time, so roots are explicit and discoverable.

using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind<IInvoiceGenerator>().To<PdfInvoiceGenerator>()
    .Bind<IInvoiceGenerator>("Online").To<HtmlInvoiceGenerator>()
    .Bind<ILogger>().To<FileLogger>()

    // Specifies to create a regular composition root
    // of type "IInvoiceGenerator" with the name "InvoiceGenerator".
    // This will be the main entry point for invoice generation.
    .Root<IInvoiceGenerator>("InvoiceGenerator")

    // Specifies to create an anonymous composition root
    // that is only accessible from "Resolve()" methods.
    // This is useful for auxiliary types or testing.
    .Root<ILogger>()

    // Specifies to create a regular composition root
    // of type "IInvoiceGenerator" with the name "OnlineInvoiceGenerator"
    // using the "Online" tag to differentiate implementations.
    .Root<IInvoiceGenerator>("OnlineInvoiceGenerator", "Online");

var composition = new Composition();

// Resolves the default invoice generator (PDF) with all its dependencies
// invoiceGenerator = new PdfInvoiceGenerator(new FileLogger());
var invoiceGenerator = composition.InvoiceGenerator;

// Resolves the online invoice generator (HTML)
// onlineInvoiceGenerator = new HtmlInvoiceGenerator();
var onlineInvoiceGenerator = composition.OnlineInvoiceGenerator;

// All and only the roots of the composition
// can be obtained by Resolve method.
// Here we resolve the private root 'ILogger'.
var logger = composition.Resolve<ILogger>();

// We can also resolve tagged roots dynamically if needed
var tagged = composition.Resolve<IInvoiceGenerator>("Online");

// Common logger interface used across the system
interface ILogger;

// Concrete implementation of a logger that writes to a file
class FileLogger : ILogger;

// Abstract definition of an invoice generator
interface IInvoiceGenerator;

// Implementation for generating PDF invoices, dependent on ILogger
class PdfInvoiceGenerator(ILogger logger) : IInvoiceGenerator;

// Implementation for generating HTML invoices for online viewing
class HtmlInvoiceGenerator : IInvoiceGenerator;

To run the above code, the following NuGet package must be added:

The name of the composition root is arbitrarily chosen depending on its purpose but should be restricted by the property naming conventions in C# since it is the same name as a property in the composition class. In reality, the Root property has the form:

public IService Root
{
  get
  {
    return new Service(new Dependency());
  }
}

To avoid generating Resolve methods just add a comment // Resolve = Off before a Setup method:

// Resolve = Off
DI.Setup("Composition")
  .Bind<IDependency>().To<Dependency>()
  ...

This can be done if these methods are not needed, in case only certain composition roots are used. It's not significant then, but it will help save resources during compilation. Limitations: too many public roots increase composition API surface and make architecture boundaries harder to track. Common pitfalls:

  • Exposing internal services as roots instead of keeping them private.
  • Depending on Resolve everywhere instead of explicit root members. See also: Resolve methods, Root arguments.

Transient

The Transient lifetime specifies to create a new dependency instance each time. It is the default lifetime and can be omitted.

using Shouldly;
using Pure.DI;
using static Pure.DI.Lifetime;

DI.Setup(nameof(Composition))
    .Bind().As(Transient).To<Buffer>()
    .Bind().To<BatchProcessor>()
    .Root<IBatchProcessor>("Processor");

var composition = new Composition();
var processor = composition.Processor;

// Verify that input and output buffers are different instances.
// This is critical for the batch processor to avoid data corruption
// during reading. The Transient lifetime ensures a new instance
// is created for each dependency injection.
processor.Input.ShouldNotBe(processor.Output);

// Represents a memory buffer that should be unique for each operation
interface IBuffer;

class Buffer : IBuffer;

interface IBatchProcessor
{
    public IBuffer Input { get; }

    public IBuffer Output { get; }
}

class BatchProcessor(
    IBuffer input,
    IBuffer output)
    : IBatchProcessor
{
    public IBuffer Input { get; } = input;

    public IBuffer Output { get; } = output;
}

To run the above code, the following NuGet packages must be added:

The Transient lifetime is the safest and is used by default. Yes, its widespread use can cause a lot of memory traffic, but if there are doubts about thread safety, the Transient lifetime is preferable because each consumer has its own instance of the dependency. The following nuances should be considered when choosing the Transient lifetime:

  • There will be unnecessary memory overhead that could be avoided.

  • Every object created must be disposed of, and this will waste CPU resources, at least when the GC does its memory-clearing job.

  • Poorly designed constructors can run slowly, perform functions that are not their own, and greatly hinder the efficient creation of compositions of multiple objects.

Important

The following very important rule, in my opinion, will help in the last point. Now, when a constructor is used to implement dependencies, it should not be loaded with other tasks. Accordingly, constructors should be free of all logic except for checking arguments and saving them for later use. Following this rule, even the largest compositions of objects will be built quickly.

Singleton

The Singleton lifetime ensures that there will be a single instance of the dependency for each composition.

using Shouldly;
using Pure.DI;
using System.Diagnostics.CodeAnalysis;
using static Pure.DI.Lifetime;

DI.Setup(nameof(Composition))
    // Bind the cache as Singleton to share it across all services
    .Bind().As(Singleton).To<Cache>()
    // Bind the order service as Transient (default) for per-request instances
    .Bind().To<OrderService>()
    .Root<IOrderService>("OrderService");

var composition = new Composition();
var orderService1 = composition.OrderService; // First order service instance
var orderService2 = composition.OrderService; // Second order service instance

// Verify that both services share the same cache instance (Singleton behavior)
orderService1.Cache.ShouldBe(orderService2.Cache);
// Simulate real-world usage: add data to cache via one service and check via another
orderService1.AddToCache("Order123", "Processed");
orderService2.GetFromCache("Order123").ShouldBe("Processed");

// Interface for a shared cache (e.g., for storing order statuses)
interface ICache
{
    void Add(string key, string value);

    bool TryGet(string key, [MaybeNullWhen(false)] out string value);
}

// Implementation of a simple in-memory cache (must be thread-safe in real apps)
class Cache : ICache
{
    private readonly Dictionary<string, string> _data = new();

    public void Add(string key, string value) =>
        _data[key] = value;

    public bool TryGet(string key, [MaybeNullWhen(false)] out string value) =>
        _data.TryGetValue(key, out value);
}

// Interface for order processing service
interface IOrderService
{
    ICache Cache { get; }

    void AddToCache(string orderId, string status);

    string GetFromCache(string orderId);
}

// Order service that uses the shared cache
class OrderService(ICache cache) : IOrderService
{
    // The cache is injected and shared (Singleton)
    public ICache Cache { get; } = cache;

    // Real-world method: add order status to cache
    public void AddToCache(string orderId, string status) =>
        Cache.Add(orderId, status);

    // Real-world method: retrieve order status from cache
    public string GetFromCache(string orderId) =>
        Cache.TryGet(orderId, out var status) ? status : "unknown";
}

To run the above code, the following NuGet packages must be added:

Some articles advise using objects with a Singleton lifetime as often as possible, but the following details must be considered:

  • For .NET the default behavior is to create a new instance of the type each time it is needed, other behavior requires, additional logic that is not free and requires additional resources.

  • The use of Singleton adds a requirement for thread-safety controls on their use, since singletons are more likely to share their state between different threads without even realizing it.

  • The thread-safety control should be automatically extended to all dependencies that Singleton uses, since their state is also now shared.

  • Logic for thread-safety control can be resource-costly, error-prone, interlocking, and difficult to test.

  • Singleton can retain dependency references longer than their expected lifetime, this is especially significant for objects that hold "non-renewable" resources, such as the operating system Handler.

  • Sometimes additional logic is required to dispose of Singleton.

Scoped

The Scoped lifetime ensures that there will be a single instance of the dependency for each scope.

using Shouldly;
using Pure.DI;
using static Pure.DI.Lifetime;

var composition = new Composition();
var app = composition.AppRoot;

// Real-world analogy:
// each HTTP request (or message consumer handling) creates its own scope.
// Scoped services live exactly as long as the request is being processed.

// Request #1
var request1 = app.CreateRequestScope();
var checkout1 = request1.RequestRoot;

var ctx11 = checkout1.Context;
var ctx12 = checkout1.Context;

// Same request => same scoped instance
ctx11.ShouldBe(ctx12);

// Request #2
var request2 = app.CreateRequestScope();
var checkout2 = request2.RequestRoot;

var ctx2 = checkout2.Context;

// Different request => different scoped instance
ctx11.ShouldNotBe(ctx2);

// End of Request #1 => scoped instance is disposed
request1.Dispose();
ctx11.IsDisposed.ShouldBeTrue();

// End of Request #2 => scoped instance is disposed
request2.Dispose();
ctx2.IsDisposed.ShouldBeTrue();

interface IRequestContext
{
    Guid CorrelationId { get; }

    bool IsDisposed { get; }
}

// Typically: DbContext / UnitOfWork / RequestTelemetry / Activity, etc.
sealed class RequestContext : IRequestContext, IDisposable
{
    public Guid CorrelationId { get; } = Guid.NewGuid();

    public bool IsDisposed { get; private set; }

    public void Dispose() => IsDisposed = true;
}

interface ICheckoutService
{
    IRequestContext Context { get; }
}

// "Controller/service" that participates in request processing.
// It depends on a scoped context (per-request resource).
sealed class CheckoutService(IRequestContext context) : ICheckoutService
{
    public IRequestContext Context => context;
}

// Implements a request scope (per-request composition)
sealed class RequestScope(Composition parent) : Composition(parent);

partial class App(Func<RequestScope> requestScopeFactory)
{
    // In a web app this would roughly map to: "create scope for request"
    public RequestScope CreateRequestScope() => requestScopeFactory();
}

partial class Composition
{
    static void Setup() =>

        DI.Setup()
            // Per-request lifetime
            .Bind().As(Scoped).To<RequestContext>()

            // Regular service that consumes scoped context
            .Bind().To<CheckoutService>()

            // "Request root" (what your controller/handler resolves)
            .Root<ICheckoutService>("RequestRoot")

            // "Application root" (what creates request scopes)
            .Root<App>("AppRoot");
}

To run the above code, the following NuGet packages must be added:

Note

Scoped lifetime is essential for request-based or session-based scenarios where instances should be shared within a scope but isolated between scopes.

Tags

Tags let you control dependency selection when multiple implementations exist: This is practical for scenarios like public/internal API clients, multiple payment providers, or environment-specific integrations.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    // The `default` tag is used when the consumer does not specify a tag
    .Bind<IApiClient>("Public", default).To<RestApiClient>()
    .Bind<IApiClient>("Internal").As(Lifetime.Singleton).To<InternalApiClient>()
    .Bind<IApiFacade>().To<ApiFacade>()

    // "InternalRoot" is a root name, "Internal" is a tag
    .Root<IApiClient>("InternalRoot", "Internal")

    // Specifies to create the composition root named "Root"
    .Root<IApiFacade>("Api");

var composition = new Composition();
var api = composition.Api;
api.PublicClient.ShouldBeOfType<RestApiClient>();
api.InternalClient.ShouldBeOfType<InternalApiClient>();
api.InternalClient.ShouldBe(composition.InternalRoot);
api.DefaultClient.ShouldBeOfType<RestApiClient>();

interface IApiClient;

class RestApiClient : IApiClient;

class InternalApiClient : IApiClient;

interface IApiFacade
{
    IApiClient PublicClient { get; }

    IApiClient InternalClient { get; }

    IApiClient DefaultClient { get; }
}

class ApiFacade(
    [Tag("Public")] IApiClient publicClient,
    [Tag("Internal")] IApiClient internalClient,
    IApiClient defaultClient)
    : IApiFacade
{
    public IApiClient PublicClient { get; } = publicClient;

    public IApiClient InternalClient { get; } = internalClient;

    public IApiClient DefaultClient { get; } = defaultClient;
}

To run the above code, the following NuGet packages must be added:

The example shows how to:

  • Define multiple bindings for the same interface
  • Use tags to differentiate between implementations
  • Control lifetime management
  • Inject tagged dependencies into constructors

The tag can be a constant, a type, a smart tag, or a value of an Enum type. The default and null tags are also supported. Limitations: extensive tag usage can become hard to navigate if naming conventions are inconsistent. Common pitfalls:

  • Using many ad-hoc string tags without central conventions.
  • Forgetting to define a default tag path for untagged consumers. See also: Smart tags, Composition roots.

Factory

Constructor injection covers most cases, but sometimes an instance needs extra work before it is ready to use — like the Connect() call here that opens a database connection. A factory binding To<T>(ctx => ...) puts that creation logic under your control: call ctx.Inject(out var dependency) to have the container provide dependencies, run any setup code, then return the finished instance.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind<IDatabaseService>().To<DatabaseService>(ctx => {
        // Some logic for creating an instance.
        // For example, we need to manually initialize the connection.
        ctx.Inject(out DatabaseService service);
        service.Connect();
        return service;
    })
    .Bind<IUserRegistry>().To<UserRegistry>()

    // Composition root
    .Root<IUserRegistry>("Registry");

var composition = new Composition();
var registry = composition.Registry;
registry.Database.IsConnected.ShouldBeTrue();

interface IDatabaseService
{
    bool IsConnected { get; }
}

class DatabaseService : IDatabaseService
{
    public bool IsConnected { get; private set; }

    // Simulates a connection establishment that must be called explicitly
    public void Connect() => IsConnected = true;
}

interface IUserRegistry
{
    IDatabaseService Database { get; }
}

class UserRegistry(IDatabaseService database) : IUserRegistry
{
    public IDatabaseService Database { get; } = database;
}

To run the above code, the following NuGet packages must be added:

There are scenarios where manual control over the creation process is required, such as

  • When additional initialization logic is needed
  • When complex construction steps are required
  • When specific object states need to be set during creation

Important

The method Inject() cannot be used outside of the binding setup. Limitations: factory bindings introduce custom construction logic that must be maintained and tested. Common pitfalls:

Simplified factory

This example shows a simplified manual factory. Each lambda parameter represents an injected dependency, and starting with C# 10 you can add Tag(...) to specify a tagged dependency.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind("today").To(() => DateTime.Today)
    // Injects FileLogger and DateTime
    // and applies additional initialization logic
    .Bind<IFileLogger>().To((
        FileLogger logger,
        [Tag("today")] DateTime date) => {
        logger.Init($"app-{date:yyyy-MM-dd}.log");
        return logger;
    })
    .Bind().To<OrderProcessingService>()

    // Composition root
    .Root<IOrderProcessingService>("OrderService");

var composition = new Composition();
var service = composition.OrderService;

service.Logger.FileName.ShouldBe($"app-{DateTime.Today:yyyy-MM-dd}.log");

interface IFileLogger
{
    string FileName { get; }

    void Log(string message);
}

class FileLogger : IFileLogger
{
    public string FileName { get; private set; } = "";

    public void Init(string fileName) => FileName = fileName;

    public void Log(string message)
    {
        // Write to file
    }
}

interface IOrderProcessingService
{
    IFileLogger Logger { get; }
}

class OrderProcessingService(IFileLogger logger) : IOrderProcessingService
{
    public IFileLogger Logger { get; } = logger;
}

To run the above code, the following NuGet packages must be added:

The example creates a service that depends on a logger initialized with a date-based file name. This style keeps the setup concise while still allowing explicit initialization logic. The Tag attribute enables named dependencies for more complex setups. Limitations: compact lambda factories stay readable only while initialization logic remains small. Common pitfalls:

  • Putting heavy imperative setup code into short lambda factories.
  • Forgetting explicit tags when several same-type dependencies exist. See also: Factory, Tags.

Injection on demand

This example creates dependencies on demand using a factory delegate. The service (GameLevel) needs multiple instances of IEnemy, so it receives a Func<IEnemy> that can create new instances when needed. This approach is useful when instances are created lazily or repeatedly during business execution.

using Shouldly;
using Pure.DI;
using System.Collections.Generic;

DI.Setup(nameof(Composition))
    .Bind().To<Enemy>()
    .Bind().To<GameLevel>()

    // Composition root
    .Root<IGameLevel>("GameLevel");

var composition = new Composition();
var gameLevel = composition.GameLevel;

// Verifies that two distinct enemies have been spawned
gameLevel.Enemies.Count.ShouldBe(2);

// Represents a game entity that acts as an enemy
interface IEnemy;

class Enemy : IEnemy;

// Represents a game level that manages entities
interface IGameLevel
{
    IReadOnlyList<IEnemy> Enemies { get; }
}

class GameLevel(Func<IEnemy> enemySpawner) : IGameLevel
{
    // The factory spawns a fresh enemy instance on each call.
    public IReadOnlyList<IEnemy> Enemies { get; } =
    [
        enemySpawner(),
        enemySpawner()
    ];
}

To run the above code, the following NuGet packages must be added:

Key elements:

  • Enemy is bound to the IEnemy interface, and GameLevel is bound to IGameLevel.
  • The GameLevel constructor accepts Func<IEnemy>, enabling deferred creation of entities.
  • The GameLevel calls the factory twice, resulting in two distinct Enemy instances stored in its Enemies collection.

This approach lets factories control lifetime and instantiation timing. Pure.DI resolves a new IEnemy each time the factory is invoked. Limitations: factory delegate calls can create many objects, so lifetime choices still matter for performance and state. Common pitfalls:

Composition arguments

Use composition arguments when you need to pass state into the composition. Define them with Arg<T>(string argName) (optionally with tags) and use them like any other dependency. Only arguments that are used in the object graph become constructor parameters. This is a clean way to inject external runtime state without global static variables.

Note

Actually, composition arguments work like normal bindings. The difference is that they bind to the values of the arguments. These values will be injected wherever they are required.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind<IBankGateway>().To<BankGateway>()
    .Bind<IPaymentProcessor>().To<PaymentProcessor>()

    // Composition root "PaymentService"
    .Root<IPaymentProcessor>("PaymentService")

    // Composition argument: Connection timeout (e.g., from config)
    .Arg<int>("timeoutSeconds")

    // Composition argument: API Token (using a tag to distinguish from other strings)
    .Arg<string>("authToken", "api token")

    // Composition argument: Bank gateway address
    .Arg<string>("gatewayUrl");

// Create the composition, passing real settings from outside
var composition = new Composition(
    timeoutSeconds: 30,
    authToken: "secret_token_123",
    gatewayUrl: "https://api.bank.com/v1");

var paymentService = composition.PaymentService;

paymentService.Token.ShouldBe("secret_token_123");
paymentService.Gateway.Timeout.ShouldBe(30);
paymentService.Gateway.Url.ShouldBe("https://api.bank.com/v1");

interface IBankGateway
{
    int Timeout { get; }

    string Url { get; }
}

// Simulation of a bank gateway client
class BankGateway(int timeoutSeconds, string gatewayUrl) : IBankGateway
{
    public int Timeout { get; } = timeoutSeconds;

    public string Url { get; } = gatewayUrl;
}

interface IPaymentProcessor
{
    string Token { get; }

    IBankGateway Gateway { get; }
}

// Payment processing service
class PaymentProcessor(
    // The tag allows specifying exactly which string to inject here
    [Tag("api token")] string token,
    IBankGateway gateway) : IPaymentProcessor
{
    public string Token { get; } = token;

    public IBankGateway Gateway { get; } = gateway;
}

To run the above code, the following NuGet packages must be added:

Note

Composition arguments provide a way to inject runtime values into the composition, making your DI configuration more flexible. Limitations: too many composition arguments can bloat composition constructors and blur configuration boundaries. Common pitfalls:

  • Using untagged primitive arguments where several values of the same type exist.
  • Treating composition arguments as mutable runtime state holders. See also: Root arguments, Tags.

Root arguments

Use root arguments when you need to pass state into a specific root. Define them with RootArg<T>(string argName) (optionally with tags) and use them like any other dependency. A root that uses at least one root argument becomes a method, and only arguments used in that root's object graph appear in the method signature. Use unique argument names to avoid collisions. Root arguments are useful when runtime values belong to one entry point, not to the whole composition.

Note

Actually, root arguments work like normal bindings. The difference is that they bind to the values of the arguments. These values will be injected wherever they are required.

using Shouldly;
using Pure.DI;
using static Pure.DI.Tag;

DI.Setup(nameof(Composition))
    // Disable Resolve methods because root arguments are not compatible
    .Hint(Hint.Resolve, "Off")
    .Bind<IDatabaseService>().To<DatabaseService>()
    .Bind<IApplication>().To<Application>()

    // Root arguments serve as values passed
    // to the composition root method
    .RootArg<int>("port")
    .RootArg<string>("connectionString")

    // An argument can be tagged
    // to be injectable by type and this tag
    .RootArg<string>("appName", AppDetail)

    // Composition root
    .Root<IApplication>("CreateApplication");

var composition = new Composition();

// Creates an application with specific arguments
var app = composition.CreateApplication(
    appName: "MySuperApp",
    port: 8080,
    connectionString: "Server=.;Database=MyDb;");

app.Name.ShouldBe("MySuperApp");
app.Database.Port.ShouldBe(8080);
app.Database.ConnectionString.ShouldBe("Server=.;Database=MyDb;");

interface IDatabaseService
{
    int Port { get; }

    string ConnectionString { get; }
}

class DatabaseService(int port, string connectionString) : IDatabaseService
{
    public int Port { get; } = port;

    public string ConnectionString { get; } = connectionString;
}

interface IApplication
{
    string Name { get; }

    IDatabaseService Database { get; }
}

class Application(
    [Tag(AppDetail)] string name,
    IDatabaseService database)
    : IApplication
{
    public string Name { get; } = name;

    public IDatabaseService Database { get; } = database;
}

To run the above code, the following NuGet packages must be added:

When using root arguments, compilation warnings are emitted if Resolve methods are generated because these methods cannot create such roots. Disable Resolve via Hint(Hint.Resolve, "Off"), or ignore the warnings and accept the risks. Limitations: roots with root arguments become methods and are incompatible with generated Resolve methods. Common pitfalls:

  • Reusing ambiguous argument names for different concepts.
  • Forgetting to disable or avoid Resolve usage in these setups. See also: Composition arguments, Resolve hint.

Resolve methods

This example shows how to resolve dependencies via generated Resolve methods, i.e. through the Service Locator style. Use this style mainly for integration scenarios; explicit roots are usually cleaner and safer.

using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind<IDevice>().To<Device>()
    .Bind<ISensor>().To<TemperatureSensor>()
    .Bind<ISensor>("Humidity").To<HumiditySensor>()

    // Specifies to create a private root
    // that is only accessible from _Resolve_ methods
    .Root<ISensor>()

    // Specifies to create a public root named _HumiditySensor_
    // using the "Humidity" tag
    .Root<ISensor>("HumiditySensor", "Humidity");

var composition = new Composition();

// The next 3 lines of code do the same thing:
var sensor1 = composition.Resolve<ISensor>();
var sensor2 = composition.Resolve(typeof(ISensor));
var sensor3 = composition.Resolve(typeof(ISensor), null);

// Resolve by "Humidity" tag
// The next 3 lines of code do the same thing too:
var humiditySensor1 = composition.Resolve<ISensor>("Humidity");
var humiditySensor2 = composition.Resolve(typeof(ISensor), "Humidity");
var humiditySensor3 = composition.HumiditySensor; // Resolve via the public root

interface IDevice;

class Device : IDevice;

interface ISensor;

class TemperatureSensor(IDevice device) : ISensor;

class HumiditySensor : ISensor;

To run the above code, the following NuGet package must be added:

Resolve methods are similar to calling composition roots, which are properties (or methods). Roots are efficient and do not throw, so they are preferred. In contrast, Resolve methods have drawbacks:

  • They provide access to an unlimited set of dependencies (Service Locator).
  • Their use can potentially lead to runtime exceptions. For example, when the corresponding root has not been defined.
  • They are awkward for some UI binding scenarios (e.g., MAUI/WPF/Avalonia). Limitations: Resolve is dynamic access to the graph, so it weakens compile-time clarity compared to explicit roots. Common pitfalls:
  • Using Resolve as the default access pattern across the codebase.
  • Assuming runtime resolve calls are always safe when no matching root exists. See also: Composition roots, Resolve hint.

Injections on demand with arguments

This example uses a parameterized factory so dependencies can be created with runtime arguments. The service creates sensors with specific IDs at instantiation time. It is a type-safe way to combine DI-managed creation with runtime data.

using Shouldly;
using Pure.DI;
using System.Collections.Generic;

DI.Setup(nameof(Composition))
    .Bind().To<Sensor>()
    .Bind().To<SmartHome>()

    // Composition root
    .Root<ISmartHome>("SmartHome");

var composition = new Composition();
var smartHome = composition.SmartHome;
var sensors = smartHome.Sensors;

sensors.Count.ShouldBe(2);
sensors[0].Id.ShouldBe(101);
sensors[1].Id.ShouldBe(102);

interface ISensor
{
    int Id { get; }
}

class Sensor(int id) : ISensor
{
    public int Id { get; } = id;
}

interface ISmartHome
{
    IReadOnlyList<ISensor> Sensors { get; }
}

class SmartHome(Func<int, ISensor> sensorFactory) : ISmartHome
{
    public IReadOnlyList<ISensor> Sensors { get; } =
    [
        // Use the injected factory to create a sensor with ID 101
        sensorFactory(101),

        // Create another sensor with ID 102
        sensorFactory(102)
    ];
}

To run the above code, the following NuGet packages must be added:

Delayed dependency instantiation:

  • Injection of dependencies requiring runtime parameters
  • Creation of distinct instances with different configurations
  • Type-safe resolution of dependencies with constructor arguments Limitations: runtime arguments improve flexibility but can increase coupling between call sites and construction signatures. Common pitfalls:
  • Passing infrastructure concerns as runtime arguments instead of normal dependencies.
  • Duplicating argument validation logic across consumers. See also: Injection on demand, Root arguments.

Smart tags

Large object graphs often need many tags. String tags are error-prone and easy to mistype. Prefer Enum values as tags, and Pure.DI helps make this safe. Smart tags improve refactoring safety by moving tag usage into compiler-checked symbols.

When the compiler cannot determine a tag value, Pure.DI generates a constant inside Pure.DI.Tag. For the example below, the generated constants would look like this:

namespace Pure.DI
{
  internal partial class Tag
  {
    public const string Abc = "Abc";
    public const string Xyz = "Xyz";
  }
}

This enables safe refactoring and compiler-checked tag usage, reducing errors.

The example below also uses the using static Pure.DI.Tag; directive to access tags in Pure.DI.Tag without specifying a type name:

using Shouldly;
using Pure.DI;

using static Pure.DI.Tag;
using static Pure.DI.Lifetime;

DI.Setup(nameof(Composition))
    // The `default` tag is used to resolve dependencies
    // when the tag was not specified by the consumer
    .Bind<IMessageSender>(Email, default).To<EmailSender>()
    .Bind<IMessageSender>(Sms).As(Singleton).To<SmsSender>()
    .Bind<IMessagingService>().To<MessagingService>()

    // "SmsSenderRoot" is root name, Sms is tag
    .Root<IMessageSender>("SmsSenderRoot", Sms)

    // Specifies to create the composition root named "Root"
    .Root<IMessagingService>("MessagingService");

var composition = new Composition();
var messagingService = composition.MessagingService;
messagingService.EmailSender.ShouldBeOfType<EmailSender>();
messagingService.SmsSender.ShouldBeOfType<SmsSender>();
messagingService.SmsSender.ShouldBe(composition.SmsSenderRoot);
messagingService.DefaultSender.ShouldBeOfType<EmailSender>();

interface IMessageSender;

class EmailSender : IMessageSender;

class SmsSender : IMessageSender;

interface IMessagingService
{
    IMessageSender EmailSender { get; }

    IMessageSender SmsSender { get; }

    IMessageSender DefaultSender { get; }
}

class MessagingService(
    [Tag(Email)] IMessageSender emailSender,
    [Tag(Sms)] IMessageSender smsSender,
    IMessageSender defaultSender)
    : IMessagingService
{
    public IMessageSender EmailSender { get; } = emailSender;

    public IMessageSender SmsSender { get; } = smsSender;

    public IMessageSender DefaultSender { get; } = defaultSender;
}

To run the above code, the following NuGet packages must be added:

Note

Smart tags provide compile-time safety for tag values, reducing runtime errors and improving code maintainability. Limitations: smart tags reduce typo risk, but tag policy still needs clear naming and ownership conventions. Common pitfalls:

  • Mixing string literals and smart tags in the same area without a migration plan.
  • Treating generated tag constants as domain concepts instead of DI composition details. See also: Tags, Generics.

Simplified lifetime-specific bindings

You can use the Transient<>(), Singleton<>(), PerResolve<>(), etc. methods. In this case binding will be performed for the implementation type itself, and if the implementation is not an abstract type or structure, for all abstract but NOT special types that are directly implemented. This keeps lifetime configuration concise while preserving explicit lifetime semantics.

using System.Collections;
using Pure.DI;

// Specifies to create a partial class "Composition"
DI.Setup(nameof(Composition))
    // The equivalent of the following:
    // .Bind<IOrderRepository, IOrderNotification, OrderManager>()
    //   .As(Lifetime.PerBlock)
    //   .To<OrderManager>()
    .PerBlock<OrderManager>()
    // The equivalent of the following:
    // .Bind<IShop, Shop>()
    //   .As(Lifetime.Transient)
    //   .To<Shop>()
    // .Bind<IOrderNameFormatter, OrderNameFormatter>()
    //   .As(Lifetime.Transient)
    //   .To<OrderNameFormatter>()
    .Transient<Shop, OrderNameFormatter>()

    // Specifies to create a property "MyShop"
    .Root<IShop>("MyShop");

var composition = new Composition();
var shop = composition.MyShop;

interface IManager;

class ManagerBase : IManager;

interface IOrderRepository;

interface IOrderNotification;

class OrderManager(IOrderNameFormatter orderNameFormatter) :
    ManagerBase,
    IOrderRepository,
    IOrderNotification,
    IDisposable,
    IEnumerable<string>
{
    public void Dispose() {}

    public IEnumerator<string> GetEnumerator() =>
        new List<string>
        {
            orderNameFormatter.Format(1),
            orderNameFormatter.Format(2)
        }.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

interface IOrderNameFormatter
{
    string Format(int orderId);
}

class OrderNameFormatter : IOrderNameFormatter
{
    public string Format(int orderId) => $"Order #{orderId}";
}

interface IShop;

class Shop(
    OrderManager manager,
    IOrderRepository repository,
    IOrderNotification notification)
    : IShop;

To run the above code, the following NuGet package must be added:

These methods perform the binding with appropriate lifetime:

  • with the implementation type itself
  • and if it is NOT an abstract type or structure
    • with all abstract types that it directly implements
    • exceptions are special types

Special types will not be added to bindings:

  • System.Object
  • System.Enum
  • System.MulticastDelegate
  • System.Delegate
  • System.Collections.IEnumerable
  • System.Collections.Generic.IEnumerable<T>
  • System.Collections.Generic.IList<T>
  • System.Collections.Generic.ICollection<T>
  • System.Collections.IEnumerator
  • System.Collections.Generic.IEnumerator<T>
  • System.Collections.Generic.IReadOnlyList<T>
  • System.Collections.Generic.IReadOnlyCollection<T>
  • System.IDisposable
  • System.IAsyncResult
  • System.AsyncCallback

If you want to add your own special type, use the SpecialType<T>() call.

For class OrderManager, the PerBlock<OrderManager>() binding will be equivalent to the Bind<IOrderRepository, IOrderNotification, OrderManager>().As(Lifetime.PerBlock).To<OrderManager>() binding. The types IDisposable, IEnumerable<string> did not get into the binding because they are special from the list above. ManagerBase did not get into the binding because it is not abstract. IManager is not included because it is not implemented directly by class OrderManager.

yesOrderManagerimplementation type itself
yesIOrderRepositorydirectly implements
yesIOrderNotificationdirectly implements
noIDisposablespecial type
noIEnumerable<string>special type
noManagerBasenon-abstract
noIManageris not directly implemented by class OrderManager
Limitations: lifetime-specific shortcuts still rely on inferred contracts, so review inferred bindings carefully.
Common pitfalls:
  • Applying singleton shortcuts to stateful services without thread-safety guarantees.
  • Assuming shortcut APIs bypass special-type exclusion rules. See also: Transient, Simplified binding.

Simplified lifetime-specific factory

Lifetime-named shortcuts such as Transient(...) and Singleton(...) register a factory and its lifetime in a single call, replacing the longer Bind().As(...).To(...) chain. Overloads accept a plain lambda (optionally with a tag, like Transient(() => DateTime.Today, "today")) or a lambda whose parameters are injected dependencies — parameters may carry attributes such as [Tag] — so you can initialize the instance before returning it, as Singleton<FileLogger, DateTime, IFileLogger> does when setting up the log file name.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Transient(Guid.NewGuid)
    .Transient(() => DateTime.Today, "today")
    // Injects FileLogger and DateTime instances
    // and performs further initialization logic
    // defined in the lambda function to set up the log file name
    .Singleton<FileLogger, DateTime, IFileLogger>((
        logger,
        [Tag("today")] date) => {
        logger.Init($"app-{date:yyyy-MM-dd}.log");
        return logger;
    })
    .Transient<OrderProcessingService>()

    // Composition root
    .Root<IOrderProcessingService>("OrderService");

var composition = new Composition();
var service = composition.OrderService;

service.Logger.FileName.ShouldBe($"app-{DateTime.Today:yyyy-MM-dd}.log");

interface IFileLogger
{
    string FileName { get; }

    void Log(string message);
}

class FileLogger(Func<Guid> idFactory) : IFileLogger
{
    public string FileName { get; private set; } = "";

    public void Init(string fileName) => FileName = fileName;

    public void Log(string message)
    {
        var id = idFactory();
        // Write to file
    }
}

interface IOrderProcessingService
{
    IFileLogger Logger { get; }
}

class OrderProcessingService(IFileLogger logger) : IOrderProcessingService
{
    public IFileLogger Logger { get; } = logger;
}

To run the above code, the following NuGet packages must be added:

Note

Lifetime-specific factories combine the convenience of simplified syntax with explicit lifetime control for optimal performance and correctness.

Method injection

To use dependency injection for a method, simply add the Dependency (or Ordinal) attribute to that method, specifying the sequence number that will be used to define the call to that method:

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind<IMap>().To<Map>()
    .Bind<INavigator>().To<Navigator>()

    // Composition root
    .Root<INavigator>("Navigator");

var composition = new Composition();
var navigator = composition.Navigator;
navigator.CurrentMap.ShouldBeOfType<Map>();

interface IMap;

class Map : IMap;

interface INavigator
{
    IMap? CurrentMap { get; }
}

class Navigator : INavigator
{
    // The Dependency (or Ordinal) attribute indicates that the method
    // should be called to inject the dependency.
    [Dependency(ordinal: 0)]
    public void LoadMap(IMap map) =>
        CurrentMap = map;

    public IMap? CurrentMap { get; private set; }
}

To run the above code, the following NuGet packages must be added:

The key points are:

  • The method must be available to be called from a composition class
  • The Dependency (or Ordinal) attribute is used to mark the method for injection
  • The DI automatically calls the method to inject dependencies

Property injection

To use dependency injection on a property, make sure the property is writable and simply add the Ordinal attribute to that property, specifying the ordinal that will be used to determine the injection order:

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind<ILogger>().To<ConsoleLogger>()
    .Bind<IService>().To<Service>()

    // Composition root
    .Root<IService>("MyService");

var composition = new Composition();
var service = composition.MyService;
service.Logger.ShouldBeOfType<ConsoleLogger>();

interface ILogger;

class ConsoleLogger : ILogger;

interface IService
{
    ILogger? Logger { get; }
}

class Service : IService
{
    // The Dependency attribute specifies to perform an injection,
    // the integer value in the argument specifies
    // the ordinal of injection.
    // Usually, property injection is used for optional dependencies.
    [Dependency] public ILogger? Logger { get; set; }
}

To run the above code, the following NuGet packages must be added:

The key points are:

  • The property must be writable
  • The Dependency (or Ordinal) attribute is used to mark the property for injection
  • The DI automatically injects the dependency when resolving the object graph

Field injection

To use dependency injection for a field, make sure the field is writable and simply add the Ordinal attribute to that field, specifying an ordinal that will be used to determine the injection order:

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind<ICoffeeMachine>().To<CoffeeMachine>()
    .Bind<ISmartKitchen>().To<SmartKitchen>()

    // Composition root
    .Root<ISmartKitchen>("Kitchen");

var composition = new Composition();
var kitchen = composition.Kitchen;
kitchen.CoffeeMachine.ShouldBeOfType<CoffeeMachine>();

interface ICoffeeMachine;

class CoffeeMachine : ICoffeeMachine;

interface ISmartKitchen
{
    ICoffeeMachine? CoffeeMachine { get; }
}

class SmartKitchen : ISmartKitchen
{
    // The Dependency attribute specifies to perform an injection.
    // The DI will automatically assign a value to this field
    // when creating the SmartKitchen instance.
    [Dependency]
    public ICoffeeMachine? CoffeeMachineImpl;

    // Expose the injected dependency through a public property
    public ICoffeeMachine? CoffeeMachine => CoffeeMachineImpl;
}

To run the above code, the following NuGet packages must be added:

The key points are:

  • The field must be writable
  • The Dependency (or Ordinal) attribute is used to mark the field for injection
  • The DI automatically injects the dependency when resolving the object graph

Default values

This example shows how to use default values in dependency injection when explicit injection is not possible.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind<ISensor>().To<MotionSensor>()
    .Bind<ISecuritySystem>().To<SecuritySystem>()

    // Composition root
    .Root<ISecuritySystem>("SecuritySystem");

var composition = new Composition();
var securitySystem = composition.SecuritySystem;
securitySystem.Sensor.ShouldBeOfType<MotionSensor>();
securitySystem.SystemName.ShouldBe("Home Guard");

interface ISensor;

class MotionSensor : ISensor;

interface ISecuritySystem
{
    string SystemName { get; }

    ISensor Sensor { get; }
}

// If injection cannot be performed explicitly,
// the default value will be used
class SecuritySystem(string systemName = "Home Guard") : ISecuritySystem
{
    public string SystemName { get; } = systemName;

    // The 'required' modifier ensures that the property is set during initialization.
    // The default value 'new MotionSensor()' provides a fallback
    // if no dependency is injected.
    public required ISensor Sensor { get; init; } = new MotionSensor();
}

To run the above code, the following NuGet packages must be added:

The key points are:

  • Default constructor arguments can be used for simple values
  • The DI will use these defaults if no explicit bindings are provided

This example shows how to handle default values in a dependency injection scenario:

  • Constructor Default Argument: The SecuritySystem class has a constructor with a default value for the name parameter. If no value is provided, "Home Guard" will be used.
  • Required Property with Default: The Sensor property is marked as required but has a default instantiation. This ensures that:
    • The property must be set
    • If no explicit injection occurs, a default value will be used

Required properties or fields

This example shows how the required modifier can be used to automatically inject dependencies into properties and fields. When a property or field is marked with required, the DI will automatically inject the dependency without additional effort.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Arg<string>("connectionString")
    .Bind<IDatabase>().To<SqlDatabase>()
    .Bind<IUserRepository>().To<UserRepository>()

    // Composition root
    .Root<IUserRepository>("Repository");

var composition = new Composition(connectionString: "Server=.;Database=MyDb;");
var repository = composition.Repository;

repository.Database.ShouldBeOfType<SqlDatabase>();
repository.ConnectionString.ShouldBe("Server=.;Database=MyDb;");

interface IDatabase;

class SqlDatabase : IDatabase;

interface IUserRepository
{
    string ConnectionString { get; }

    IDatabase Database { get; }
}

class UserRepository : IUserRepository
{
    // The required field will be injected automatically.
    // In this case, it gets the value from the composition argument
    // of type 'string'.
    public required string ConnectionStringField;

    public string ConnectionString => ConnectionStringField;

    // The required property will be injected automatically
    // without additional effort.
    public required IDatabase Database { get; init; }
}

To run the above code, the following NuGet packages must be added:

This approach simplifies dependency injection by eliminating the need to manually configure bindings for required dependencies, making the code more concise and easier to maintain.

Build up of an existing object

This example shows the Build-Up pattern in dependency injection, where an existing object is injected with necessary dependencies through its properties, methods, or fields.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .RootArg<string>("name")
    .Bind().To(Guid.NewGuid)
    .Bind().To(ctx => {
        var person = new Person();
        // Injects dependencies into an existing object
        ctx.BuildUp(person);
        return person;
    })
    .Bind().To<Greeter>()

    // Composition root
    .Root<IGreeter>("GetGreeter");

var composition = new Composition();
var greeter = composition.GetGreeter("Nik");

greeter.Person.Name.ShouldBe("Nik");
greeter.Person.Id.ShouldNotBe(Guid.Empty);

interface IPerson
{
    string Name { get; }

    Guid Id { get; }
}

class Person : IPerson
{
    // The Dependency attribute specifies to perform an injection and its order
    [Dependency] public string Name { get; set; } = "";

    public Guid Id { get; private set; } = Guid.Empty;

    // The Dependency attribute specifies to perform an injection and its order
    [Dependency] public void SetId(Guid id) => Id = id;
}

interface IGreeter
{
    IPerson Person { get; }
}

record Greeter(IPerson Person) : IGreeter;

To run the above code, the following NuGet packages must be added:

Key Concepts: Build-Up - injecting dependencies into an already created object Dependency Attribute - marker for identifying injectable members

Builder

Sometimes you need to build up an existing composition root and inject all of its dependencies, in which case the Builder method will be useful, as in the example below:

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind().To(Guid.NewGuid)
    .Bind().To<PhotonBlaster>()
    .Builder<Player>("Equip");

var composition = new Composition();

// The Game Engine instantiates the Player entity,
// so we need to inject dependencies into the existing instance.
var player = composition.Equip(new Player());

player.Id.ShouldNotBe(Guid.Empty);
player.Weapon.ShouldBeOfType<PhotonBlaster>();

interface IWeapon;

class PhotonBlaster : IWeapon;

interface IGameEntity
{
    Guid Id { get; }

    IWeapon? Weapon { get; }
}

record Player : IGameEntity
{
    public Guid Id { get; private set; } = Guid.Empty;

    // The Dependency attribute specifies to perform an injection
    [Dependency]
    public IWeapon? Weapon { get; set; }

    [Dependency]
    public void SetId(Guid id) => Id = id;
}

To run the above code, the following NuGet packages must be added:

Important Notes:

  • The default builder method name is BuildUp
  • The first argument to the builder method is always the instance to be built

Advantages:

  • Allows working with pre-existing objects
  • Provides flexibility in dependency injection
  • Supports partial injection of dependencies
  • Can be used with legacy code

Use Cases:

  • When objects are created outside the DI
  • For working with third-party libraries
  • When migrating existing code to DI
  • For complex object graphs where full construction is not feasible

Builder with arguments

This example shows how to use builders with custom arguments in dependency injection. It shows how to pass additional parameters during the build-up process.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .RootArg<Guid>("id")
    .Bind().To<TelemetrySystem>()
    .Builder<Satellite>("Initialize");

var composition = new Composition();

var id = Guid.NewGuid();
var satellite = composition.Initialize(new Satellite(), id);
satellite.Id.ShouldBe(id);
satellite.Telemetry.ShouldBeOfType<TelemetrySystem>();

interface ITelemetrySystem;

class TelemetrySystem : ITelemetrySystem;

interface ISatellite
{
    Guid Id { get; }

    ITelemetrySystem? Telemetry { get; }
}

record Satellite : ISatellite
{
    public Guid Id { get; private set; } = Guid.Empty;

    // The Dependency attribute specifies to perform an injection
    [Dependency]
    public ITelemetrySystem? Telemetry { get; set; }

    [Dependency]
    public void SetId(Guid id) => Id = id;
}

To run the above code, the following NuGet packages must be added:

Important Notes:

  • The default builder method name is BuildUp
  • The first argument to the builder method is always the instance to be built
  • Additional arguments are passed in the order they are defined in the setup
  • Root arguments can be used to provide custom values during build-up

Use Cases:

  • When additional parameters are required during object construction
  • For scenarios where dependencies depend on runtime values
  • When specific initialization data is needed
  • For conditional injection based on provided arguments

Best Practices

  • Keep the number of builder arguments minimal
  • Use meaningful names for root arguments

Builders

Sometimes you need builders for all types derived from T that are known at compile time.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind().To(Guid.NewGuid)
    .Bind().To<PlutoniumBattery>()
    // Creates a builder for each type inherited from IRobot.
    // These types must be available at this point in the code.
    .Builders<IRobot>("BuildUp", filter: "*Bot");

var composition = new Composition();

var cleaner = composition.BuildUp(new CleanerBot());
cleaner.Token.ShouldNotBe(Guid.Empty);
cleaner.Battery.ShouldBeOfType<PlutoniumBattery>();

var guard = composition.BuildUp(new GuardBot());
guard.Token.ShouldBe(Guid.Empty);
guard.Battery.ShouldBeOfType<PlutoniumBattery>();

// Uses a common method to build an instance
IRobot robot = new CleanerBot();
robot = composition.BuildUp(robot);
robot.ShouldBeOfType<CleanerBot>();
robot.Token.ShouldNotBe(Guid.Empty);
robot.Battery.ShouldBeOfType<PlutoniumBattery>();

// Uses a safe common method when the runtime subtype may be unknown.
var externalRobot = new ExternalRobot();
composition.TryBuildUp(externalRobot).ShouldBeFalse();
externalRobot.Battery.ShouldBeNull();

// The strict builder still throws for unknown runtime subtypes.
Should.Throw<ArgumentException>(() => composition.BuildUp(externalRobot));

interface IBattery;

class PlutoniumBattery : IBattery;

interface IRobot
{
    Guid Token { get; }

    IBattery? Battery { get; }
}

record CleanerBot : IRobot
{
    public Guid Token { get; private set; } = Guid.Empty;

    // The Dependency attribute specifies to perform an injection
    [Dependency]
    public IBattery? Battery { get; set; }

    [Dependency]
    public void SetToken(Guid token) => Token = token;
}

record GuardBot : IRobot
{
    public Guid Token => Guid.Empty;

    [Dependency]
    public IBattery? Battery { get; set; }
}

record ExternalRobot : IRobot
{
    public Guid Token => Guid.Empty;

    public IBattery? Battery => null;
}

To run the above code, the following NuGet packages must be added:

Important Notes:

  • The default builder method name is BuildUp
  • The first argument to the builder method is always the instance to be built
  • Builders<T> also generates TryBuildUp for safe build-up when the runtime subtype may be unknown

Builders with a name template

Sometimes you need to build up an existing composition root and inject all of its dependencies, in which case the Builder method will be useful, as in the example below:

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind().To(Guid.NewGuid)
    .Bind().To<WiFi>()
    // Creates a builder based on the name template
    // for each type inherited from IDevice.
    // These types must be available at this point in the code.
    .Builders<IDevice>("Install{type}");

var composition = new Composition();

var webcam = composition.InstallWebcam(new Webcam());
webcam.Id.ShouldNotBe(Guid.Empty);
webcam.Network.ShouldBeOfType<WiFi>();

var thermostat = composition.InstallThermostat(new Thermostat());
thermostat.Id.ShouldBe(Guid.Empty);
thermostat.Network.ShouldBeOfType<WiFi>();

// Uses a common method to build an instance
IDevice device = new Webcam();
device = composition.InstallIDevice(device);
device.ShouldBeOfType<Webcam>();
device.Id.ShouldNotBe(Guid.Empty);
device.Network.ShouldBeOfType<WiFi>();

interface INetwork;

class WiFi : INetwork;

interface IDevice
{
    Guid Id { get; }

    INetwork? Network { get; }
}

record Webcam : IDevice
{
    public Guid Id { get; private set; } = Guid.Empty;

    // The Dependency attribute specifies to perform an injection
    [Dependency]
    public INetwork? Network { get; set; }

    [Dependency]
    public void SetId(Guid id) => Id = id;
}

record Thermostat : IDevice
{
    public Guid Id => Guid.Empty;

    // The Dependency attribute specifies to perform an injection
    [Dependency]
    public INetwork? Network { get; set; }
}

To run the above code, the following NuGet packages must be added:

The default builder method name is BuildUp. The first argument to this method will always be the instance to be built.

Overrides

This example shows advanced dependency injection techniques using Pure.DI's override mechanism to customize dependency instantiation with runtime arguments and tagged parameters. The implementation creates multiple IDependency instances with values manipulated through explicit overrides.

using Shouldly;
using Pure.DI;
using System.Collections.Immutable;
using System.Drawing;

DI.Setup(nameof(Composition))
    .Bind(Tag.Red).To(() => Color.Red)
    .Bind().As(Lifetime.Singleton).To<Clock>()
    // The factory accepts the widget ID and the layer index
    .Bind().To<Func<int, int, IWidget>>(ctx =>
        (widgetId, layerIndex) => {
            // Overrides the 'id' argument of the constructor with the first lambda argument
            ctx.Override(widgetId);

            // Overrides the 'layer' tagged argument of the constructor with the second lambda argument
            ctx.Override(layerIndex, "layer");

            // Overrides the 'name' argument with a formatted string
            ctx.Override($"Widget {widgetId} on layer {layerIndex}");

            // Resolves the 'Color' dependency tagged with 'Red'
            ctx.Inject(Tag.Red, out Color color);
            // Overrides the 'color' argument with the resolved value
            ctx.Override(color);

            // Creates the instance using the overridden values
            ctx.Inject<Widget>(out var widget);
            return widget;
        })
    .Bind().To<Dashboard>()

    // Composition root
    .Root<IDashboard>("Dashboard");

var composition = new Composition();
var dashboard = composition.Dashboard;
dashboard.Widgets.Length.ShouldBe(3);

dashboard.Widgets[0].Id.ShouldBe(0);
dashboard.Widgets[0].Layer.ShouldBe(99);
dashboard.Widgets[0].Name.ShouldBe("Widget 0 on layer 99");

dashboard.Widgets[1].Id.ShouldBe(1);
dashboard.Widgets[1].Name.ShouldBe("Widget 1 on layer 99");

dashboard.Widgets[2].Id.ShouldBe(2);
dashboard.Widgets[2].Name.ShouldBe("Widget 2 on layer 99");

interface IClock
{
    DateTimeOffset Now { get; }
}

class Clock : IClock
{
    public DateTimeOffset Now => DateTimeOffset.Now;
}

interface IWidget
{
    string Name { get; }

    int Id { get; }

    int Layer { get; }
}

class Widget(
    string name,
    IClock clock,
    int id,
    [Tag("layer")] int layer,
    Color color)
    : IWidget
{
    public string Name => name;

    public int Id => id;

    public int Layer => layer;
}

interface IDashboard
{
    ImmutableArray<IWidget> Widgets { get; }
}

class Dashboard(Func<int, int, IWidget> widgetFactory) : IDashboard
{
    public ImmutableArray<IWidget> Widgets { get; } =
    [
        widgetFactory(0, 99),
        widgetFactory(1, 99),
        widgetFactory(2, 99)
    ];
}

To run the above code, the following NuGet packages must be added:

Note

Overrides provide fine-grained control over dependency resolution, allowing you to customize bindings at runtime or for specific scenarios.

Nullable reference types

Pure.DI preserves nullable reference type annotations when it reads dependency contracts, builds the graph, and generates composition members. Use nullable dependencies for values that are allowed to be absent. A nullable root or composition argument does not get a generated null check, while a non-null reference argument still does. A non-null binding can satisfy a nullable dependency request. This is useful for optional constructor parameters, nullable factory results, and nullable collection elements.

Tip

T? means that the consumer can handle null; it does not mean that a missing binding is ignored. If no binding or auto-binding can provide the type, Pure.DI still reports the graph error. [!NOTE] When a nullable reference type is used as a generic argument, the generic type must allow nullable arguments. For example, prefer where T : class? over where T : class for contracts such as IBox<string?>; otherwise the C# compiler reports a nullable constraint warning before Pure.DI analyzes the graph.

using Shouldly;
using Pure.DI;
using System.Collections.Generic;
using System.Linq;

DI.Setup(nameof(Composition))
    .Hint(Hint.Resolve, "Off")
    .Bind<IDatabase>().To<Database>()
    .Bind<IReportService>().To<ReportService>()

    // Nullable composition argument: no generated null check
    .Arg<string?>("defaultTitle", "title")

    // Nullable root argument: no generated null check
    .RootArg<string?>("connectionString", "connection")

    // Composition root
    .Root<IReportService>("CreateReportService");

var composition = new Composition(defaultTitle: null);
var reportService = composition.CreateReportService(connectionString: null);

reportService.DefaultTitle.ShouldBeNull();
reportService.ConnectionString.ShouldBeNull();
reportService.OptionalDatabase.ShouldNotBeNull();
reportService.Databases.Count.ShouldBe(1);

interface IDatabase;

class Database : IDatabase;

interface IReportService
{
    string? DefaultTitle { get; }

    string? ConnectionString { get; }

    IDatabase? OptionalDatabase { get; }

    IReadOnlyList<IDatabase?> Databases { get; }
}

class ReportService(
    [Tag("title")] string? defaultTitle,
    [Tag("connection")] string? connectionString,
    IDatabase? optionalDatabase,
    IEnumerable<IDatabase?> databases)
    : IReportService
{
    public string? DefaultTitle { get; } = defaultTitle;

    public string? ConnectionString { get; } = connectionString;

    public IDatabase? OptionalDatabase { get; } = optionalDatabase;

    public IReadOnlyList<IDatabase?> Databases { get; } = databases.ToList();
}

To run the above code, the following NuGet packages must be added:

Limitations: nullable annotations describe compile-time contracts. They are not runtime validation rules and do not replace explicit domain validation. Common pitfalls:

  • Using T? to hide a missing binding instead of modelling an optional value.
  • Forgetting tags for nullable primitive values when several values of the same type exist.
  • Assuming IEnumerable<T?> changes the lifetime of elements; lifetime still comes from the matched bindings.
  • Declaring generic contracts with where T : class and then consuming them as T?; use a nullable-aware constraint such as where T : class? when nullable generic arguments are valid. See also: Composition arguments, Root arguments, Injection on demand.

Root binding

In general, it is recommended to define one composition root for the entire application. But Sometimes you need to have multiple roots. To simplify the definition of composition roots, a "hybrid" API method RootBind<T>(string rootName) was added. It lets you define a binding and at the same time the root of the composition. You can it in order to reduce repetitions. The registration composition.RootBind<IDependency>().To<Dependency>() is an equivalent to composition.Bind<IDependency>().To<Dependency>().Root<IDependency>().

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind().As(Lifetime.Singleton).To<DbConnection>()
    // RootBind lets you define a binding and a composition root
    // simultaneously. This is useful for creating public entry points
    // for your application components while keeping the configuration concise.
    .RootBind<IOrderService>("OrderService").To<OrderService>();

// The line above is functionally equivalent to:
//  .Bind<IOrderService>().To<OrderService>()
//  .Root<IOrderService>("OrderService")

var composition = new Composition();
var orderService = composition.OrderService;
orderService.ShouldBeOfType<OrderService>();

interface IDbConnection;

class DbConnection : IDbConnection;

interface IOrderService;

class OrderService(IDbConnection connection) : IOrderService;

To run the above code, the following NuGet packages must be added:

Note

RootBind reduces boilerplate when you need both a binding and a root for the same type.

Static root

Passing kind: RootKinds.Static to Root<T>(...) makes the generated root a static member, so an instance can be obtained directly from the composition type — Composition.GlobalConfiguration — without creating a composition object. This is useful for stateless entry-point services such as static configuration readers, validators, or one-shot command helpers where the composition itself does not carry state.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind().As(Lifetime.PerResolve).To<FileSystem>()
    .Bind().To<Configuration>()
    .Root<IConfiguration>("GlobalConfiguration", kind: RootKinds.Static);

var configuration = Composition.GlobalConfiguration;
configuration.ShouldBeOfType<Configuration>();

interface IFileSystem;

class FileSystem : IFileSystem;

interface IConfiguration;

class Configuration(IFileSystem fileSystem) : IConfiguration;

To run the above code, the following NuGet packages must be added:

Note

Static roots keep the call site compact and avoid allocating a composition instance for graphs that do not need composition-level state. Avoid static roots for graphs that depend on scoped state, per-composition caches, or externally supplied constructor arguments. In those cases, an instance composition keeps ownership and lifetime boundaries clearer.

Async Root

A composition root can be asynchronous: declare it as Root<Task<IService>>(...) and Pure.DI generates a root method you can await. This is useful when building the object graph is costly and you don't want to block the caller. Add RootArg<CancellationToken>("cancellationToken") to pass a cancellation token that is used when resolving the root.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind<IFileStore>().To<FileStore>()
    .Bind<IBackupService>().To<BackupService>()

    // Specifies to use CancellationToken from the argument
    // when resolving a composition root
    .RootArg<CancellationToken>("cancellationToken")

    // Composition root
    .Root<Task<IBackupService>>("GetBackupServiceAsync");

var composition = new Composition();

// Resolves composition roots asynchronously
var service = await composition.GetBackupServiceAsync(CancellationToken.None);

interface IFileStore;

class FileStore : IFileStore;

interface IBackupService;

class BackupService(IFileStore fileStore) : IBackupService;

To run the above code, the following NuGet packages must be added:

Note

Async roots are useful when you need to perform asynchronous initialization or when your services require async creation.

Roots

Sometimes you need roots for all types inherited from available at compile time at the point where the method is called.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind().As(Lifetime.Singleton).To<Preferences>()
    // Roots can be used to register all descendants of a type as roots.
    .Roots<IWindow>("{type}");

var composition = new Composition();
composition.MainWindow.ShouldBeOfType<MainWindow>();
composition.SettingsWindow.ShouldBeOfType<SettingsWindow>();

interface IPreferences;

class Preferences : IPreferences;

interface IWindow;

class MainWindow(IPreferences preferences) : IWindow;

class SettingsWindow(IPreferences preferences) : IWindow;

To run the above code, the following NuGet packages must be added:

Note

This feature is useful for plugin-style architectures where you need to expose all implementations of a base type or interface.

Roots with filter

Roots<T>(name, filter) creates a composition root for every implementation of T whose type name matches a wildcard filter, with {type} in the name template replaced by each type's name. Filtering matters when some implementations should not be exposed: here filter: "*Email*" picks up EmailService but skips SmsService, whose string apiKey dependency has no binding and therefore cannot be resolved.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind().As(Lifetime.Singleton).To<Configuration>()
    .Roots<INotificationService>("My{type}", filter: "*Email*");

var composition = new Composition();
composition.MyEmailService.ShouldBeOfType<EmailService>();

interface IConfiguration;

class Configuration : IConfiguration;

interface INotificationService;

// This service requires an API key which is not bound,
// so it cannot be resolved and should be filtered out.
class SmsService(string apiKey) : INotificationService;

class EmailService(IConfiguration config) : INotificationService;

To run the above code, the following NuGet packages must be added:

Note

Filtering roots provides fine-grained control over which implementations are exposed, useful for conditional feature activation.

Consumer type

ConsumerType is used to get the consumer type of the given dependency. The use of ConsumerType is demonstrated on the example of Serilog library:

using Shouldly;
using Serilog.Core;
using Serilog.Events;
using Pure.DI;
using Serilog.Core;

Serilog.ILogger serilogLogger = new Serilog.LoggerConfiguration().CreateLogger();
var composition = new Composition(logger: serilogLogger);
var service = composition.Root;

interface IDependency;

class Dependency : IDependency
{
    public Dependency(Serilog.ILogger log)
    {
        log.Information("Dependency created");
    }
}

interface IService
{
    IDependency Dependency { get; }
}

class Service : IService
{
    public Service(
        Serilog.ILogger log,
        IDependency dependency)
    {
        Dependency = dependency;
        log.Information("Service initialized");
    }

    public IDependency Dependency { get; }
}

partial class Composition
{
    private void Setup() =>

        DI.Setup(nameof(Composition))
            .Arg<Serilog.ILogger>("logger", "from arg")
            .Bind().To(ctx => {
                ctx.Inject<Serilog.ILogger>("from arg", out var logger);

                // Enriches the logger with the specific context of the consumer type.
                // ctx.ConsumerType represents the type into which the dependency is being injected.
                // This allows logs to be tagged with the name of the class that created them.
                return logger.ForContext(ctx.ConsumerType);
            })
            .Bind().To<Dependency>()
            .Bind().To<Service>()
            .Root<IService>(nameof(Root));
}

To run the above code, the following NuGet packages must be added:

Note

ConsumerType is useful for creating context-aware loggers or when you need to know which type is consuming a dependency.

Ref dependencies

High-performance code often relies on ref struct types such as Span<T>, which cannot be stored in fields — so ordinary constructor or property injection is off the table. Instead, inject them by ref through a method marked with [Ordinal]: here a ref struct Data wrapping the bound int[] is passed into Initialize(ref Data data) and consumed without extra allocations.

using Shouldly;
using Pure.DI;

DI.Setup("Composition")
    // Represents a large data set or buffer
    .Bind().To<int[]>(() => [10, 20, 30])
    .Root<Service>("MyService");

var composition = new Composition();
var service = composition.MyService;
service.Sum.ShouldBe(60);

class Service
{
    public int Sum { get; private set; }

    // Ref structs cannot be fields, so they are injected via a method
    // with the [Ordinal] attribute. This allows working with
    // high-performance types like Span<T> or other ref structs.
    [Ordinal]
    public void Initialize(ref Data data) =>
        Sum = data.Sum();
}

// A ref struct that holds a reference to the data
// to process it without additional memory allocations
readonly ref struct Data(ref int[] data)
{
    private readonly ref int[] _dep = ref data;

    public int Sum() => _dep.Sum();
}

To run the above code, the following NuGet packages must be added:

Note

ref injection through an [Ordinal] method lets dependencies use stack-only types like Span<T> and avoids copying large structs.

PerResolve

The PerResolve lifetime ensures that there will be one instance of the dependency for each composition root instance.

using Shouldly;
using Pure.DI;
using static Pure.DI.Lifetime;

DI.Setup(nameof(Composition))
    // PerResolve = one "planning session" per root access.
    // Imagine: each time you ask for a plan, you get a fresh context.
    .Bind().As(PerResolve).To<RoutePlanningSession>()

    // Singleton = created once per Composition instance.
    // Here it intentionally captures session when it's created the first time
    // (this is a realistic pitfall: singleton accidentally holds request-scoped state).
    .Bind().As(Singleton).To<(IRoutePlanningSession s3, IRoutePlanningSession s4)>()

    // Composition root
    .Root<TrainTripPlanner>("Planner");

var composition = new Composition();

// First "user request": plan a trip now
var plan1 = composition.Planner;

// In the same request, PerResolve dependencies are the same instance:
plan1.SessionForOutbound.ShouldBe(plan1.SessionForReturn);

// Tuple is Singleton, so both entries are the same captured instance:
plan1.CapturedSessionA.ShouldBe(plan1.CapturedSessionB);

// Because the singleton tuple was created during the first request,
// it captured THAT request's PerResolve session:
plan1.SessionForOutbound.ShouldBe(plan1.CapturedSessionA);

// Second "user request": plan another trip (new root access)
var plan2 = composition.Planner;

// New request => new PerResolve session:
plan2.SessionForOutbound.ShouldNotBe(plan1.SessionForOutbound);

// But the singleton still holds the old captured session from the first request:
plan2.CapturedSessionA.ShouldBe(plan1.CapturedSessionA);
plan2.SessionForOutbound.ShouldNotBe(plan2.CapturedSessionA);

// A request-scoped context: e.g., contains "now", locale, pricing rules version,
// feature flags, etc. You typically want a new one per route planning request.
interface IRoutePlanningSession;

class RoutePlanningSession : IRoutePlanningSession;

// A service that plans a train trip.
// It asks for two session instances to demonstrate PerResolve:
// both should be the same within a single request.
class TrainTripPlanner(
    IRoutePlanningSession sessionForOutbound,
    IRoutePlanningSession sessionForReturn,
    (IRoutePlanningSession capturedA, IRoutePlanningSession capturedB) capturedSessions)
{
    public IRoutePlanningSession SessionForOutbound { get; } = sessionForOutbound;

    public IRoutePlanningSession SessionForReturn { get; } = sessionForReturn;

    // These come from a singleton tuple - effectively "global cached" instances.
    public IRoutePlanningSession CapturedSessionA { get; } = capturedSessions.capturedA;

    public IRoutePlanningSession CapturedSessionB { get; } = capturedSessions.capturedB;
}

To run the above code, the following NuGet packages must be added:

Note

PerResolve lifetime is useful when you want to share a dependency instance within a single composition root resolution.

Func

Func helps when the logic must enter instances of some type on demand or more than once. This is a very handy mechanism for instance replication. For example it is used when implementing the Lazy<T> injection.

using Shouldly;
using Pure.DI;
using System.Collections.Immutable;

DI.Setup(nameof(Composition))
    .Bind().As(Lifetime.Singleton).To<TicketIdGenerator>()
    .Bind().To<Ticket>()
    .Bind().To<QueueTerminal>()

    // Composition root
    .Root<IQueueTerminal>("Terminal");

var composition = new Composition();
var terminal = composition.Terminal;

terminal.Tickets.Length.ShouldBe(3);

terminal.Tickets[0].Id.ShouldBe(1);
terminal.Tickets[1].Id.ShouldBe(2);
terminal.Tickets[2].Id.ShouldBe(3);

interface ITicketIdGenerator
{
    int NextId { get; }
}

class TicketIdGenerator : ITicketIdGenerator
{
    public int NextId => ++field;
}

interface ITicket
{
    int Id { get; }
}

class Ticket(ITicketIdGenerator idGenerator) : ITicket
{
    public int Id { get; } = idGenerator.NextId;
}

interface IQueueTerminal
{
    ImmutableArray<ITicket> Tickets { get; }
}

class QueueTerminal(Func<ITicket> ticketFactory) : IQueueTerminal
{
    public ImmutableArray<ITicket> Tickets { get; } =
    [
        // The factory creates a new instance of the ticket each time it is called
        ticketFactory(),
        ticketFactory(),
        ticketFactory()
    ];
}

To run the above code, the following NuGet packages must be added:

Be careful, replication takes into account the lifetime of the object.

Func with arguments

Sometimes an instance can only be created with values known at runtime, such as an id or a name. Injecting a Func<..., T> with arguments gives you a factory: values passed at call time are matched by type to the constructor parameters (here int id and string name of Person), while the remaining dependencies, like IClock, are resolved from the composition as usual.

using Shouldly;
using Pure.DI;
using System.Collections.Immutable;

DI.Setup(nameof(Composition))
    .Bind().As(Lifetime.Singleton).To<Clock>()
    .Bind().To<Person>()
    .Bind().To<Team>()

    // Composition root
    .Root<ITeam>("Team");

var composition = new Composition();
var team = composition.Team;

team.Members.Length.ShouldBe(3);

team.Members[0].Id.ShouldBe(10);
team.Members[0].Name.ShouldBe("Nik");

team.Members[1].Id.ShouldBe(20);
team.Members[1].Name.ShouldBe("Mike");

team.Members[2].Id.ShouldBe(30);
team.Members[2].Name.ShouldBe("Jake");

interface IClock
{
    DateTimeOffset Now { get; }
}

class Clock : IClock
{
    public DateTimeOffset Now => DateTimeOffset.Now;
}

interface IPerson
{
    int Id { get; }

    string Name { get; }
}

class Person(string name, IClock clock, int id)
    : IPerson
{
    public int Id => id;

    public string Name => name;
}

interface ITeam
{
    ImmutableArray<IPerson> Members { get; }
}

class Team(Func<int, string, IPerson> personFactory) : ITeam
{
    public ImmutableArray<IPerson> Members { get; } =
    [
        personFactory(10, "Nik"),
        personFactory(20, "Mike"),
        personFactory(30, "Jake")
    ];
}

To run the above code, the following NuGet packages must be added:

Note

Func with arguments provides flexibility for scenarios where you need to pass runtime parameters during instance creation.

Func with tag

A tag applied to a Func<T> dependency carries over to the instances it creates. Here [Tag("postgres")] Func<IDbConnection> resolves the binding registered with the "postgres" tag, and each call returns a new NpgsqlConnection, letting the pool create as many distinct connections as it needs.

using Shouldly;
using Pure.DI;
using System.Collections.Immutable;

DI.Setup(nameof(Composition))
    .Bind<IDbConnection>("postgres").To<NpgsqlConnection>()
    .Bind<IConnectionPool>().To<ConnectionPool>()

    // Composition root
    .Root<IConnectionPool>("ConnectionPool");

var composition = new Composition();
var pool = composition.ConnectionPool;

// Check that the pool has created 3 connections
pool.Connections.Length.ShouldBe(3);
pool.Connections[0].ShouldBeOfType<NpgsqlConnection>();

interface IDbConnection;

// Specific implementation for PostgreSQL
class NpgsqlConnection : IDbConnection;

interface IConnectionPool
{
    ImmutableArray<IDbConnection> Connections { get; }
}

class ConnectionPool([Tag("postgres")] Func<IDbConnection> connectionFactory) : IConnectionPool
{
    public ImmutableArray<IDbConnection> Connections { get; } =
    [
        // Use the factory to create distinct connection instances
        connectionFactory(),
        connectionFactory(),
        connectionFactory()
    ];
}

To run the above code, the following NuGet packages must be added:

Note

Func with tags allows you to create instances with specific tags dynamically, useful for factory patterns with multiple implementations.

Method injection for a hot path

Method injection is useful when a service has stable dependencies but the hot-path input changes on every call. Instead of storing request state in a heap object, pass the per-call value as a root argument and consume it immediately in an initialization method. The route matcher below has a singleton-like route table and receives a ReadOnlySpan<char> request path for each call. The generated root method passes the span into Match(...), so the stack-only value stays inside the current call frame and is not stored in a field or property.

using Shouldly;
using Pure.DI;
using System;

DI.Setup(nameof(Composition))
    .Bind<IRouteTable>().To<RouteTable>()
    .Bind<IRouteMatcher>().To<RouteMatcher>()
    .RootArg<ReadOnlySpan<char>>("path")
    .Root<IRouteMatcher>("CreateMatcher");

var composition = new Composition();

var matcher = composition.CreateMatcher("/orders/42".AsSpan());
matcher.Route.ShouldBe("orders");

interface IRouteTable
{
    string Find(ReadOnlySpan<char> path);
}

sealed class RouteTable : IRouteTable
{
    public string Find(ReadOnlySpan<char> path) =>
        path.StartsWith("/orders/".AsSpan(), StringComparison.Ordinal)
            ? "orders"
            : "not-found";
}

interface IRouteMatcher
{
    string Route { get; }
}

sealed class RouteMatcher(IRouteTable routeTable) : IRouteMatcher
{
    public string Route { get; private set; } = "";

    [Ordinal(0)]
    public void Match(ReadOnlySpan<char> path) =>
        Route = routeTable.Find(path);
}

To run the above code, the following NuGet packages must be added:

This is a good shape for parsers, routers, protocol decoders, validation pipelines, and other code that reads request data immediately. Keep the method side-effect small and do not store Span<T>/ReadOnlySpan<T> in the created object; Pure.DI reports diagnostics when stack-only values are routed to storage-like injection sites.

Span and ReadOnlySpan

Specifying Span<T> and ReadOnlySpan<T> work the same as with the array T[] for immediate constructor or method use.

using Shouldly;
using Pure.DI;

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>()

    // Composition root
    .Root<IPath>("Path");

var composition = new Composition();
var path = composition.Path;
path.PointCount.ShouldBe(3);

readonly struct Point(int x, int y)
{
    public int X { get; } = x;

    public int Y { get; } = y;
}

interface IPath
{
    int PointCount { get; }
}

class Path(ReadOnlySpan<Point> points) : IPath
{
    // The 'points' span is allocated on the stack, so it's very efficient.
    // However, we cannot store it in a field because it's a ref struct.
    // We can process it here in the constructor.
    public int PointCount { get; } = points.Length;
}

To run the above code, the following NuGet packages must be added:

This scenario is even more efficient in the case of Span<T> or ReadOnlySpan<T> when T is a value type. In this case, there is no heap allocation, and the composition root IPath looks like this:

public IPath Path
{
  get
  {
    ReadOnlySpan<Point> points = stackalloc Point[3] { new Point(1, 1), new Point(2, 2), new Point(3, 3) };
    return new Path(points);
  }
}

Constructor injection into a heap type is available for compatibility and reports warning DIW012. Prefer method injection for new code when the stack-only value is only needed during initialization. Generic root arguments with where T : allows ref struct follow the same rules. Pure.DI treats such T as maybe stack-only and emits scoped T in generated root signatures. When a root has several arguments, scoped is applied only to arguments that are stack-only or maybe stack-only themselves. Heap-safe wrappers such as Wrapper<T> remain regular parameters even when T allows ref structs. Factory bodies may resolve and consume stack-only values immediately via ctx.Inject<T>(...) when the API target supports allows ref struct. Pure.DI reports DIE049 when such values are captured behind a generated delegate or deferred factory. Factory overrides may pass stack-only values with ctx.Override<T>(...) or ctx.Let<T>(...) only inside the current synchronous factory frame or the current delegate invocation. Delegate arguments such as T text can be consumed immediately by method injection, but Pure.DI still reports DIE049 if an outer stack-only value is captured by the delegate or if text is passed into a nested/returned delegate. When manual ctx.Override<T>(...) or ctx.Let<T>(...) calls are used in factory delegates, the usual thread-safety rule still applies: wrap the override and the following injection in lock (ctx.Lock) or disable thread safety for known single-threaded compositions. Otherwise Pure.DI reports DIW013. The generated default Func<ReadOnlySpan<char>, T> binding does not use shared override state and does not require a manual lock; use it when the standard Func shape is enough. Generic interfaces are allowed when the implementation is heap-safe, for example class Parser<T> : IParser<T>. Pure.DI reports DIE048 only when the implementation itself is stack-only, such as ref struct Parser<T> : IParser<T>. Pure.DI reports errors when Span<T>, ReadOnlySpan<T>, or custom ref struct values are injected into fields, properties, stored lifetimes, delegate captures, or stack-only interface conversions.

Default Func with ReadOnlySpan

Pure.DI can generate the standard Func<ReadOnlySpan<char>, T> factory automatically. The runtime span argument is kept as a local value inside the generated delegate invocation, so no manual ctx.Override(...) call and no lock (ctx.Lock) block are required.

using Shouldly;
using Pure.DI;
using System;

DI.Setup(nameof(Composition))

    // Composition root
    .Root<Func<ReadOnlySpan<char>, Parser>>("ParserFactory");

var composition = new Composition();
var parser = composition.ParserFactory("Hello".AsSpan());

parser.Value.ShouldBe("Hello");

class Parser
{
    private string _value = "";

    [Ordinal]
    public void Initialize(ReadOnlySpan<char> text)
    {
        _value = text.ToString();
    }

    public string Value => _value;
}

To run the above code, the following NuGet packages must be added:

Use this default Func binding when the standard delegate shape is enough. Use a custom delegate factory with explicit ctx.Override<T>(...) only when you need a custom delegate type or additional factory logic.

Generics

Generic types are supported out of the box: a single binding like Bind<IRepository<TT>>().To<Repository<TT>>() covers IRepository<User>, IRepository<Order> and any other instantiation used in the object graph. Since Pure.DI is a source generator, each of them is turned into concrete, reflection-free code at compile time.

Important

Instead of open generic types, as in classical DI container libraries, regular generic types with marker types as type parameters are used here. Such "marker" types allow to define dependency graph more precisely.

For the case of IRepository<TT>, TT is a marker type, which allows the usual IRepository<TT> to be used instead of an open generic type like IRepository<>. This makes it easy to bind generic types by specifying marker types such as TT, TT1, etc. as parameters of generic types:

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    // Disable Resolve methods to keep the public API minimal
    .Hint(Hint.Resolve, "Off")
    // Binding a generic interface to a generic implementation
    // using the marker type TT. This allows Pure.DI to match
    // IRepository<User> to Repository<User>, IRepository<Order> to Repository<Order>, etc.
    .Bind<IRepository<TT>>().To<Repository<TT>>()
    .Bind<IDataService>().To<DataService>()

    // Composition root
    .Root<IDataService>("DataService");

var composition = new Composition();
var dataService = composition.DataService;

// Verifying that the correct generic types were injected
dataService.Users.ShouldBeOfType<Repository<User>>();
dataService.Orders.ShouldBeOfType<Repository<Order>>();

interface IRepository<T>;

class Repository<T> : IRepository<T>;

// Domain entities
record User;

record Order;

interface IDataService
{
    IRepository<User> Users { get; }

    IRepository<Order> Orders { get; }
}

class DataService(
    IRepository<User> users,
    IRepository<Order> orders)
    : IDataService
{
    public IRepository<User> Users { get; } = users;

    public IRepository<Order> Orders { get; } = orders;
}

To run the above code, the following NuGet packages must be added:

Actually, the property DataService looks like:

public IDataService DataService
{
  get
  {
    return new DataService(new Repository<User>(), new Repository<Order>());
  }
}

Even in this simple scenario, it is not possible to precisely define the binding of an abstraction to its implementation using open generic types:

.Bind(typeof(IMap<,>)).To(typeof(Map<,>))

You can try to match them by order or by name derived from the .NET type reflection. But this is not reliable, since order and name matching is not guaranteed. For example, there is some interface with two arguments of type _key and value. But in its implementation the sequence of type arguments is mixed up: first comes the value and then the key and the names do not match:

class Map<TV, TK>: IMap<TKey, TValue> { }

At the same time, the marker types TT1 and TT2 handle this easily. They determine the exact correspondence between the type arguments in the interface and its implementation:

.Bind<IMap<TT1, TT2>>().To<Map<TT2, TT1>>()

The first argument of the type in the interface, corresponds to the second argument of the type in the implementation and is a key. The second argument of the type in the interface, corresponds to the first argument of the type in the implementation and is a value. This is a simple example. Obviously, there are plenty of more complex scenarios where tokenized types will be useful. Marker types are regular .NET types marked with a special attribute, such as:

[GenericTypeArgument]
internal abstract class TT1 { }

[GenericTypeArgument]
internal abstract class TT2 { }

This way you can easily create your own, including making them fit the constraints on the type parameter, for example:

[GenericTypeArgument]
internal struct TTS { }

[GenericTypeArgument]
internal interface TTDisposable: IDisposable { }

[GenericTypeArgument]
internal interface TTEnumerator<out T>: IEnumerator<T> { }

Generic composition roots

Sometimes you want to be able to create composition roots with type parameters. In this case, the composition root can only be represented by a method.

Important

Resolve() methods cannot be used to resolve generic composition roots.

using Pure.DI;

DI.Setup(nameof(Composition))
    // Disable Resolve methods to keep the public API minimal
    .Hint(Hint.Resolve, "Off")
    .Bind().To<Repository<TT>>()
    .Bind().To<CreateCommandHandler<TT>>()
    // Creates UpdateCommandHandler manually,
    // just for the sake of example
    .Bind("Update").To(ctx => {
        ctx.Inject(out IRepository<TT> repository);
        return new UpdateCommandHandler<TT>(repository);
    })

    // Specifies to create a regular public method
    // to get a composition root of type ICommandHandler<T>
    // with the name "GetCreateCommandHandler"
    .Root<ICommandHandler<TT>>("GetCreateCommandHandler")

    // Specifies to create a regular public method
    // to get a composition root of type ICommandHandler<T>
    // with the name "GetUpdateCommandHandler"
    // using the "Update" tag
    .Root<ICommandHandler<TT>>("GetUpdateCommandHandler", "Update");

var composition = new Composition();

// createHandler = new CreateCommandHandler<int>(new Repository<int>());
var createHandler = composition.GetCreateCommandHandler<int>();

// updateHandler = new UpdateCommandHandler<string>(new Repository<string>());
var updateHandler = composition.GetUpdateCommandHandler<string>();

interface IRepository<T>;

class Repository<T> : IRepository<T>;

interface ICommandHandler<T>;

class CreateCommandHandler<T>(IRepository<T> repository) : ICommandHandler<T>;

class UpdateCommandHandler<T>(IRepository<T> repository) : ICommandHandler<T>;

To run the above code, the following NuGet package must be added:

Important

The method Inject() cannot be used outside of the binding setup.

Generic composition roots with constraints

Generic composition roots respect type constraints. Using constrained marker types — TTDisposable (IDisposable) and TTS (struct) — in Root<IDataProcessor<TTDisposable, TTS>>("GetProcessor") produces a generic method GetProcessor<T, TOptions>() whose type parameters carry the same constraints. A tagged root can also fix one of the type arguments, as GetSpecializedProcessor<T>() does with bool.

Important

Resolve methods cannot be used to resolve generic composition roots.

using Pure.DI;

DI.Setup(nameof(Composition))
    // Disable Resolve methods to keep the public API minimal
    .Hint(Hint.Resolve, "Off")
    .Bind().To<StreamSource<TTDisposable>>()
    .Bind().To<DataProcessor<TTDisposable, TTS>>()
    // Creates SpecializedDataProcessor manually,
    // just for the sake of example.
    // It treats 'bool' as the options type for specific boolean flags.
    .Bind("Specialized").To(ctx => {
        ctx.Inject(out IStreamSource<TTDisposable> source);
        return new SpecializedDataProcessor<TTDisposable>(source);
    })

    // Specifies to create a regular public method
    // to get a composition root of type DataProcessor<T, TOptions>
    // with the name "GetProcessor"
    .Root<IDataProcessor<TTDisposable, TTS>>("GetProcessor")

    // Specifies to create a regular public method
    // to get a composition root of type SpecializedDataProcessor<T>
    // with the name "GetSpecializedProcessor"
    // using the "Specialized" tag
    .Root<IDataProcessor<TTDisposable, bool>>("GetSpecializedProcessor", "Specialized");

var composition = new Composition();

// Creates a processor for a Stream with 'double' as options (e.g., threshold)
// processor = new DataProcessor<Stream, double>(new StreamSource<Stream>());
var processor = composition.GetProcessor<Stream, double>();

// Creates a specialized processor for a BinaryReader
// specializedProcessor = new SpecializedDataProcessor<BinaryReader>(new StreamSource<BinaryReader>());
var specializedProcessor = composition.GetSpecializedProcessor<BinaryReader>();

interface IStreamSource<T>
    where T : IDisposable;

class StreamSource<T> : IStreamSource<T>
    where T : IDisposable;

interface IDataProcessor<T, TOptions>
    where T : IDisposable
    where TOptions : struct;

class DataProcessor<T, TOptions>(IStreamSource<T> source) : IDataProcessor<T, TOptions>
    where T : IDisposable
    where TOptions : struct;

class SpecializedDataProcessor<T>(IStreamSource<T> source) : IDataProcessor<T, bool>
    where T : IDisposable;

To run the above code, the following NuGet package must be added:

Important

The method Inject() cannot be used outside of the binding setup.

Constructor ordinal attribute

Applying this attribute disables automatic constructor selection. Only constructors marked with this attribute are considered, ordered by ordinal (ascending).

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Arg<string>("connectionString")
    .Bind().To<Configuration>()
    .Bind().To<SqlDatabaseClient>()

    // Composition root
    .Root<IDatabaseClient>("Client");

var composition = new Composition(connectionString: "Server=.;Database=MyDb;");
var client = composition.Client;

// The client was created using the connection string constructor (Ordinal 0)
// even though the configuration constructor (Ordinal 1) was also possible.
client.ConnectionString.ShouldBe("Server=.;Database=MyDb;");

interface IConfiguration;

class Configuration : IConfiguration;

interface IDatabaseClient
{
    string ConnectionString { get; }
}

class SqlDatabaseClient : IDatabaseClient
{
    // The integer value in the argument specifies
    // the ordinal of injection.
    // The DI will try to use this constructor first (Ordinal 0).
    [Ordinal(0)]
    internal SqlDatabaseClient(string connectionString) =>
        ConnectionString = connectionString;

    // If the first constructor cannot be used (e.g. connectionString is missing),
    // the DI will try to use this one (Ordinal 1).
    [Ordinal(1)]
    public SqlDatabaseClient(IConfiguration configuration) =>
        ConnectionString = "Server=.;Database=DefaultDb;";

    public SqlDatabaseClient() =>
        ConnectionString = "InMemory";

    public string ConnectionString { get; }
}

To run the above code, the following NuGet packages must be added:

The Ordinal attribute is part of the API, but you can define your own in any assembly or namespace. A lower constructor ordinal takes precedence over the number of injection parameters and accessibility. If that constructor cannot be resolved, Pure.DI tries the next ordinal. Constructors with the same ordinal use the regular secondary criteria: more injection parameters first, then public before internal. Use distinct ordinal values when the choice affects behavior.

Member ordinal attribute

When applied to a field, property, method, or method parameter, the member participates in DI, ordered by ordinal (ascending).

using Shouldly;
using Pure.DI;
using System.Text;

DI.Setup(nameof(PersonComposition))
    .Arg<int>("personId")
    .Arg<string>("personName")
    .Arg<DateTime>("personBirthday")
    .Bind().To<Person>()

    // Composition root
    .Root<IPerson>("Person");

var composition = new PersonComposition(
    personId: 123,
    personName: "Nik",
    personBirthday: new DateTime(1977, 11, 16));

var person = composition.Person;
person.Name.ShouldBe("123 Nik 1977-11-16");

interface IPerson
{
    string Name { get; }
}

class Person : IPerson
{
    private readonly StringBuilder _name = new();

    public string Name => _name.ToString();

    // The Ordinal attribute specifies to perform an injection,
    // the integer value in the argument specifies
    // the ordinal of injection
    [Ordinal(0)] public int Id;

    [Ordinal(1)]
    public string FirstName
    {
        set
        {
            _name.Append(Id);
            _name.Append(' ');
            _name.Append(value);
        }
    }

    public void SetBirthday([Ordinal(2)] DateTime value)
    {
        _name.Append(' ');
        _name.Append($"{value:yyyy-MM-dd}");
    }
}

To run the above code, the following NuGet packages must be added:

The Ordinal attribute is part of the API, but you can define your own in any assembly or namespace. For an injection method, Ordinal can be placed on the method or its parameters. A method-level ordinal takes precedence; otherwise, the lowest parameter ordinal determines the method's execution order. Required fields and required or selected init properties are assigned in the object initializer before regular member injection. Within regular member injection, lower ordinals run first; equal ordinals are ordered by field, property, and method, then by declaration order within each kind. For equal ordinals and the same member kind, members declared on a derived type run before members inherited from its base types. Equal ordinals are valid and do not produce a diagnostic. Negative ordinal values are also supported.

Dependency attribute

When applied to a property or field, the member participates in DI, ordered by ordinal (ascending).

using Shouldly;
using Pure.DI;
using System.Text;

DI.Setup(nameof(PersonComposition))
    .Arg<int>("personId")
    .Arg<string>("personName")
    .Arg<DateTime>("personBirthday")
    .Bind().To<Person>()

    // Composition root
    .Root<IPerson>("Person");

var composition = new PersonComposition(
    personId: 123,
    personName: "Nik",
    personBirthday: new DateTime(1977, 11, 16));

var person = composition.Person;
person.Name.ShouldBe("123 Nik 1977-11-16");

interface IPerson
{
    string Name { get; }
}

class Person : IPerson
{
    private readonly StringBuilder _name = new();

    public string Name => _name.ToString();

    [Dependency] public int Id;

    // The Ordinal attribute specifies to perform an injection,
    // the integer value in the argument specifies
    // the ordinal of injection
    [Dependency(ordinal: 1)]
    public string FirstName
    {
        set
        {
            _name.Append(Id);
            _name.Append(' ');
            _name.Append(value);
        }
    }

    [Dependency(ordinal: 2)]
    public DateTime Birthday
    {
        set
        {
            _name.Append(' ');
            _name.Append($"{value:yyyy-MM-dd}");
        }
    }
}

To run the above code, the following NuGet packages must be added:

The Dependency attribute is part of the API, but you can define your own in any assembly or namespace.

Overload resolution priority

Library types sometimes keep an older constructor for compatibility while introducing a better overload for newly compiled applications. Starting with C# 13, OverloadResolutionPriorityAttribute tells the compiler which overload should be preferred. Pure.DI follows the same priority when it chooses a constructor and builds its dependency graph. For a primary constructor, place the attribute on the type declaration with the method: target.

using Shouldly;
using Pure.DI;
using System.Runtime.CompilerServices;

DI.Setup(nameof(Composition))
    // Both constructor dependencies can be created automatically.
    // The constructor attribute determines which graph is preferred.
    .Root<BillingApiClient>("Client");

var composition = new Composition();
var client = composition.Client;

client.Transport.ShouldBe("resilient-http");

class LegacyHttpOptions;

class ResilientHttpOptions;

[method: OverloadResolutionPriority(1)]
class BillingApiClient(ResilientHttpOptions options)
{
    // Kept so existing callers compiled against the old API continue to work.
    public BillingApiClient(LegacyHttpOptions options)
        : this(new ResilientHttpOptions()) => Transport = "legacy-http";

    // New compilations, including generated Pure.DI code, prefer the primary constructor.
    public ResilientHttpOptions Options { get; } = options;

    public string Transport { get; private set; } = "resilient-http";
}

To run the above code, the following NuGet packages must be added:

Higher integer values are preferred; unannotated constructors have priority 0, and negative values can de-prioritize legacy overloads. If the preferred constructor cannot be resolved, Pure.DI continues with the next applicable constructor. A primary constructor participates with the same rules as an explicitly declared constructor. Use [method: OverloadResolutionPriority(...)] or [method: Ordinal(...)] to attach a constructor attribute to a class or positional record primary constructor. When neither OrdinalAttribute nor an explicit OverloadResolutionPriorityAttribute is present, Pure.DI uses the primary constructor only as the final tie-breaker after the number of injections and constructor accessibility. This makes an otherwise equal choice deterministic without displacing a constructor designed for richer dependency injection. OrdinalAttribute remains the explicit Pure.DI override. When any accessible constructor is marked with Ordinal, only marked constructors participate, and their ordinal order takes precedence over OverloadResolutionPriorityAttribute.

Decorator

Interception is the ability to intercept calls between objects in order to enrich or change their behavior, but without having to change their code. A prerequisite for interception is weak binding. That is, if programming is abstraction-based, the underlying implementation can be transformed or improved by "packaging" it into other implementations of the same abstraction. At its core, intercept is an application of the Decorator design pattern. This pattern provides a flexible alternative to inheritance by dynamically "attaching" additional responsibility to an object. Decorator "packs" one implementation of an abstraction into another implementation of the same abstraction like a "matryoshka doll". Decorator is a well-known and useful design pattern. It is convenient to use tagged dependencies to build a chain of nested decorators, as in the example below:

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind("base").To<TextWidget>()
    .Bind().To<BoxWidget>()
    .Root<IWidget>("Widget");

var composition = new Composition();
var widget = composition.Widget;
widget.Render().ShouldBe("[ Hello World ]");

interface IWidget
{
    string Render();
}

class TextWidget : IWidget
{
    public string Render() => "Hello World";
}

class BoxWidget([Tag("base")] IWidget baseWidget) : IWidget
{
    public string Render() => $"[ {baseWidget.Render()} ]";
}

To run the above code, the following NuGet packages must be added:

Here an instance of the TextWidget type, labeled "base", is injected in the decorator BoxWidget. You can use any tag that semantically reflects the feature of the abstraction being embedded. The tag can be a constant, a type, or a value of an enumerated type.

Interception

Interception lets you enrich or change the behavior of a certain set of objects from the object graph being created without changing the code of the corresponding types.

using Shouldly;
using Castle.DynamicProxy;
using System.Runtime.CompilerServices;
using Pure.DI;

// OnDependencyInjection = On
// OnDependencyInjectionContractTypeNameWildcard = *IGreeter
DI.Setup(nameof(Composition))
    .Bind().To<Greeter>()
    .Root<IGreeter>("Greeter");

var composition = new Composition();
var greeter = composition.Greeter;

// The greeting is modified by the interceptor
greeter.Greet("World").ShouldBe("Hello World !!!");

public interface IGreeter
{
    string Greet(string name);
}

class Greeter : IGreeter
{
    public string Greet(string name) => $"Hello {name}";
}

partial class Composition : IInterceptor
{
    private static readonly ProxyGenerator ProxyGenerator = new();

    // Intercepts the instantiation of services to wrap them in a proxy
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private partial T OnDependencyInjection<T>(
        in T value,
        object? tag,
        Lifetime lifetime)
    {
        // Proxying is only possible for reference types (interfaces, classes)
        if (typeof(T).IsValueType)
        {
            return value;
        }

        // Creates a proxy that delegates calls to the 'value' object
        // and passes them through the 'this' interceptor
        return (T)ProxyGenerator.CreateInterfaceProxyWithTargetInterface(
            typeof(T),
            value,
            this);
    }

    // Logic performed when a method on the proxy is called
    public void Intercept(IInvocation invocation)
    {
        // Executes the original method
        invocation.Proceed();

        // Enhances the result of the Greet method
        if (invocation.Method.Name == nameof(IGreeter.Greet)
            && invocation.ReturnValue is string message)
        {
            invocation.ReturnValue = $"{message} !!!";
        }
    }
}

To run the above code, the following NuGet packages must be added:

Using an intercept gives you the ability to add end-to-end functionality such as:

  • Logging

  • Action logging

  • Performance monitoring

  • Security

  • Caching

  • Error handling

  • Providing resistance to failures, etc.

Advanced interception

Advanced interception is useful when cross-cutting behavior is required on a hot path and the proxy factory itself must not become the bottleneck. Instead of asking Castle DynamicProxy to discover the construction path repeatedly, this scenario compiles and caches a strongly typed proxy factory per service type. The example wraps business services with a logging interceptor and caches the proxy creation delegate in a generic nested ProxyFactory<T>. The generated Pure.DI graph still creates the target services directly, while the interception hook applies the cached proxy layer after each dependency is built.

using Shouldly;
using Castle.DynamicProxy;
using System.Collections.Immutable;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using Pure.DI;

// OnDependencyInjection = On
DI.Setup(nameof(Composition))
    .Bind().To<DataService>()
    .Bind().To<BusinessService>()
    .Root<IBusinessService>("BusinessService");

var log = new List<string>();
var composition = new Composition(log);
var businessService = composition.BusinessService;

// Use the services to verify interception.
businessService.Process();
businessService.DataService.Count();

log.ShouldBe(
    [
        "Process returns Processed",
        "get_DataService returns Castle.Proxies.IDataServiceProxy",
        "Count returns 55"
    ]);

public interface IDataService
{
    int Count();
}

class DataService : IDataService
{
    public int Count() => 55;
}

public interface IBusinessService
{
    IDataService DataService { get; }

    string Process();
}

class BusinessService(IDataService dataService) : IBusinessService
{
    public IDataService DataService { get; } = dataService;

    public string Process() => "Processed";
}

internal partial class Composition : IInterceptor
{
    private readonly List<string> _log;
    private static readonly IProxyBuilder ProxyBuilder = new DefaultProxyBuilder();
    private readonly IInterceptor[] _interceptors;

    public Composition(List<string> log)
    {
        _log = log;
        _interceptors = [this];
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private partial T OnDependencyInjection<T>(
        in T value,
        object? tag,
        Lifetime lifetime)
    {
        if (typeof(T).IsValueType)
        {
            return value;
        }

        return ProxyFactory<T>.GetFactory(ProxyBuilder)(
            value,
            _interceptors);
    }

    public void Intercept(IInvocation invocation)
    {
        invocation.Proceed();
        _log.Add($"{invocation.Method.Name} returns {invocation.ReturnValue}");
    }

    private static class ProxyFactory<T>
    {
        private static Func<T, IInterceptor[], T>? _factory;

        public static Func<T, IInterceptor[], T> GetFactory(IProxyBuilder proxyBuilder) =>
            _factory ?? CreateFactory(proxyBuilder);

        private static Func<T, IInterceptor[], T> CreateFactory(IProxyBuilder proxyBuilder)
        {
            // Compiles a delegate to create a proxy for the performance boost
            var proxyType = proxyBuilder.CreateInterfaceProxyTypeWithTargetInterface(
                typeof(T),
                Type.EmptyTypes,
                ProxyGenerationOptions.Default);
            var ctor = proxyType.GetConstructors()
                .Single(i => i.GetParameters().Length == 2);
            var instance = Expression.Parameter(typeof(T));
            var interceptors = Expression.Parameter(typeof(IInterceptor[]));
            var newProxyExpression = Expression.New(ctor, interceptors, instance);
            return _factory = Expression.Lambda<Func<T, IInterceptor[], T>>(
                    newProxyExpression,
                    instance,
                    interceptors)
                .Compile();
        }
    }
}

To run the above code, the following NuGet packages must be added:

Note

Advanced interception provides high-performance proxy generation for scenarios where runtime interception overhead must be minimized. Use this only when decorators are not expressive enough or when an existing interception ecosystem is required. For simple cross-cutting behavior, decorators are easier to read and cheaper to reason about.

Generate an interface from a class

This example shows how a concrete service can generate a matching interface and be consumed through Pure.DI.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind().To<EmailSender>()
    .Root<App>(nameof(App));

var composition = new Composition();
var app = composition.App;

app.Provider.ShouldBe("smtp");
app.Result.ShouldBe("sent:ops@contoso.com");

public partial interface IEmailSender;

[GenerateInterface]
public class EmailSender : IEmailSender
{
    public string Provider => "smtp";

    public string Send(string address) => $"sent:{address}";
}

public class App(IEmailSender sender)
{
    public string Provider { get; } = sender.Provider;

    public string Result { get; } = sender.Send("ops@contoso.com");
}

To run the above code, the following NuGet packages must be added:

The example shows how to:

  • Generate an interface from a class
  • Bind the generated contract in Pure.DI
  • Resolve a consumer that depends on the interface

Ignore members in the generated interface

This example shows how to exclude internal-only members from a generated interface.

using Shouldly;
using Pure.DI;

DI.Setup(nameof(Composition))
    .Bind().To<ApiClient>()
    .Root<App>(nameof(App));

var composition = new Composition();
var app = composition.App;

app.Endpoint.ShouldBe("https://api.contoso.com");

public partial interface IApiClient;

[GenerateInterface]
public class ApiClient : IApiClient
{
    public string Endpoint => "https://api.contoso.com";

    [IgnoreInterface]
    public string GetAccessToken() => "internal-token";
}

public class App(IApiClient client)
{
    public string Endpoint { get; } = client.Endpoint;
}

To run the above code, the following NuGet packages must be added:

The example shows how to:

  • Mark members with IgnoreInterface
  • Keep only the intended contract surface
  • Use the generated interface in Pure.DI