Pure.DI for .NET

May 9, 2026 · View on GitHub

NuGet License GitHub Build

Pure.DI is a compile-time dependency injection (DI) code generator. Supports .NET starting with .NET Framework 2.0, released 2005-10-27, and all newer versions.

Usage Requirements

  • .NET SDK 6.0.4+
    Required for compilation. Projects can target older frameworks (e.g., .NET Framework 2.0).
  • C# 8+
    Only required for projects using the Pure.DI source generator. Other projects support any C# version.

Key Features

✔️ Zero Overhead

Pure.DI is a .NET code generator designed to produce clean, efficient dependency injection logic. By leveraging basic language constructs, it generates straightforward code indistinguishable from manual implementation—essentially composing objects through nested constructor invocations. Unlike traditional DI frameworks, Pure.DI avoids reflection and dynamic instantiation entirely, eliminating performance penalties associated with runtime overhead.

✔️ Compile-Time Validation

All analysis of object, constructor, and method graphs occurs at compile time. Pure.DI proactively detects and alerts developers to issues such as missing dependencies, cyclic references, or dependencies unsuitable for injection—ensuring these errors are resolved before execution. This approach guarantees that developers cannot produce a program vulnerable to runtime crashes caused by faulty dependency wiring. The validation process operates seamlessly alongside code development, creating an immediate feedback loop: as you modify your code, Pure.DI verifies its integrity in real time, effectively delivering tested, production-ready logic the moment changes are implemented.

✔️ Works everywhere

The pure dependency injection approach introduces no runtime dependencies and avoids .NET reflection , ensuring consistent execution across all supported platforms. This includes the Full .NET Framework 2.0+, .NET Core, .NET 5+, UWP/Xbox, .NET IoT, Unity, Xamarin, Native AOT, and beyond. By decoupling runtime constraints, it preserves predictable behavior regardless of the target environment.

✔️ Familiar Syntax

The Pure.DI API is intentionally designed to closely mirror the APIs of mainstream IoC/DI frameworks. This approach ensures developers can leverage their existing knowledge of dependency injection patterns without requiring significant adaptation to a proprietary syntax.

✔️ Precise Generics

Pure.DI recommends utilizing dedicated marker types rather than relying on open generics. This approach enables more precise construction of object graphs while allowing developers to fully leverage the capabilities of generic types.

✔️ Transparency

Pure.DI allows to view and debug the generated code, making debugging and testing easier.

✔️ Built-in BCL Support

Pure.DI provides native support for numerous Base Class Library (BCL) types out of the box without any extra effort.

When to Use Pure.DI

✔️ High-Performance Applications

Pure.DI is designed for high-performance applications where speed and minimal memory consumption are critical.

✔️ Projects with a Focus on Clean Code

Pure.DI is suitable for projects where code cleanliness and minimalism are important factors.

✔️ Applications with Complex Dependencies

Pure.DI can handle complex dependencies and provides flexible configuration options.

✔️ Ideal for Libraries

Its high performance, zero memory consumption/preparation overhead, and lack of dependencies make it ideal for building libraries and frameworks.

NuGet packages

NuGet packageDescription
Pure.DIDI source code generator
Pure.DI.AbstractionsAbstractions for Pure.DI
Pure.DI.TemplatesTemplate package, for creating projects from the shell/command line
Pure.DI.MSAdd-ons for Pure.DI to work with Microsoft DI

Schrödinger's cat demonstrates how it all works CSharp

The reality is

Cat

Let's create an abstraction

interface IBox<out T>
{
    T Content { get; }
}

interface ICat
{
    State State { get; }
}

enum State { Alive, Dead }

Here's our implementation

record CardboardBox<T>(T Content) : IBox<T>;

class ShroedingersCat(Lazy<State> superposition): ICat
{
    // The decoherence of the superposition
    // at the time of observation via an irreversible process
    public State State => superposition.Value;
}

Important

Our abstraction and implementation know nothing about the magic of DI or any frameworks.

Let's glue it all together

Add the Pure.DI package to your project:

NuGet

Let's bind the abstractions to their implementations and set up the creation of the object graph:

DI.Setup(nameof(Composition))
    // Models a random subatomic event that may or may not occur
    .Bind().As(Singleton).To<Random>()
    // Represents a quantum superposition of two states: Alive or Dead
    .Bind().To((Random random) => (State)random.Next(2))
    .Bind().To<ShroedingersCat>()
    // Cardboard box with any contents
    .Bind().To<CardboardBox<TT>>()
    // Composition Root
    .Root<Program>("Root");

Note

In fact, the Bind().As(Singleton).To<Random>() binding is unnecessary since Pure.DI supports many .NET BCL types out of the box, including Random. It was added just for the example of using the Singleton lifetime.

The code above specifies the generation of a partial class named Composition. This name is defined in the DI.Setup(nameof(Composition)) call. The class contains a Root property that returns an object graph with an object of type Program as the root. The type and name of the property are defined by calling Root<Program>("Root"). The generated code looks like this:

partial class Composition
{
    private readonly Lock _lock = new Lock();
    private Random? _random;
    
    public Program Root
    {
      get
      {
        var stateFunc = new Func<State>(() => {
              if (_random is null)
                lock (_lock)
                  if (_random is null)
                    _random = new Random();

              return (State)_random.Next(2)
            });

        return new Program(
          new CardboardBox<ICat>(
            new ShroedingersCat(
              new Lazy<State>(
                stateFunc))));    
      }
    }

    public T Resolve<T>() { ... }
    public T Resolve<T>(object? tag) { ... }

    public object Resolve(Type type) { ... }
    public object Resolve(Type type, object? tag) { ... }
}
Class diagram
classDiagram
    ShroedingersCat --|> ICat
    CardboardBoxᐸICatᐳ --|> IBoxᐸICatᐳ
    CardboardBoxᐸICatᐳ --|> IEquatableᐸCardboardBoxᐸICatᐳᐳ
    Composition ..> Program : Program Root
    State o-- "Singleton" Random : Random
    ShroedingersCat *--  LazyᐸStateᐳ : LazyᐸStateᐳ
    Program *--  CardboardBoxᐸICatᐳ : IBoxᐸICatᐳ
    CardboardBoxᐸICatᐳ *--  ShroedingersCat : ICat
    LazyᐸStateᐳ o-- "PerBlock" FuncᐸStateᐳ : FuncᐸStateᐳ
    FuncᐸStateᐳ *--  State : State
    
namespace Sample {
    class CardboardBoxᐸICatᐳ {
        <<record>>
        +CardboardBox(ICat Content)
    }
    
    class Composition {
        <<partial>>
        +Program Root
        + T ResolveᐸTᐳ()
        + T ResolveᐸTᐳ(object? tag)
        + object Resolve(Type type)
        + object Resolve(Type type, object? tag)
    }
    
    class IBoxᐸICatᐳ {
        <<interface>>
    }
    
    class ICat {
        <<interface>>
    }
    
    class Program {
        <<class>>
        +Program(IBoxᐸICatᐳ box)
    }
    
    class ShroedingersCat {
    <<class>>
        +ShroedingersCat(LazyᐸStateᐳ superposition)
    }
    
    class State {
        <<enum>>
    }
}
    
namespace System {
    class FuncᐸStateᐳ {
        <<delegate>>
    }
    
    class IEquatableᐸCardboardBoxᐸICatᐳᐳ {
        <<interface>>
    }
    
    class LazyᐸStateᐳ {
        <<class>>
    }
    
    class Random {
        <<class>>
        +Random()
    }
}

You can see the class diagram at any time by following the link in the comment of the generated class:

This code does not depend on other libraries, does not use type reflection, and avoids tricks that can negatively affect performance and memory consumption. It looks like efficient code written by hand. At any given time, you can study it and understand how it works.

The public Program Root { get; } property is a Composition Root, the only place in the application where the composition of the object graph takes place. Each instance is created using basic language constructs, which compile with all optimizations and minimal impact on performance and memory consumption. In general, applications may have multiple composition roots and thus such properties. Each composition root must have its own unique name, which is defined when the Root<T>(string name) method is called, as shown in the code above.

Time to open boxes!

