Pure.DI Documentation

repository·master·Indexed 21 days ago

https://github.com/devteam/pure.di

A high-performance, compile-time dependency injection code generator for .NET that replaces runtime reflection with generated C# code. It provides zero runtime overhead, compile-time validation of dependency graphs, and supports a wide range of platforms including .NET Framework 2.0+, .NET Core, .NET 5+, Native AOT, Unity, and Xamarin. Key features include composition roots, lifetime management (Singleton, Transient, Scoped), and integration with Microsoft.Extensions.DependencyInjection via the Pure.DI.MS package.

Tokens
219.5K
Snippets
569
Records
684
Agent score
66%

What's inside Pure.DI

  1. Overview of Pure.DI

    master

    Pure.DI is a compile-time dependency injection (DI) code generator for .NET. Unlike traditional DI frameworks that rely on runtime reflection and dynamic instantiation, Pure.DI generates straightforward C# code that performs object composition through nested constructor invocations. This results in zero runtime overhead and allows for compile-time validation of dependency graphs.

    Key Benefits:

    • Zero Overhead: No reflection or dynamic instantiation; performance is identical to manual object creation.
    • Compile-Time Validation: Detects missing dependencies, cyclic references, and injection errors during compilation rather than at runtime.
    • High Portability: Works on .NET Framework 2.0+, .NET Core, .NET 5+, UWP/Xbox, .NET IoT, Unity, Xamarin, and Native AOT.
    • Transparency: The generated code is viewable and debuggable.
  2. Benefits of Pure DI over traditional DI containers

    master

    Pure.DI provides several advantages for high-performance and highly predictable applications:

    • Predictable Performance: Eliminates costs associated with reflection and dynamic calls. The generated code is as efficient as manually written instantiation.
    • Compile-Time Validation: Configuration errors (like missing registrations or circular dependencies) are caught during compilation rather than causing runtime crashes.
    • Transparency: You can inspect and debug the generated C# code just like your own handwritten code.
    • AOT and Restricted Environments: Because it requires no runtime reflection, it is ideal for Ahead-of-Time (AOT) compilation, Unity, or environments where reflection is restricted or undesirable.
    • No Service Locator: It encourages better architecture by using explicit composition roots instead of a global Resolve<T>() pattern.
  3. What is Pure.DI and how does it work

    master

    Pure.DI is a C# code generator for dependency injection that builds the dependency graph at compile-time. Instead of using a runtime container with reflection or service locators, it generates standard C# code (essentially a chain of new calls) to instantiate objects.

    Key Concepts

    • Compile-Time Generation: The generator analyzes your dependency graph, validates it (checking for missing dependencies, cycles, or inaccessible constructors), and generates a partial composition class.
    • Zero Overhead: Since the output is plain C# code, there is no runtime reflection, no assembly scanning, and no hidden allocations typical of traditional DI containers.
    • Composition Roots: Instead of calling Resolve<T>() from anywhere, Pure.DI uses explicit properties or methods in a composition class as entry points to the object graph.
    • Built-in BCL Support: It natively supports common .NET Base Class Library types like Func<>, Lazy<>, IEnumerable<>, Task, ValueTask, Span, and Tuple.
    // Pure.DI generates code that effectively performs:
    var service = new CheckoutService(new PaymentGatewayClient(new HttpClient(), "api-key"), new ConsoleLogger());
  4. What is a Composition class in Pure.DI

    master

    In Pure.DI, the Composition class is the central location where the dependency graph is configured. Unlike traditional DI containers that operate via reflection at runtime, Pure.DI generates a standard C# class that you can inspect, debug, and call directly.

    The Composition class handles:

    • Bindings: Mapping interfaces to specific implementations (e.g., Bind<IContract>().To<Implementation>()).
    • Roots: Defining public entry points into the graph (e.g., .Root<IService>("MyService")).
    • Lifetimes: Configuring how long dependencies live.
    • Generator Hints: Providing additional metadata to the source generator.
  5. Track disposable instances per a composition root using Owned<T>

    master

    By default, Pure.DI manages disposables per composition. However, you can use the Owned<T> type to track and dispose of disposable instances per a specific composition root.

    To implement this, declare your root using .Root<Owned<T>>("Name"). Each time you access this root property on the Composition instance, it returns an Owned<T> wrapper. This wrapper owns every disposable instance created within that specific dependency graph. Calling .Dispose() on the Owned<T> instance cleans up exactly those instances associated with that specific access, without affecting other roots or instances created from the same composition.

    // Setup the composition with an Owned root
    partial class Composition
    {
        static void Setup() =>
            DI.Setup()
                .Bind().To<DbConnection>()
                .Bind().To<OrderProcessingService>()
                .Root<Owned<IOrderProcessingService>>("OrderProcessingService");
    }
    
    // Usage
    var composition = new Composition();
    var service1 = composition.OrderProcessingService;
    var service2 = composition.OrderProcessingService;
    
    // Disposing service2 only cleans up its specific dependency graph
    service2.Dispose();
    
    // service1.Value.DbConnection remains alive
    // service2.Value.DbConnection is now disposed
  6. New features in Pure.DI (v2.3.5–2.5.1)

    master

    Recent releases have introduced several advanced features for DI configuration and performance:

    DI Configuration & Contracts

    • Union types as DI contracts: Using union types to define dependency requirements.
    • Interface generation: Automatically generating interfaces from classes using [GenerateInterface].
    • Attribute-based bindings: Defining bindings directly in implementations using attributes like [Bind], [Type], [Tag], and [Lifetime].
    • [Export] attribute: Using class members as dependency sources.
    • Custom binding attributes: Support for user-defined binding attributes.
    • Nullable Reference Types: Full support for nullable reference types.
    • TryBuildUp: A mechanism for safe object 'completion' or 'building up'.

    Scopes

    • SetupScope for general DI setup.
    • Unity-specific scene scopes.
    • CreateScope() for integration with Microsoft DI.

    High-Performance & Hot Path DI

    • Stack-only support: Support for Span<T>, ReadOnlySpan<T>, and ref struct dependencies.
    • Ref struct factories: Factories that allow allows ref struct.
    • Zero-copy patterns: Support for zero-copy parsing and method injection on hot paths.
    • Non-boxing union results: Optimized handling of union types to avoid boxing.
    • Advanced performance patterns: Support for ArrayPool, object pools, closure-free factories, ValueTask<T> roots, and ThreadSafe = Off configurations.
  7. How keyed services and tags work together

    master

    Keyed services allow you to distinguish between multiple implementations of the same interface.

    1. Registration: You use .Bind<T>("Key") to register an implementation under a specific tag.
    2. Injection: When a constructor requires a dependency, you use the [Tag("Key")] attribute on the parameter. Pure.DI will look for the implementation registered with that specific key.
    3. Resolution: You can manually resolve these tagged services from the Composition root using GetRequiredKeyedService<T>("Key") if the class implements IKeyedServiceProvider.
    class OnlineOrderService([Tag("PayPal")] IPaymentGateway paymentGateway) : IOrderService
    {
        public IPaymentGateway PaymentGateway { get; } = paymentGateway;
    }
  8. Collect multiple implementations using Enumerable generics

    master

    Pure.DI allows you to collect all registered implementations of a generic interface into an IEnumerable<T>. This is particularly useful for implementing middleware patterns or plugin architectures where multiple handlers must be invoked in sequence.

    When you bind a generic type (e.g., IMiddleware<TT>) to an implementation (e.g., LoggingMiddleware<TT>), Pure.DI tracks all such bindings. When a consumer requests IEnumerable<IMiddleware<T>>, the generated composition root provides an enumeration containing all matching implementations, including those with specific tags.

    using Pure.DI;
    using System.Collections.Immutable;
    
    DI.Setup(nameof(Composition))
        // Register generic middleware components.
        .Bind<IMiddleware<TT>>().To<LoggingMiddleware<TT>>()
        // Register with a specific tag
        .Bind<IMiddleware<TT>>("Metrics").To<MetricsMiddleware<TT>>()
    
        // Register the pipeline that consumes the collection
        .Bind<IPipeline<TT>>().To<Pipeline<TT>>()
    
        // Define composition roots
        .Root<IPipeline<int>>("IntPipeline")
        .Root<IPipeline<string>>("StringPipeline");
    
    var composition = new Composition();
    var intPipeline = composition.IntPipeline;
    // intPipeline.Middlewares will contain both LoggingMiddleware<int> and MetricsMiddleware<int>
  9. Use ctx.ConsumerType to create context-aware dependencies

    master

    In Pure.DI, ctx.ConsumerType provides access to the type of the class that is currently receiving the dependency being resolved. This is particularly useful for creating context-aware objects, such as loggers that are automatically tagged with the name of the consuming class.

    When using .Bind().To(ctx => { ... }), you can access ctx.ConsumerType to customize the instance returned based on where it is being injected.

    DI.Setup(nameof(Composition))
        .Arg<Serilog.ILogger>("logger", "from arg")
        .Bind().To(ctx => {
            ctx.Inject<Serilog.ILogger>("from arg", out var logger);
    
            // 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));
  10. Control override propagation depth with Override vs Let

    master

    When configuring a factory, you can control how far override values propagate into the dependency graph. This is useful when you want to override a constructor parameter for a specific class without affecting its nested dependencies.

    Deep Overrides (ctx.Override)

    Using ctx.Override(value) inside a factory binding causes the override to propagate into the entire nested dependency graph. Any dependencies required by the object being injected will also receive this overridden value if they are of the same type.

    Shallow Overrides (ctx.Let)

    Using ctx.Let(value) inside a factory binding keeps the override local to the immediate injection level. The object being injected will use the overridden value, but its nested dependencies will continue to use the original values defined in the DI setup.

    Summary Table

    MethodPropagation ScopeUse Case
    ctx.Override(val)Deep: Affects the object and its nested dependencies.When a specific value must be used throughout a whole sub-graph.
    ctx.Let(val)Shallow: Affects only the immediate object.When you want to override a parameter for one class without side effects on its children.
    // Deep Override: Dependency.Id will also be 42
    DI.Setup("Deep")
        .Bind().To<Service>(ctx =>
        {
            ctx.Override(42);
            ctx.Inject(out Service service);
            return service;
        })
        .Root<Service>("Service");
    
    // Shallow Override: Dependency.Id remains the original bound value (e.g., 7)
    DI.Setup("Shallow")
        .Bind().To<Service>(ctx =>
        {
            ctx.Let(42);
            ctx.Inject(out Service service);
            return service;
        })
        .Root<Service>("Service");
  11. Use the PerResolve lifetime

    master

    The PerResolve lifetime ensures that a single instance of a dependency is shared within a single resolution of a composition root. This is ideal for request-scoped context (e.g., locale, pricing rules, or feature flags) where you want all dependencies in a single 'planning session' or request to see the same instance, but want a fresh instance for every new root access.

    To use it, bind your type using .Bind().As(PerResolve).To<T>().

    using Pure.DI;
    using static Pure.DI.Lifetime;
    
    DI.Setup(nameof(Composition))
        .Bind().As(PerResolve).To<RoutePlanningSession>()
        .Root<TrainTripPlanner>("Planner");
    
    var composition = new Composition();
    
    // Every time you access the root, you get a new PerResolve instance
    var plan1 = composition.Planner;
    var plan2 = composition.Planner;
    
    // plan1.SessionForOutbound will be the same as plan1.SessionForReturn
    // plan2.SessionForOutbound will NOT be the same as plan1.SessionForOutbound
  12. Use the BuildUp pattern for existing objects

    master
    When an object is created outside of the DI control (e.g., via JSON deserialization, plugins, game engines, or UI frameworks), you can use the BuildUp pattern. Pure.DI can generate builders for types derived from a known base T that 'fill in' dependencies into existing instances via fields, properties, or methods marked with injection attributes.