class Program(IBox<ICat> box)
{
  // Composition Root, a single place in an application
  // where the composition of the object graphs
  // for an application takes place
  static void Main() => new Composition().Root.Run();

  private void Run() => Console.WriteLine(box);
}

Pure.DI creates efficient code in a pure DI paradigm, using only basic language constructs as if you were writing code by hand. This allows you to take full advantage of Dependency Injection everywhere and always, without any compromise!

The full equivalent of this application with top-level statements can be found here.

Just try creating a project from scratch!

Install the project template

dotnet new install Pure.DI.Templates

In a directory, create a console application

dotnet new di

Run it

dotnet run

An introductory article that will help you understand the basic idea and get started with Pure.DI

Examples

Basics

Lifetimes

Base Class Library

Generics

Attributes

Interception

Hints

Interfaces

Advanced

Use Cases

Unity

Applications

Generated Code

Each generated class (a composition) is configured via Setup(string compositionTypeName):

DI.Setup("Composition")
    .Bind<IDependency>().To<Dependency>()
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");
The following class will be generated
partial class Composition
{
    // Composition root
    public IService Root
    {
        get
        {
            return new Service(new Dependency());
        }
    }
}

The compositionTypeName parameter can be omitted:

  • If the setup is performed inside a partial class, the composition will be created for that partial class.
  • For a class with composition kind CompositionKind.Global, see this example.
Setup arguments

The first parameter is used to specify the name of the composition class. All sets with the same name will be combined to create one composition class. Alternatively, this name may contain a namespace, e.g. a composition class is generated for Sample.Composition:

namespace Sample
{
    partial class Composition
    {
        ...
    }
}

The second optional parameter may have multiple values to determine the kind of composition.

CompositionKind.Public

This value is used by default. If this value is specified, a normal composition class will be created.

CompositionKind.Internal

If you specify this value, the class will not be generated, but this setup can be used by others as a base setup. For example:

DI.Setup("BaseComposition", CompositionKind.Internal)
    .Bind().To<Dependency>();

DI.Setup("Composition").DependsOn("BaseComposition")
    .Bind().To<Service>();

If the CompositionKind.Public flag is set in the composition setup, it can also serve as the base for other compositions, as in the example above.

CompositionKind.Global

No composition class will be created when this value is specified, but this setup is the base setup for all setups in the current project, and DependsOn(...) is not required.

Constructors

By default, starting with version 2.3.0, no constructors are generated for a composition. The actual set of constructors depends on the composition arguments and lifetime scopes.

Parameterized constructor (automatic generation)

If the composition has any arguments defined, Pure.DI automatically generates a public parameterized constructor that includes all specified arguments. Setup contexts passed via DependsOn(..., kind, name) with SetupContextKind.Argument are treated as arguments and also appear in this constructor.

Example configuration:

DI.Setup("Composition")
  .Arg<string>("name")
  .Arg<int>("id")
  // ...

Resulting constructor:

public Composition(string name, int id) { /* ... */ }

Important notes:

  • Only arguments that are actually used in the object graph appear in the constructor.
  • Unused arguments are omitted to optimize resource usage.
  • If no arguments (including setup context arguments) are specified, no parameterized constructor is created.

Setup context storage

When a dependent setup needs instance state from another setup, you can pass an explicit setup context via DependsOn:

  • SetupContextKind.Argument: adds the setup context as a constructor argument.

  • SetupContextKind.RootArgument: adds the setup context to root methods that require it, no constructor argument.

  • SetupContextKind.Members: copies referenced setup members into the dependent composition, no constructor argument. The name parameter is optional.

    • Public and protected members are copied to the dependent composition.
    • Properties without custom logic (simple field-backed accessors) are copied without requiring partial methods.
    • Properties with custom logic require implementation of partial accessor methods. Referenced methods are declared partial without bodies. Properties with custom accessors use partial get_/set_ methods (for example, get__MyProperty() for properties, note the double underscore prefix).

This is useful for Unity/MonoBehaviour scenarios where the composition must remain parameterless and the host sets fields or properties.

Example: Simple field-backed property
// BaseComposition
internal partial class BaseComposition
{
    // Simple property without custom logic
    public string ConnectionString { get; set; } = "";
}

// Composition - the property is automatically copied, no partial methods needed
internal partial class Composition
{
    // ConnectionString is automatically available here
}
Example: Property with custom accessor logic
// BaseComposition
internal partial class BaseComposition
{
    private int _maxConnections = 100;

    // Property with custom getter logic
    public int MaxConnections
    {
        get => _maxConnections;
        set => _maxConnections = value;
    }
}

// Composition - implements custom accessor logic
internal partial class Composition
{
    private int _maxConnections = 100;

    // Implement custom getter logic (note the double underscore prefix)
    private partial int get__MaxConnections() => _maxConnections + 1;

    public void SetMaxConnections(int value) => _maxConnections = value;
}
Example: Protected field access
// BaseComposition
internal partial class BaseComposition
{
    // Protected field accessible in derived compositions
    protected bool EnableDiagnostics = false;

    private void Setup()
    {
        DI.Setup(nameof(BaseComposition), CompositionKind.Internal)
            .Bind("enableDiagnostics").To(_ => EnableDiagnostics);
    }
}

// Composition - can access and modify protected fields
internal partial class Composition
{
    public Composition() => EnableDiagnostics = true; // Access protected field
}

If there is at least one binding with Lifetime.Scoped, Pure.DI generates two constructors:

  1. Public default constructor

Used for creating the root scope instance.

public Composition() { /* ... */ }
  1. Internal constructor with parent scope

Used for creating child scope instances. This constructor is internal and accepts a single parameter � the parent scope.

internal Composition(Composition parentScope) { /* ... */ }

Important notes:

  • The public default constructor enables initialization of the root composition.
  • The internal constructor with parent reference enables proper scoping hierarchy for Lifetime.Scoped dependencies.
  • These constructors are only generated when Lifetime.Scoped bindings exist in the composition.
  • Setup contexts with SetupContextKind.Field or SetupContextKind.Property do not require constructors and can be set by the host (for example, Unity).

Summary of constructor generation rules

  • No arguments + no Scoped lifetimes: no constructors generated.
  • Arguments present (including setup context arguments): public parameterized constructor with all used arguments.
  • At least one Scoped lifetime: two constructors (public default + internal with parent).
  • Both arguments and Scoped lifetimes: all three constructors (parameterized, public default, internal with parent).
  • Setup context with SetupContextKind.Argument behaves like an argument and can add a constructor parameter.
  • Setup context with SetupContextKind.Field, SetupContextKind.Property, SetupContextKind.RootArgument, or SetupContextKind.Members does not add constructor parameters.
Composition Roots

Regular Composition Roots

To create an object graph quickly and conveniently, a set of properties (or methods) is formed. These properties/methods are here called composition roots. The type of a property/method is the type of the root object created by the composition. Accordingly, each invocation of a property/method leads to the creation of an object graph with a root element of this type.

DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    .Root<IService>("MyService");

var composition = new Composition();
var service = composition.MyService;
service = composition.Resolve<IService>();
service = composition.Resolve(typeof(IService));

In this case, the property for the IService type will be named MyService and will be available for direct use. The result of its use will be the creation of an object graph with the root of IService type:

public IService MyService
{
    get
    {
        ...
        return new Service(...);
    }
}

This is the recommended way to create a composition root. A composition class can contain any number of roots.

In addition, the composition roots can be resolved using the Resolve/ResolveByTag methods:

service = composition.Resolve<IService>();
service = composition.Resolve(typeof(IService));

Tip

  • There is no limit to the number of roots, but you should consider limiting the number of roots. Ideally, an application should have a single composition root.
  • The name of the composition root is arbitrarily chosen depending on its purpose, but should follow C# property naming conventions since it becomes a property name in the composition class.
  • It is recommended that composition roots be resolved using normal properties or methods instead of Resolve/ResolveByTag methods.

Anonymous Composition Roots

If the root name is empty, an anonymous composition root with a random name is created:

private IService RootM07D16di_0001
{
    get { ... }
}

These properties (or methods) have an arbitrary name and access modifier private and cannot be used directly from the code. Do not attempt to use them, as their names can change between builds. Anonymous composition roots can be resolved by Resolve/ResolveByTag methods:

DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    .Root<IService>();

var composition = new Composition();
var service = composition.Resolve<IService>();
service = composition.Resolve(typeof(IService));

Root Arguments

When a root needs data that changes per call — not shared across the whole composition — use RootArg<T>(name). The generated root becomes a method instead of a property, and the argument is passed at every call site:

DI.Setup("Composition")
    // RootArg is incompatible with Resolve methods — disable them when using RootArg
    .Hint(Hint.Resolve, "Off")
    .RootArg<Guid>("userId")
    .Bind<IUserService>().To<UserService>()
    .Root<IUserService>("GetUserService");

var composition = new Composition();
var service = composition.GetUserService(userId: Guid.NewGuid());

Note

Because RootArg binds a value only for a specific root call, it is incompatible with Resolve/ResolveByTag methods. Disable them with .Hint(Hint.Resolve, "Off") when using root arguments.

See also: Root arguments example

Resolve/ResolveByTag methods

Resolve/ResolveByTag methods

By default, a set of four Resolve/ResolveByTag methods is generated:

public T Resolve<T>() { ... }

public T Resolve<T>(object? tag) { ... }

public object Resolve(Type type) { ... }

public object Resolve(Type type, object? tag) { ... }

These methods can resolve both public and anonymous composition roots that do not depend on root arguments. They are useful when using the Service Locator approach, where the code resolves composition roots in place:

var composition = new Composition();

composition.Resolve<IService>();

This is not recommended because Resolve/ResolveByTag methods have a number of disadvantages:

  • They provide access to an unlimited set of dependencies.
  • Their use can potentially lead to runtime exceptions, for example, when the corresponding root has not been defined.
  • Can be slower because they perform a lookup by type and tag.

To control the generation of these methods, see the Resolve hint.

Dispose and DisposeAsync

Provides a mechanism to release unmanaged resources. These methods are generated only if the composition contains at least one singleton/scoped instance that implements either IDisposable or IAsyncDisposable. The Dispose() or DisposeAsync() method of the composition should be called to dispose of all created singleton/scoped objects:

using var composition = new Composition();

or

await using var composition = new Composition();

To dispose objects of other lifetimes, see this or this example.

Bindings

Bindings

Bindings are the core mechanism of Pure.DI, used to define how types are created and which contracts they fulfill.

Overview

For Implementations

To bind a contract to a specific implementation:

.Bind<Contract1>(tags).Bind<ContractN>(tags)
    .Tags(tags)
    .As(lifetime)
    .To<Implementation>()

Alternatively, you can bind multiple contracts at once:

.Bind<Contract1, Contract2>(tags)
    .To<Implementation>()

Example:

.Bind<IService>().To<Service>()

Nullable reference type contracts

When nullable reference types are enabled, Pure.DI treats nullable annotations as part of the dependency contract while it builds the graph and generates code. This makes T and T? different contracts for bindings, arguments, factories, and generic contracts.

DI.Setup("Composition")
    .Bind<string>().To(_ => "required")
    .Bind<string?>().To(_ => (string?)null)
    .Bind<IBox<TT>>().To<Box<TT>>()
    .Bind<IBox<TT?>>().To<NullableBox<TT>>()
    .Root<IBox<string>>("RequiredBox")
    .Root<IBox<string?>>("OptionalBox");

Matching rules:

  • An exact nullable match wins when both T and T? bindings exist.
  • A non-null binding T can satisfy a nullable dependency T?.
  • A nullable binding T? is not used for a non-null dependency T.
  • The same rules apply to factory bindings, ctx.Inject(...), Override, Let, composition arguments, root arguments, and open generic contracts such as IBox<T> and IBox<T?>.

For nullable generic arguments, make sure the generic type accepts nullable reference arguments. For example, prefer where T : class? when a contract such as IBox<string?> is valid.

Resolve<T>() preserves the nullable contract in generated code. Runtime methods Resolve(Type) and Resolve(Type, tag) receive only System.Type, so they cannot distinguish T from T?; Pure.DI reports warning DIW011 when nullable and non-nullable roots have the same runtime type while Resolve methods are generated.

For Factories

To use a custom factory logic via IContext:

.Bind<Contract1>(tags).Bind<ContractN>(tags)
    .Tags(tags)
    .As(Lifetime)
    .To(ctx => {
        ctx.Inject(out Dependency Dependency)
        return new Implementation(dependency);
    })

Example:

.Bind<IService>().To(ctx => {
    ctx.Inject(out IDependency dependency);
    return new Service(dependency);
})

Override Depth in Factories

Use Let to keep an override at the current injection level:

DI.Setup(nameof(Composition))
    .Bind().To<int>(_ => 7)
    .Bind().To<Dependency>()
    .Bind().To<Service>(ctx =>
    {
        // Override only the immediate injection
        ctx.Let(42);
        ctx.Inject(out Service service);
        return service;
    })
    .Root<Service>("Service");

Override precedence:

  • The nearest override wins for nested dependencies.
  • If multiple overrides target the same type and tag in one factory, the last call wins.
  • Let applies only to the current injection level.

For Simplified Factories

When you only need to inject specific dependencies without accessing the full context:

.Bind<Contract1>(tags).Bind<ContractN>(tags)
    .Tags(tags)
    .As(lifetime)
    .To<Implementation>((Dependency1 dep1, Dependency2 dep2) => new Implementation(dep1, dep2))

Example:

.Bind<IService>().To((IDependency dep) => new Service(dep))

Lifetimes

Lifetimes control how long an object lives and how it is reused:

  • Transient: A new instance is created for every injection (default).
  • Singleton: A single instance is created for the entire composition.
  • PerResolve: A single instance is reused within a single composition root (or a Resolve/ResolveByTag call).
  • PerBlock: Reuses instances within a code block to reduce allocations.
  • Scoped: A single instance is reused within a specific scope.

Default Lifetimes

You can set a default lifetime for all subsequent bindings in a setup:

.DefaultLifetime(Lifetime.Singleton)
// This will be a Singleton
.Bind<IInterface>().To<Implementation>()

Alternatively, you can set a default lifetime for a specific contract type:

.DefaultLifetime<IDisposable>(Lifetime.Singleton)

Tags

Tags allow you to distinguish between multiple implementations of the same contract.

  • Use .Bind<T>(tags) or .Tags(tags) to apply tags to a binding.
  • Use the [Tag(tag)] attribute or ctx.Resolve<T>(tag) to consume a tagged dependency.

Example:

.Bind<IService>("MyTag").To<Service>()

Implementation Bindings

Implementation bindings allow for a more concise syntax where the implementation type itself serves as the contract or where you want the binder to automatically infer suitable base types and interfaces.

For Implementations

// Infers all suitable base types and interfaces automatically
.Bind(tags).Tags(tags).As(Lifetime).To<Implementation>()

Alternatively, you can use the implementation type as the contract:

.Bind().To<Implementation>()

Example:

.Bind().To<Service>()

For Factories

.Bind(tags).Tags(tags).To(ctx => new Implementation())

Example:

.Bind().To(ctx => new Service())

For Simplified Factories

.Bind(tags).Tags(tags).To((Dependency dep) => new Implementation(dep))

Example:

.Bind().To((IDependency dep) => new Service(dep))

Special types will not be added to bindings

By default, Pure.DI avoids binding to special types during auto-inference to prevent polluting the container with unintended bindings for types like IDisposable, IEnumerable, or object. Special types will not be added to bindings by default:

  • 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 example:

.SpecialType<MonoBehaviour>()
.Bind().To<MyMonoBehaviourImplementation>()
// Now MonoBehaviour will not be added to the contracts

Simplified Lifetime-Specific Bindings

Pure.DI provides syntactic sugar for common lifetimes. These methods combine Bind(), .Tags(tags), As(Lifetime), and To() into a single call.

For Implementations

// Equivalent to Bind<T, T1, ...>(tags).As(Lifetime.Transient).To<Implementation>()
.Transient<T>(tags)
// or multiple types at once
.PerResolve<T, T1, ...>(tags)

Example:

.Transient<Service>()
.Singleton<Service2, Service3, Service4>()

For Factories

// Equivalent to Bind(tags).As(Lifetime.Singleton).To(ctx => ...)
.Singleton<Implementation>(ctx => new Implementation(), tags)

Example:

.Singleton<IService>(ctx => new Service())

For Simplified Factories

// Equivalent to Bind(tags).As(Lifetime.PerResolve).To((Dependency dep) => ...)
.PerResolve((Dependency dep) => new Implementation(dep), tags)

Example:

.PerResolve((IDependency dep) => new Service(dep))

Equivalent shortcuts exist for all lifetimes:

  • Transient<T>(...)
  • Singleton<T>(...)
  • Scoped<T>(...)
  • PerResolve<T>(...)
  • PerBlock<T>(...)
Setup hints

Setup hints

Hints are per-setup switches and filters that control how the composition code is generated (for example, whether Resolve methods are emitted, thread-safety, diagnostics hooks, or ToString() diagrams). Think of them as generator settings that let you trade off features, diagnostics, and compile-time cost.

Guidelines:

  • Prefer the fluent API for discoverability and refactoring safety; use comment directives for quick local overrides.
  • Hints affect only the DI.Setup(...) they are attached to (unless you use a global composition).
  • If you set the same hint multiple times, the last value wins.
DI.Setup("Composition")
    .Hint(Hint.Resolve, "Off")
    .Hint(Hint.ThreadSafe, "Off")
    .Hint(Hint.ToString, "On")
    ...

As an alternative to the fluent API, you can place comment directives before the Setup method in the form hint = value. For example:

// Resolve = Off
// ThreadSafe = Off
DI.Setup("Composition")
    ...

Both approaches can be mixed when it improves readability:

// Resolve = Off
DI.Setup("Composition")
    .Hint(Hint.ThreadSafe, "Off")
    ...
HintValuesC# versionDefault
ResolveOn or OffOn
OnNewInstanceOn or Off9.0Off
OnNewInstancePartialOn or OffOn
OnNewInstanceImplementationTypeNameRegularExpressionRegular expression.+
OnNewInstanceImplementationTypeNameWildcardWildcard*
OnNewInstanceTagRegularExpressionRegular expression.+
OnNewInstanceTagWildcardWildcard*
OnNewInstanceLifetimeRegularExpressionRegular expression.+
OnNewInstanceLifetimeWildcardWildcard*
OnDependencyInjectionOn or Off9.0Off
OnDependencyInjectionPartialOn or OffOn
OnDependencyInjectionImplementationTypeNameRegularExpressionRegular expression.+
OnDependencyInjectionImplementationTypeNameWildcardWildcard*
OnDependencyInjectionContractTypeNameRegularExpressionRegular expression.+
OnDependencyInjectionContractTypeNameWildcardWildcard*
OnDependencyInjectionTagRegularExpressionRegular expression.+
OnDependencyInjectionTagWildcardWildcard*
OnDependencyInjectionLifetimeRegularExpressionRegular expression.+
OnDependencyInjectionLifetimeWildcardWildcard*
OnCannotResolveOn or Off9.0Off
OnCannotResolvePartialOn or OffOn
OnCannotResolveContractTypeNameRegularExpressionRegular expression.+
OnCannotResolveContractTypeNameWildcardWildcard*
OnCannotResolveTagRegularExpressionRegular expression.+
OnCannotResolveTagWildcardWildcard*
OnCannotResolveLifetimeRegularExpressionRegular expression.+
OnCannotResolveLifetimeWildcardWildcard*
OnNewRootOn or OffOff
OnNewRootPartialOn or OffOn
ToStringOn or OffOff
ThreadSafeOn or OffOn
ResolveMethodModifiersMethod modifierpublic
ResolveMethodNameMethod nameResolve
ResolveByTagMethodModifiersMethod modifierpublic
ResolveByTagMethodNameMethod nameResolve
ObjectResolveMethodModifiersMethod modifierpublic
ObjectResolveMethodNameMethod nameResolve
ObjectResolveByTagMethodModifiersMethod modifierpublic
ObjectResolveByTagMethodNameMethod nameResolve
DisposeMethodModifiersMethod modifierpublic
DisposeAsyncMethodModifiersMethod modifierpublic
FormatCodeOn or OffOff
SeverityOfNotImplementedContractError or Warning or Info or HiddenError
CommentsOn or OffOn
SkipDefaultConstructorOn or OffOff
SkipDefaultConstructorImplementationTypeNameRegularExpressionRegular expression.+
SkipDefaultConstructorImplementationTypeNameWildcardWildcard*
SkipDefaultConstructorLifetimeRegularExpressionRegular expression.+
SkipDefaultConstructorLifetimeWildcardWildcard*
DisableAutoBindingOn or OffOff
DisableAutoBindingImplementationTypeNameRegularExpressionRegular expression.+
DisableAutoBindingImplementationTypeNameWildcardWildcard*
DisableAutoBindingLifetimeRegularExpressionRegular expression.+
DisableAutoBindingLifetimeWildcardWildcard*
LightweightAnonymousRootOn or OffOn
SystemThreadingLockOn or OffOn
ScopeMethodNameMethod name

The list of hints will be gradually expanded to meet the needs and desires for fine-tuning code generation. Please feel free to add your ideas.

Resolve Hint

Controls whether Resolve/ResolveByTag methods are generated. By default they are enabled. Turn this Off to reduce generated code size and compilation time, but note that anonymous roots are not created and only explicitly named roots are available. When disabled, always define public roots via Root<T>(string name).

// Resolve = Off
DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");

OnNewInstance Hint

Enables the OnNewInstance callback to observe or replace newly created instances (for example, logging, diagnostics, or decoration). It is Off by default to avoid overhead.

internal partial class Composition
{
    partial void OnNewInstance<T>(ref T value, object? tag, object lifetime) =>
        Console.WriteLine($"'{typeof(T)}'('{tag}') created.");
}

You can also replace the created instance with a T type, where T is the actual type of the created instance. To minimize performance loss when calling OnNewInstance, use the three hints below.

OnNewInstancePartial Hint

Controls whether the OnNewInstance partial method signature is generated when OnNewInstance is enabled. Turn it Off if you want to enable filtering hints without emitting the partial method.

// OnNewInstance = On
// OnNewInstancePartial = Off
DI.Setup("Composition")
    .Bind<IService>().To<Service>();

OnNewInstanceImplementationTypeNameRegularExpression Hint

This is a regular expression for filtering by instance type name. This hint is useful when OnNewInstance is in On state and it is necessary to limit the set of types for which the OnNewInstance method will be called.

// OnNewInstance = On
// OnNewInstanceImplementationTypeNameRegularExpression = .*Service
DI.Setup("Composition")
    .Bind<IService>().To<Service>();

OnNewInstanceImplementationTypeNameWildcard Hint

This is a Wildcard for filtering by instance type name. This hint is useful when OnNewInstance is in On state and it is necessary to limit the set of types for which the OnNewInstance method will be called.

// OnNewInstance = On
// OnNewInstanceImplementationTypeNameWildcard = *Service
DI.Setup("Composition")
    .Bind<IService>().To<Service>();

OnNewInstanceTagRegularExpression Hint

This is a regular expression for filtering by tag. This hint is also useful when OnNewInstance is in On state and it is necessary to limit the set of tags for which the OnNewInstance method will be called.

// OnNewInstance = On
// OnNewInstanceTagRegularExpression = Internal|Public
DI.Setup("Composition")
    .Bind<IService>("Internal").To<Service>();

OnNewInstanceTagWildcard Hint

This is a wildcard for filtering by tag. This hint is also useful when OnNewInstance is in On state and it is necessary to limit the set of tags for which the OnNewInstance method will be called.

// OnNewInstance = On
// OnNewInstanceTagWildcard = *Internal
DI.Setup("Composition")
    .Bind<IService>("Internal").To<Service>();

OnNewInstanceLifetimeRegularExpression Hint

This is a regular expression for filtering by lifetime. This hint is also useful when OnNewInstance is in On state and it is necessary to restrict the set of lifetimes for which the OnNewInstance method will be called.

// OnNewInstance = On
// OnNewInstanceLifetimeRegularExpression = Singleton|Scoped
DI.Setup("Composition")
    .Bind<IService>().As(Lifetime.Singleton).To<Service>();

OnNewInstanceLifetimeWildcard Hint

This is a wildcard for filtering by lifetime. This hint is also useful when OnNewInstance is in On state and it is necessary to restrict the set of lifetimes for which the OnNewInstance method will be called.

// OnNewInstance = On
// OnNewInstanceLifetimeWildcard = *Singleton
DI.Setup("Composition")
    .Bind<IService>().As(Lifetime.Singleton).To<Service>();

OnDependencyInjection Hint

Enables the OnDependencyInjection callback that can intercept dependency injection and return an alternative instance. Use for advanced scenarios like interception, conditional injection, or diagnostics. It is Off by default.

// OnDependencyInjection = On
// OnDependencyInjectionPartial = Off
// OnDependencyInjectionContractTypeNameRegularExpression = ICalculator[\d]{1}
// OnDependencyInjectionTagRegularExpression = Abc
DI.Setup("Composition")
    ...

OnDependencyInjectionPartial Hint

Controls whether the OnDependencyInjection partial method is generated. Because it returns a value, the method must be implemented when generated. Turn it Off to rely on filters without emitting the method body.

// OnDependencyInjection = On
// OnDependencyInjectionContractTypeNameRegularExpression = ICalculator[\d]{1}
// OnDependencyInjectionTagRegularExpression = Abc
DI.Setup("Composition")
    ...

To minimize performance loss when calling OnDependencyInjection, use the three tips below.

OnDependencyInjectionImplementationTypeNameRegularExpression Hint

This is a regular expression for filtering by instance type name. This hint is useful when OnDependencyInjection is in On state and it is necessary to restrict the set of types for which the OnDependencyInjection method will be called.

// OnDependencyInjection = On
// OnDependencyInjectionImplementationTypeNameRegularExpression = .*Service
DI.Setup("Composition")
    .Bind<IService>().To<Service>();

OnDependencyInjectionImplementationTypeNameWildcard Hint

This is a wildcard for filtering by instance type name. This hint is useful when OnDependencyInjection is in On state and it is necessary to restrict the set of types for which the OnDependencyInjection method will be called.

// OnDependencyInjection = On
// OnDependencyInjectionImplementationTypeNameWildcard = *Service
DI.Setup("Composition")
    .Bind<IService>().To<Service>();

OnDependencyInjectionContractTypeNameRegularExpression Hint

This is a regular expression for filtering by the name of the resolving type. This hint is also useful when OnDependencyInjection is in On state and it is necessary to limit the set of permissive types for which the OnDependencyInjection method will be called.

// OnDependencyInjection = On
// OnDependencyInjectionContractTypeNameRegularExpression = I.*Service
DI.Setup("Composition")
    .Bind<IService>().To<Service>();

OnDependencyInjectionContractTypeNameWildcard Hint

This is a wildcard for filtering by the name of the resolving type. This hint is also useful when OnDependencyInjection is in On state and it is necessary to limit the set of permissive types for which the OnDependencyInjection method will be called.

// OnDependencyInjection = On
// OnDependencyInjectionContractTypeNameWildcard = I*Service
DI.Setup("Composition")
    .Bind<IService>().To<Service>();

OnDependencyInjectionTagRegularExpression Hint

This is a regular expression for filtering by tag. This hint is also useful when OnDependencyInjection is in the On state and you want to limit the set of tags for which the OnDependencyInjection method will be called.

// OnDependencyInjection = On
// OnDependencyInjectionTagRegularExpression = Internal|Public
DI.Setup("Composition")
    .Bind<IService>("Internal").To<Service>();

OnDependencyInjectionTagWildcard Hint

This is a wildcard for filtering by tag. This hint is also useful when OnDependencyInjection is in the On state and you want to limit the set of tags for which the OnDependencyInjection method will be called.

// OnDependencyInjection = On
// OnDependencyInjectionTagWildcard = *Internal
DI.Setup("Composition")
    .Bind<IService>("Internal").To<Service>();

OnDependencyInjectionLifetimeRegularExpression Hint

This is a regular expression for filtering by lifetime. This hint is also useful when OnDependencyInjection is in On state and it is necessary to restrict the set of lifetime for which the OnDependencyInjection method will be called.

// OnDependencyInjection = On
// OnDependencyInjectionLifetimeRegularExpression = Singleton|Scoped
DI.Setup("Composition")
    .Bind<IService>().As(Lifetime.Singleton).To<Service>();

OnDependencyInjectionLifetimeWildcard Hint

This is a wildcard for filtering by lifetime. This hint is also useful when OnDependencyInjection is in On state and it is necessary to restrict the set of lifetime for which the OnDependencyInjection method will be called.

// OnDependencyInjection = On
// OnDependencyInjectionLifetimeWildcard = *Singleton
DI.Setup("Composition")
    .Bind<IService>().As(Lifetime.Singleton).To<Service>();

OnCannotResolve Hint

Enables the OnCannotResolve<T>(...) callback, allowing you to provide a fallback instance or custom error handling when a root cannot be resolved. It is Off by default. The generated method must be implemented because it returns a value.

// OnCannotResolve = On
// OnCannotResolveContractTypeNameRegularExpression = string|DateTime
// OnDependencyInjectionTagRegularExpression = null
DI.Setup("Composition")
    ...

To avoid missing failed bindings by mistake, use the two relevant hints below.

OnCannotResolvePartial Hint

Controls whether the OnCannotResolve<T>(...) partial method is generated when OnCannotResolve is enabled. Turn it Off to enable filters without emitting the partial method.

// OnCannotResolve = On
// OnCannotResolvePartial = Off
// OnCannotResolveContractTypeNameRegularExpression = string|DateTime
// OnDependencyInjectionTagRegularExpression = null
DI.Setup("Composition")
    ...

To avoid missing failed bindings by mistake, use the two relevant hints below.

OnNewRoot Hint

Enables the static OnNewRoot<TContract, T>(...) callback that runs when a new root is registered. This is useful for logging or custom validation, but it bypasses some dependency resolution checks.

// OnNewRoot = On
DI.Setup("Composition")
    ...

Be careful, this hint disables checks for the ability to resolve dependencies!

OnNewRootPartial Hint

Controls whether the OnNewRoot<TContract, T>(...) partial method is generated when OnNewRoot is enabled.

// OnNewRootPartial = Off
DI.Setup("Composition")
    ...

OnCannotResolveContractTypeNameRegularExpression Hint

This is a regular expression for filtering by the name of the resolving type. This hint is also useful when OnCannotResolve is in On state and it is necessary to limit the set of resolving types for which the OnCannotResolve method will be called.

// OnCannotResolve = On
// OnCannotResolveContractTypeNameRegularExpression = string|DateTime
DI.Setup("Composition")
    .Bind<IService>().To<Service>();

OnCannotResolveContractTypeNameWildcard Hint

This is a wildcard for filtering by the name of the resolving type. This hint is also useful when OnCannotResolve is in On state and it is necessary to limit the set of resolving types for which the OnCannotResolve method will be called.

// OnCannotResolve = On
// OnCannotResolveContractTypeNameWildcard = *Service
DI.Setup("Composition")
    .Bind<IService>().To<Service>();

OnCannotResolveTagRegularExpression Hint

This is a regular expression for filtering by tag. This hint is also useful when OnCannotResolve is in On state and it is necessary to limit the set of tags for which the OnCannotResolve method will be called.

// OnCannotResolve = On
// OnCannotResolveTagRegularExpression = Internal|Public
DI.Setup("Composition")
    .Bind<IService>("Internal").To<Service>();

OnCannotResolveTagWildcard Hint

This is a wildcard for filtering by tag. This hint is also useful when OnCannotResolve is in On state and it is necessary to limit the set of tags for which the OnCannotResolve method will be called.

// OnCannotResolve = On
// OnCannotResolveTagWildcard = *Internal
DI.Setup("Composition")
    .Bind<IService>("Internal").To<Service>();

OnCannotResolveLifetimeRegularExpression Hint

This is a regular expression for filtering by lifetime. This hint is also useful when OnCannotResolve is in the On state and it is necessary to restrict the set of lifetimes for which the OnCannotResolve method will be called.

// OnCannotResolve = On
// OnCannotResolveLifetimeRegularExpression = Singleton|Scoped
DI.Setup("Composition")
    .Bind<IService>().As(Lifetime.Singleton).To<Service>();

OnCannotResolveLifetimeWildcard Hint

This is a wildcard for filtering by lifetime. This hint is also useful when OnCannotResolve is in the On state and it is necessary to restrict the set of lifetimes for which the OnCannotResolve method will be called.

// OnCannotResolve = On
// OnCannotResolveLifetimeWildcard = *Singleton
DI.Setup("Composition")
    .Bind<IService>().As(Lifetime.Singleton).To<Service>();

ToString Hint

Controls generation of ToString() that returns a Mermaid class diagram of the composition. Useful for documentation and debugging, but disabled by default to reduce generated code.

// ToString = On
DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    .Root<IService>("MyService");

var composition = new Composition();
string classDiagram = composition.ToString();

ThreadSafe Hint

Controls whether object graph creation uses synchronization. It is On by default for safety. Turn it Off if you know composition creation is single-threaded and want a small performance gain.

// ThreadSafe = Off
DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    .Root<IService>("MyService");

ResolveMethodModifiers Hint

Overrides the modifiers of the public T Resolve<T>() method.

DI.Setup("Composition")
    .Hint(Hint.ResolveMethodModifiers, "internal")
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");

ResolveMethodName Hint

Overrides the method name for public T Resolve<T>().

DI.Setup("Composition")
    .Hint(Hint.ResolveMethodName, "GetRoot")
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");

ResolveByTagMethodModifiers Hint

Overrides the modifiers of the public T Resolve<T>(object? tag) method.

DI.Setup("Composition")
    .Hint(Hint.ResolveByTagMethodModifiers, "internal")
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");

ResolveByTagMethodName Hint

Overrides the method name for public T Resolve<T>(object? tag).

DI.Setup("Composition")
    .Hint(Hint.ResolveByTagMethodName, "GetRootByTag")
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");

ObjectResolveMethodModifiers Hint

Overrides the modifiers of the public object Resolve(Type type) method.

DI.Setup("Composition")
    .Hint(Hint.ObjectResolveMethodModifiers, "internal")
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");

ObjectResolveMethodName Hint

Overrides the method name for public object Resolve(Type type).

DI.Setup("Composition")
    .Hint(Hint.ObjectResolveMethodName, "GetObject")
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");

ObjectResolveByTagMethodModifiers Hint

Overrides the modifiers of the public object Resolve(Type type, object? tag) method.

DI.Setup("Composition")
    .Hint(Hint.ObjectResolveByTagMethodModifiers, "internal")
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");

ObjectResolveByTagMethodName Hint

Overrides the method name for public object Resolve(Type type, object? tag).

DI.Setup("Composition")
    .Hint(Hint.ObjectResolveByTagMethodName, "GetObjectByTag")
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");

DisposeMethodModifiers Hint

Overrides the modifiers of the public void Dispose() method.

DI.Setup("Composition")
    .Hint(Hint.DisposeMethodModifiers, "internal")
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");

DisposeAsyncMethodModifiers Hint

Overrides the modifiers of the public ValueTask DisposeAsync() method.

DI.Setup("Composition")
    .Hint(Hint.DisposeAsyncMethodModifiers, "internal")
    .Bind<IService>().To<Service>()
    .Root<IService>("Root");

FormatCode Hint

Enables formatting of generated code. This can significantly increase compilation time and memory usage, so it is best reserved for debugging or presentation scenarios.

// FormatCode = On
DI.Setup("Composition")
    .Bind<IService>().To<Service>();

SeverityOfNotImplementedContract Hint

Controls the diagnostic severity emitted when a binding declares a contract that the implementation does not actually implement. Possible values:

  • "Error" - this is the default value.
  • "Warning" - something suspicious but allowed.
  • "Info" - information that does not indicate a problem.
  • "Hidden" - not a problem.
DI.Setup("Composition")
    .Hint(Hint.SeverityOfNotImplementedContract, "Warning")
    .Bind<IService>().To<Service>();

Comments Hint

Specifies whether the generated code should be commented.

// Represents the composition class
DI.Setup(nameof(Composition))
    .Bind<IService>().To<Service>()
    // Provides a composition root of my service
    .Root<IService>("MyService");

Appropriate comments will be added to the generated Composition class and the documentation for the class, depending on the IDE used, will look something like this:

ReadmeDocumentation1.png

Then documentation for the composition root:

ReadmeDocumentation2.png

SkipDefaultConstructor Hint

Enables/disables skipping the default constructor. Default: Off (meaning the default constructor is used when available).

// SkipDefaultConstructor = On
DI.Setup("Composition")
    .Bind<IDependency>().To<Dependency>();

SkipDefaultConstructorImplementationTypeNameRegularExpression Hint

Regular expression filter for implementation type names when skipping default constructors. Default: .+.

// SkipDefaultConstructor = On
// SkipDefaultConstructorImplementationTypeNameRegularExpression = .*Repository
DI.Setup("Composition")
    .Bind().To<OrderRepository>();

SkipDefaultConstructorImplementationTypeNameWildcard Hint

Wildcard filter for implementation type names when skipping default constructors. Default: *.

// SkipDefaultConstructor = On
// SkipDefaultConstructorImplementationTypeNameWildcard = *Repository
DI.Setup("Composition")
    .Bind().To<OrderRepository>();

SkipDefaultConstructorLifetimeRegularExpression Hint

Regular expression filter for lifetimes when skipping default constructors. Default: .+.

// SkipDefaultConstructor = On
// SkipDefaultConstructorLifetimeRegularExpression = Singleton|Scoped
DI.Setup("Composition")
    .Bind().As(Lifetime.Singleton).To<OrderRepository>();

SkipDefaultConstructorLifetimeWildcard Hint

Wildcard filter for lifetimes when skipping default constructors. Default: *.

// SkipDefaultConstructor = On
// SkipDefaultConstructorLifetimeWildcard = *Singleton
DI.Setup("Composition")
    .Bind().As(Lifetime.Singleton).To<OrderRepository>();

Use these filters to restrict which implementation types or lifetimes are affected when SkipDefaultConstructor is enabled. You can apply them as comment directives or via the fluent API:

// SkipDefaultConstructor = On
// SkipDefaultConstructorImplementationTypeNameWildcard = *Repository
// SkipDefaultConstructorLifetimeRegularExpression = Singleton|Scoped
DI.Setup("Composition")
    .Bind().To<OrderRepository>()
    .Bind().To<CachedRepository>();

DisableAutoBinding Hint

Disables automatic binding when no explicit binding exists. Default: Off.

// DisableAutoBinding = On
DI.Setup("Composition")
    .Bind<IDependency>().To<Dependency>();

DisableAutoBindingImplementationTypeNameRegularExpression Hint

Regular expression filter for implementation type names to disable auto-binding. Default: .+.

// DisableAutoBinding = On
// DisableAutoBindingImplementationTypeNameRegularExpression = .*Service
DI.Setup("Composition")
    .Bind().To<OrderService>();

DisableAutoBindingImplementationTypeNameWildcard Hint

Wildcard filter for implementation type names to disable auto-binding. Default: *.

// DisableAutoBinding = On
// DisableAutoBindingImplementationTypeNameWildcard = *Service
DI.Setup("Composition")
    .Bind().To<OrderService>();

DisableAutoBindingLifetimeRegularExpression Hint

Regular expression filter for lifetimes to disable auto-binding. Default: .+.

// DisableAutoBinding = On
// DisableAutoBindingLifetimeRegularExpression = Singleton|Scoped
DI.Setup("Composition")
    .Bind().As(Lifetime.Singleton).To<OrderService>();

DisableAutoBindingLifetimeWildcard Hint

Wildcard filter for lifetimes to disable auto-binding. Default: *.

// DisableAutoBinding = On
// DisableAutoBindingLifetimeWildcard = *Singleton
DI.Setup("Composition")
    .Bind().As(Lifetime.Singleton).To<OrderService>();

Use these filters to narrow which types or lifetimes are excluded from auto-binding when DisableAutoBinding is enabled:

// DisableAutoBinding = On
// DisableAutoBindingImplementationTypeNameRegularExpression = .*Service
DI.Setup("Composition")
    .Bind().To<OrderService>()
    .Bind().To<PaymentService>();

SystemThreadingLock Hint

Indicates whether System.Threading.Lock should be used whenever possible instead of the classic approach of synchronizing object access using System.Threading.Monitor. On by default.

DI.Setup(nameof(Composition))
    .Hint(Hint.SystemThreadingLock, "Off")
    .Bind().To<Service>()
    .Root<Service>("MyService");

LightweightAnonymousRoot Hint

Controls whether anonymous composition roots are generated in a lightweight manner. When On (default), anonymous roots are optimized for minimal overhead. Set to Off if you need full debugging capabilities for anonymous roots.

// LightweightAnonymousRoot = Off
DI.Setup("Composition")
    .Bind<IService>().To<Service>();

ScopeMethodName Hint

Sets the scope factory name to be used for creating scopes.

// ScopeMethodName = CreateScope
DI.Setup("Composition")
    .Hint(Hint.ScopeMethodName, "CreateScope")
    .Bind<IDependency>().As(Lifetime.Scoped).To<Dependency>();
Interface generation

Interface generation

Pure.DI can generate interfaces from concrete classes, so a class can remain the implementation while consumers depend on a generated contract. Declare a partial interface and mark the implementation or individual members with GenerateInterface:

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

The generated interface preserves the public contract surface, including properties, methods, events, nullable annotations, generic members, and generic constraints. Members marked with IgnoreInterface are excluded from every generated interface:

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

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

The generated contract can also be customized with namespaceName and interfaceName, or split into several interfaces from one implementation:

public class Gateway : IReadGateway, IWriteGateway
{
    [GenerateInterface(interfaceName: nameof(IReadGateway))]
    public string Get(string path) => $"GET:{path}";

    [GenerateInterface(interfaceName: nameof(IWriteGateway))]
    public void Post(string path) {}
}

See also:

Comments

Pure.DI can copy comments from setup calls into generated documentation comments for the composition class, composition arguments, and composition roots.

Use regular // comments before API calls when you want Pure.DI to include the text in the generated documentation:

DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    // Provides the main service.
    .Root<IService>("Service");

For Setup(...) and composition roots, XML documentation comments written with /// replace the automatically generated documentation:

/// <summary>
/// Application composition.
/// </summary>
DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    /// <summary>
    /// Provides the main service.
    /// </summary>
    .Root<IService>("Service");

The generated root member keeps the user-defined XML documentation:

/// <summary>
/// Provides the main service.
/// </summary>
public IService Service
{
    get { ... }
}

For other setup calls, such as Arg<T>(...), comments are used as documentation text in the generated constructor documentation. Use regular // comments there unless you want XML markup to be shown as text.

Code generation workflow
flowchart TD
    start@{ shape: circle, label: Start }
    setups[fa:fa-search DI setups analysis]
    types["`fa:fa-search Types analysis
    constructors/methods/properties/fields`"]
    subgraph dep[Dependency graph]
    option[fa:fa-search Selecting a next dependency set]
    creating[fa:fa-cog Creating a dependency graph variant]
    verification{fa:fa-check-circle Verification}
    end
    codeGeneration[fa:fa-code Code generation]
    compilation[fa:fa-cog Compilation]
    failed@{ shape: dbl-circ, label: Compilation failed }
    success@{ shape: dbl-circ, label: Success }

    start ==> setups
    setups -.->|Has problems| failed
    setups ==> types
    types -.-> |Has problems| failed
    types ==> option
    option ==> creating
    option -.-> |There are no other options| failed
    creating ==> verification
    verification -->|Has problems| option
    verification ==>|Correct| codeGeneration
    codeGeneration ==> compilation
    compilation -.-> |Has problems| failed
    compilation ==> success

Project template

Install the DI template Pure.DI.Templates

dotnet new install Pure.DI.Templates

Create a "Sample" console application from the di template

dotnet new di -o ./Sample

Run it

dotnet run --project Sample

For more information about the template, please see this page.

Troubleshooting

Version update

When updating the version, it is possible that the previous version of the code generator remains active and is used by compilation services. In this case, the old and new versions of the generator may conflict. For a project where the code generator is used, it is recommended to do the following:

  • After updating the version, close the IDE if it is open
  • Delete the obj and bin directories
  • Run the following commands one by one
dotnet build-server shutdown
dotnet restore
dotnet build
Disabling API generation

Pure.DI automatically generates its API. If an assembly already has the Pure.DI API, for example, from another assembly, it is sometimes necessary to disable its automatic generation to avoid ambiguity. To do this, you need to add a DefineConstants element to the project files of these modules. For example:

<PropertyGroup>
    <DefineConstants>$(DefineConstants);PUREDI_API_SUPPRESSION</DefineConstants>
</PropertyGroup>
Display generated files

You can set project properties to save generated files and control their storage location. In the project file, add the <EmitCompilerGeneratedFiles> element to the <PropertyGroup> group and set its value to true. Build the project again. The generated files are now created in the obj/Debug/netX.X/generated/Pure.DI/Pure.DI/Pure.DI.SourceGenerator directory. The path components correspond to the build configuration, the target framework, the source generator project name, and the full name of the generator type. You can choose a more convenient output folder by adding the <CompilerGeneratedFilesOutputPath> element to the application project file. For example:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
    </PropertyGroup>

</Project>
Performance profiling

Please install the JetBrains.dotTrace.GlobalTools dotnet tool globally, for example:

dotnet tool install --global JetBrains.dotTrace.GlobalTools --version 2024.3.3

Or make sure it is installed. Add the following sections to the project:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <PureDIProfilePath>c:\profiling</PureDIProfilePath>
  </PropertyGroup>

  <ItemGroup>
    <CompilerVisibleProperty Include="PureDIProfilePath" />
  </ItemGroup>

</Project>

Replace a path like c:\profiling with the path where the profiling results will be saved.

Start a build and wait until a file like c:\profiling\pure_di_????.dtt appears in the directory.

Additional resources

Examples of how to set up a composition

Articles

Additional resources in Russian

Articles

DotNext video

DotNext Pure.DI

AI Context

AI needs to understand the situation it’s in (context). This means knowing details like API, usage scenarios, etc. This helps the AI give more relevant and personalized responses. So Markdown docs below can be useful if you or your team rely on an AI assistant to write code using Pure.DI:

AI context fileSizeTokens
AGENTS_SMALL.md62KB16K
AGENTS_MEDIUM.md112KB28K
AGENTS.md411KB105K

For different IDEs, you can use the AGENTS.md file as is by simply copying it to the root directory. For use with JetBrains Rider and Junie, please refer to these instructions. For example, you can copy any AGENTS.md file into your project (using Pure.DI) as .junie/guidelines.md.

How to contribute to Pure.DI

Thank you for your interest in contributing to the Pure.DI project! If you are planning a big change or feature, please open an issue first. That way, we can coordinate and understand whether the change fits current priorities and whether we can commit to reviewing and merging it within a reasonable timeframe. We do not want you to spend your valuable time on something that may not align with the direction of Pure.DI.

Contribution prerequisites: .NET SDK 10.0 or later installed.

This repository contains the following directories and files:

📁 .github                       GitHub related files and main.yml for building using GitGub actions
📁 .logs                         temporary files for generating the README.md file
📁 .run                          configuration files for the Rider IDE
📁 benchmarks                    projects for performance measurement
📁 build                         application for building locally and using CI/CD
📁 docs                          resources for the README.md file
📁 readme                        sample scripts and examples of application implementations
📁 samples                       sample projects
📁 src                           source codes of the code generator and all libraries
|- 📂 Pure.DI                    source code generator project
|- 📂 Pure.DI.Abstractions       abstraction library for Pure.DI
|- 📂 Pure.DI.Core               basic implementation of the source code generator
|- 📂 Pure.DI.MS                 project for integration with Microsoft DI
|- 📂 Pure.DI.Templates          project templates for creating .NET projects using Pure.DI
|- 📄 Directory.Build.props      common MSBUILD properties for all source code generator projects
|- 📄 Library.props              common MSBUILD properties for library projects such as Pure.DI.Abstractions
📁 tests                         contains projects for testing
|- 📂 Pure.DI.Example            project for testing some integration scenarios
|- 📂 Pure.DI.IntegrationTests   integration tests
|- 📂 Pure.DI.Tests              unit tests for basic functionality
|- 📂 Pure.DI.UsageTests         usage tests, used for examples in README.md
|- 📄 Directory.Build.props      common MSBUILD properties for all test projects
📄 LICENSE                       license file
📄 build.cmd                     Windows script file to run one of the build steps, see description below
📄 build.sh                      Linux/Mac OS script file to run one of the build steps, see description below
📄 .space.kts                    build file using JetBrains space actions
📄 README.md                     this README.md file
📄 SECURITY.md                   policy file for handling security bugs and vulnerabilities
📄 Directory.Build.props         basic MSBUILD properties for all projects
📄 Pure.DI.sln                   .NET solution file

The build logic is a regular .NET console application. You can use build.cmd and build.sh with the appropriate command parameters to perform all basic actions on the project, for example:

CommandsDescription
Generate AI context
bmRun benchmarks
cCompatibility checks
dpPackage deployment
eCreate examples
gBuild and test the source code generator
iInstall templates
lBuild and test libraries
pCreate NuGet packages
perfPerformance tests
pbPublish the balazor web sssembly example
rGenerate README.md
relGet release information
tCreate and deploy templates
teTest examples
uUpgrading the internal version of DI to the latest public version

For example, to build and test the source code generator:

./build.sh generator

or to run benchmarks:

./build.cmd benchmarks

If you are using the Rider IDE, it already has a set of configurations to run these commands. This project uses C# interactive build automation system for .NET. This tool helps to make .NET builds more efficient.

State of build

TestsExamplesPerformance
TestsExamplesPerformance

Thanks!

Benchmarks

BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.4894/22H2/2022Update) AMD Ryzen 9 5900X, 1 CPU, 24 logical and 12 physical cores .NET SDK 9.0.100

Transient
MethodMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
Hand Coded2.820 ns0.0419 ns0.0350 ns1.000.020.00140.000024 B1.00
Pure.DI composition root3.088 ns0.0493 ns0.0461 ns1.100.020.00140.000024 B1.00
Pure.DI Resolve()3.099 ns0.0666 ns0.0556 ns1.100.020.00140.000024 B1.00
Pure.DI Resolve(Type)6.624 ns0.3614 ns1.0656 ns2.350.380.00140.000024 B1.00
LightInject6.817 ns0.0597 ns0.0558 ns2.420.030.00140.000024 B1.00
Microsoft DI8.662 ns0.0450 ns0.0420 ns3.070.040.00140.000024 B1.00
Simple Injector10.092 ns0.0394 ns0.0307 ns3.580.040.00140.000024 B1.00
DryIoc10.987 ns0.0503 ns0.0471 ns3.900.050.00140.000024 B1.00
Unity3,393.989 ns45.3389 ns42.4101 ns1,203.7720.370.30900.00005176 B215.67
Autofac13,207.209 ns85.5680 ns80.0403 ns4,684.3061.851.81580.091630424 B1,267.67
Castle Windsor25,027.822 ns101.3300 ns94.7841 ns8,876.81109.923.05180.030551520 B2,146.67
Ninject128,400.442 ns3,583.9720 ns10,225.2745 ns45,540.773,648.866.95801.3428116912 B4,871.33

Transient details

Singleton
MethodMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
Hand Coded2.745 ns0.0343 ns0.0321 ns1.000.020.00140.000024 B1.00
Pure.DI composition root2.805 ns0.0680 ns0.0636 ns1.020.030.00140.000024 B1.00
Pure.DI Resolve()3.035 ns0.0303 ns0.0253 ns1.110.020.00140.000024 B1.00
Pure.DI Resolve(Type)6.552 ns0.1817 ns0.2719 ns2.390.100.00140.000024 B1.00
Microsoft DI9.770 ns0.0539 ns0.0504 ns3.560.040.00140.000024 B1.00
DryIoc10.695 ns0.0397 ns0.0372 ns3.900.050.00140.000024 B1.00
Simple Injector11.548 ns0.0561 ns0.0525 ns4.210.050.00140.000024 B1.00
LightInject365.712 ns1.0748 ns0.8391 ns133.261.530.00140.000024 B1.00
Unity2,235.283 ns16.7074 ns14.8106 ns814.5110.530.18690.00003184 B132.67
Autofac8,143.869 ns55.7270 ns49.4005 ns2,967.5137.581.31230.045822048 B918.67
Castle Windsor12,680.576 ns50.3451 ns47.0928 ns4,620.6254.471.37330.000023112 B963.00
Ninject65,921.823 ns856.5931 ns759.3474 ns24,020.97379.713.90630.976667040 B2,793.33

Singleton details

Func
MethodMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
Pure.DI composition root2.774 ns0.0205 ns0.0192 ns0.900.010.00140.000024 B1.00
Hand Coded3.089 ns0.0514 ns0.0480 ns1.000.020.00140.000024 B1.00
Pure.DI Resolve()3.265 ns0.0588 ns0.0491 ns1.060.020.00140.000024 B1.00
Pure.DI Resolve(Type)4.355 ns0.0301 ns0.0281 ns1.410.020.00140.000024 B1.00
DryIoc21.244 ns0.1662 ns0.1473 ns6.880.110.00720.0000120 B5.00
LightInject97.412 ns0.3602 ns0.3369 ns31.540.490.01480.0000248 B10.33
Unity1,389.847 ns8.7991 ns7.8002 ns449.977.210.15070.00002552 B106.33
Autofac4,800.897 ns15.6675 ns13.8888 ns1,554.3123.830.77820.007613128 B547.00

Func details

Enum
MethodMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
Pure.DI composition root8.284 ns0.0358 ns0.0335 ns0.970.010.00140.000024 B1.00
Hand Coded8.531 ns0.0436 ns0.0364 ns1.000.010.00140.000024 B1.00
Pure.DI Resolve()8.599 ns0.1092 ns0.0912 ns1.010.010.00140.000024 B1.00
Pure.DI Resolve(Type)9.931 ns0.0740 ns0.0618 ns1.160.010.00140.000024 B1.00
Microsoft DI19.950 ns0.1708 ns0.2280 ns2.340.030.00720.0000120 B5.00
LightInject54.081 ns0.4381 ns0.3658 ns6.340.050.02440.0000408 B17.00
DryIoc57.213 ns0.5495 ns0.4290 ns6.710.060.02440.0000408 B17.00
Unity1,516.437 ns8.2701 ns7.3313 ns177.761.110.12780.00002168 B90.33
Autofac12,752.668 ns47.7926 ns42.3669 ns1,494.917.831.57170.061026496 B1,104.00

Enum details

Array
MethodMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
Hand Coded49.80 ns0.313 ns0.293 ns1.000.010.02440.0000408 B1.00
Pure.DI Resolve(Type)51.19 ns0.426 ns0.398 ns1.030.010.02440.0000408 B1.00
Pure.DI composition root51.32 ns0.426 ns0.398 ns1.030.010.02440.0000408 B1.00
Pure.DI Resolve()52.24 ns1.072 ns1.003 ns1.050.020.02440.0000408 B1.00
LightInject58.74 ns1.209 ns1.734 ns1.180.030.02430.0000408 B1.00
DryIoc59.59 ns1.230 ns2.121 ns1.200.040.02430.0000408 B1.00
Unity3,257.81 ns64.417 ns138.665 ns65.422.790.86590.007614520 B35.59
Autofac13,300.06 ns219.742 ns194.795 ns267.084.071.57170.061026496 B64.94

Array details