Skip to content

DevTeam/Pure.DI

Repository files navigation

Pure DI for .NET

NuGet License

Key features

Pure.DI is NOT a framework or library, but a code generator that generates static method code to create an object graph in a pure DI paradigm using a set of hints that are verified at compile time. Since all the work is done at compile time, at run time you only have efficient code that is ready to be used. This generated code does not depend on library calls or .NET reflection and is efficient in terms of performance and memory consumption.

  • DI without any IoC/DI containers, frameworks, dependencies and therefore without any performance impact and side effects.

    Pure.DI is actually a .NET code generator. It generates simple code as well as if you were doing it yourself: de facto just a bunch of nested constructors` calls. And you can see this code at any time.

  • A predictable and verified dependency graph is built and verified on the fly while you write your code.

    All the logic for analyzing the graph of objects, constructors, methods happens at compile time. Thus, the Pure.DI tool notifies the developer about missing or circular dependency, for cases when some dependencies are not suitable for injection, etc., at compile-time. Developers have no chance of getting a program that crashes at runtime due to these errors. All this magic happens simultaneously as the code is written, this way, you have instant feedback between the fact that you made some changes to your code and your code was already checked and ready for use.

  • Does not add any dependencies to other assemblies.

    Using a pure DI approach, you don't add runtime dependencies to your assemblies.

  • High performance, including C# and JIT compilers optimizations.

    All generated code runs as fast as your own, in pure DI style, including compile-time and run-time optimizations. As mentioned above, graph analysis doing at compile-time, but at run-time, there are just a bunch of nested constructors, and that's it.

  • Works everywhere.

    Since a pure DI approach does not use any dependencies or the .NET reflection at runtime, it does not prevent your code from working as expected on any platform: Full .NET Framework 2.0+, .NET Core, .NET, UWP/XBOX, .NET IoT, Xamarin, Native AOT, etc.

  • Ease of use.

    The Pure.DI API is very similar to the API of most IoC/DI libraries. And it was a deliberate decision: the main reason is that programmers do not need to learn a new API.

  • Ultra-fine tuning of generic types.

    Pure.DI offers special type markers instead of using open generic types. This allows you to more accurately build the object graph and take full advantage of generic types.

  • Supports basic .NET BCL types out of the box.

    Pure.DI already supports many of BCL types like Array, IEnumerable, IList, ISet, Func, ThreadLocal, etc. without any extra effort.

Schrödinger's cat shows how it works CSharp

The reality is that

Cat

Let's create an abstraction

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

interface ICat { State State { get; } }

enum State { Alive, Dead }

Here is our implementation

class CardboardBox<T> : IBox<T>
{
    public CardboardBox(T content) => Content = content;

    public T Content { get; }
}

class ShroedingersCat : ICat
{
  // Represents the superposition of the states
  private readonly Lazy<State> _superposition;

  public ShroedingersCat(Lazy<State> superposition) => _superposition = superposition;

  // Decoherence of the superposition at the time of observation via an irreversible process
  public State State => _superposition.Value;

  public override string ToString() => $"{State} cat";
}

It is important to note that our abstraction and implementation do not know anything about DI magic or any frameworks. Also, please make attention that an instance of type Lazy<> was used here only as a sample. Still, using this type with nontrivial logic as a dependency is not recommended, so consider replacing it with some simple abstract type.

Let's glue all together

Add a package reference to

NuGet

Package Manager

Install-Package Pure.DI

.NET CLI

dotnet add package Pure.DI

Bind abstractions to their implementations or factories, define lifetimes and other options in a class like the following:

internal partial class Composition
{
  // In fact, this code is never run, and the method can have any name or be a constructor, for example,
  // and can be in any part of the compiled code because this is just a hint to set up an object graph.
  // Here the setup is part of the generated class, just as an example.
  private static void Setup() => DI.Setup(nameof(Composition))
      // Models a random subatomic event that may or may not occur
      .Bind<Random>().As(Singleton).To<Random>()
      // Represents a quantum superposition of 2 states: Alive or Dead
      .Bind<State>().To(ctx =>
      {
          ctx.Inject<Random>(out var random);
          return (State)random.Next(2);
      })
      // Represents schrodinger's cat
      .Bind<ICat>().To<ShroedingersCat>()
      // Represents a cardboard box with any content
      .Bind<IBox<TT>>().To<CardboardBox<TT>>()
      // Composition Root
      .Root<Program>("Root");
  }
}

The code above is just a chain of hints to define the dependency graph used to create a Composition class with a Root property that creates the Program composition root below. It doesn't really make sense to run this code because it doesn't do anything at runtime. So it can be placed anywhere in the class (in methods, constructors or properties) and preferably where it will not be called. Its purpose is only to check the dependency syntax and help build the dependency graph at compile time to create the Composition class. The first argument to the Setup method specifies the name of the class to be generated.

Time to open boxes!

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

  private readonly IBox<ICat> _box;

  internal Program(IBox<ICat> box) => _box = box;

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

Root is a Composition Root here, a single place in an application where the composition of the object graphs for an application takes place. Each instance is resolved by a strongly-typed block of statements like the operator new, which are compiling with all optimizations with minimal impact on performance or memory consumption. The generated Composition class contains a Root property that allows you to resolve an instance of the Program type.

Root property
public Sample.Program Root
{
  get
  {
    Func<State> stateFunc = new Func<State>(() =>
    {
      if (_randomSingleton == null)
      {
        lock (_lockObject)
        {
          if (_randomSingleton == null)
          {
            _randomSingleton = new Random();
          }
        }
      }
      
      return (State)_randomSingleton.Next(2);      
    });
    
    return new Program(
      new CardboardBox<ICat>(
        new ShroedingersCat(
          new Lazy<Sample.State>(
            stateFunc))));    
  }
}

You can find a complete analogue of this application with top level statements here. For a top level statements application the name of generated composer is "Composer" by default if it was not override in the Setup call.

Pure.DI works the same as calling a set of nested constructors, but allows dependency injection. And that's a reason to take full advantage of Dependency Injection everywhere, every time, without any compromise!

Just try!

Install the DI template Pure.DI.Templates

dotnet new -i Pure.DI.Templates

Create a "Sample" console application from the template di

dotnet new di -o ./Sample

And run it

dotnet run --project Sample

Please see this page for more details about the template.

Examples

Basics

Lifetimes

Attributes

Base Class Library

Interception

Hints

Composition class

Each generated class, hereinafter referred to as composition, needs to be configured. It starts with the Setup(...) method:

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

    internal Composition(Composition parent) { }

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

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

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

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

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

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

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

The second optional parameter can have several values to determine the kind of composition.

CompositionKind.Public

This is the default value. If this value is specified, a composition class will be created.

CompositionKind.Internal

If this value is specified, the class will not be generated, but this setup can be used for others as a base. For example:

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

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

When the composition CompositionKind.Public flag is set in the composition setup, it can also be the base composition for others like in the example above.

CompositionKind.Global

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

Constructors

Default constructor

Everything is quite banal, this constructor simply initializes the internal state.

Argument constructor

It replaces the default constructor and is only created if at least one argument is provided. For example:

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

In this case, the argument constructor looks like this:

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

and default constructor is missing.

Child constructor

This constructor is always available and is used to create a child composition based on the parent composition:

var parentComposition = new Composition();
var childComposition = new Composition(parentComposition); 

The child composition inherits the state of the parent composition in the form of arguments and singleton objects. States are copied, and compositions are completely independent, except when calling the Dispose() method on the parent container before disposing of the child container, because the child container can use singleton objects created before it was created.

Properties

To be able to quickly and conveniently create an object graph, a set of properties is generated. These properties are called compositions roots here. The type of the property is the type of a root object created by the composition. Accordingly, each access to the property leads to the creation of a composition with the root element of this type.

Public Composition Roots

To be able to use a specific composition root, that root must be explicitly defined by the Root method with a specific name and type:

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

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

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

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

Private Composition Roots

When the root name is empty, a private composition root is created. This root is used in these Resolve methods in the same way as public roots. For example:

DI.Setup("Composition")
    .Bind<IService>().To<Service>()
    .Root<IService>();
private IService Root1ABB3D0
{
    get { ... }
}

These properties have a random name and a private accessor and cannot be used directly from code. Don't try to use them. Private composition roots can be resolved by the Resolve methods.

Methods

Resolve

By default a set of four Resolve methods are 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 public composition roots as well as private roots and are useful when using the Service Locator approach when the code resolves composition roots in place:

var composition = new Composition();

composition.Resolve<IService>();

This is not recommended way to create composition roots. To control the generation of these methods, see the Resolve hint.

Dispose

Provides a mechanism for releasing unmanaged resources. This method is only generated if the composition contains at least one singleton object that implements the IDisposable interface. To dispose of all created singleton objects, call the composition Dispose() method:

using(var composition = new Composition())
{
    ...
}
Setup hints

Setup hints

Setup hints are comments before method Setup in the form hint = value that are used to fine-tune code generation. For example:

// Resolve = Off
// ThreadSafe = Off
// ToString = On
DI.Setup("Composition")
    ...
Hint Default C# version
Resolve On
OnInstanceCreation Off 9.0
OnInstanceCreationImplementationTypeNameRegularExpression .+
OnInstanceCreationTagRegularExpression .+
OnInstanceCreationLifetimeRegularExpression .+
OnDependencyInjection Off 9.0
OnDependencyInjectionImplementationTypeNameRegularExpression .+
OnDependencyInjectionContractTypeNameRegularExpression .+
OnDependencyInjectionTagRegularExpression .+
OnDependencyInjectionLifetimeRegularExpression .+
OnCannotResolve Off 9.0
OnCannotResolveContractTypeNameRegularExpression .+
OnCannotResolveTagRegularExpression .+
OnCannotResolveLifetimeRegularExpression .+
ToString Off
ThreadSafe On
ResolveMethodModifiers public
ResolveMethodName Resolve
ResolveByTagMethodModifiers public
ResolveByTagMethodName Resolve
ObjectResolveMethodModifiers public
ObjectResolveMethodName Resolve
ObjectResolveByTagMethodModifiers public
ObjectResolveByTagMethodName Resolve
DisposeMethodModifiers public

Resolve Hint

Determine whether to generate Resolve methods. By default a set of four Resolve methods are generated. Set this hint to Off to disable the generation of resolve methods. This will reduce class composition generation time and no private composition roots will be generated in this case. The composition will be tiny and will only have public roots. When the Resolve hint is disabled, only the public root properties are available, so be sure to define them explicitly with the Root<T>(...) method.

OnInstanceCreation Hint

Determine whether to generate partial OnInstanceCreation method. This partial method is not generated by default. This can be useful, for example, for logging:

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

You can also replace the created instance of type T, where T is actually type of created instance. To minimize the performance penalty when calling OnInstanceCreation, use the three related hints below.

OnInstanceCreationImplementationTypeNameRegularExpression Hint

It is a regular expression to filter by the instance type name. This hint is useful when OnInstanceCreation is in the On state and you want to limit the set of types for which the method OnInstanceCreation will be called.

OnInstanceCreationTagRegularExpression Hint

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

OnInstanceCreationLifetimeRegularExpression Hint

It is a regular expression to filter by the lifetime. This hint is useful also when OnInstanceCreation is in the On state and you want to limit the set of lifetime for which the method OnInstanceCreation will be called.

OnDependencyInjection Hint

Determine whether to generate partial OnDependencyInjection method to control of dependency injection. This partial method is not generated by default. It cannot have an empty body due to the return value. It must be overridden when generated. This can be useful, for example, for interception.

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

To minimize the performance penalty when calling OnDependencyInjection, use the three related hints below.

OnDependencyInjectionImplementationTypeNameRegularExpression Hint

It is a regular expression to filter by the instance type name. This hint is useful when OnDependencyInjection is in the On state and you want to limit the set of types for which the method OnDependencyInjection will be called.

OnDependencyInjectionContractTypeNameRegularExpression Hint

It is a regular expression to filter by the resolving type name. This hint is useful also when OnDependencyInjection is in the On state and you want to limit the set of resolving types for which the method OnDependencyInjection will be called.

OnDependencyInjectionTagRegularExpression Hint

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

OnDependencyInjectionLifetimeRegularExpression Hint

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

OnCannotResolve Hint

Determine whether to generate a partial OnCannotResolve<T>(...) method to handle a scenario where an instance which cannot be resolved. This partial method is not generated by default. It cannot have an empty body due to the return value. It must be overridden on creation.

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

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

OnCannotResolveContractTypeNameRegularExpression Hint

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

OnCannotResolveTagRegularExpression Hint

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

OnCannotResolveLifetimeRegularExpression Hint

It is a regular expression to filter by the lifetime. This hint is useful also when OnCannotResolve is in the On state and you want to limit the set of lifetime for which the method OnCannotResolve will be called.

ToString Hint

Determine if the ToString() method should be generated. This method provides a text-based class diagram in the format mermaid. To see this diagram, just call the ToString method and copy the text to this site.

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

ThreadSafe Hint

This hint determines whether object composition will be created in a thread-safe manner. This hint is On by default. It is good practice not to use threads when creating an object graph, in which case this hint can be turned off, which will lead to a slight increase in performance.

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

ResolveMethodModifiers Hint

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

ResolveMethodName Hint

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

ResolveByTagMethodModifiers Hint

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

ResolveByTagMethodName Hint

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

ObjectResolveMethodModifiers Hint

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

ObjectResolveMethodName Hint

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

ObjectResolveByTagMethodModifiers Hint

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

ObjectResolveByTagMethodName Hint

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

DisposeMethodModifiers Hint

Overrides modifiers of the method public void Dispose().

Development environment requirements

Supported frameworks

Troubleshooting

Generated files

You can set build properties to save the generated file and control where the generated files are stored. In a project file, add the element to a , and set its value to true. Build your project again. Now, the generated files are created under obj/Debug/netX.X/generated/Pure.DI/Pure.DI.SourceGenerator. The components of the path map to the build configuration, target framework, source generator project name, and fully qualified type name of the generator. You can choose a more convenient output folder by adding the element to the application's project file. For example:

<Project Sdk="Microsoft.NET.Sdk">
    
    <PropertyGroup>
        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
        <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
    </PropertyGroup>
    
</Project>
Log files

You can set build properties to save the log file. In the project file, add a element to the and set the path to the log directory, and add the related element <CompilerVisibleProperty Include="PureDILogFile" /> to the to make this property visible in the source generator. To change the log level, specify the same with the PureDISeverity property, as in the example below:

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

    <PropertyGroup>
        <PureDILogFile>.logs\Pure.DI.log</PureDILogFile>
        <PureDISeverity>Info</PureDISeverity>
    </PropertyGroup>

    <ItemGroup>
        <CompilerVisibleProperty Include="PureDILogFile" />
        <CompilerVisibleProperty Include="PureDISeverity" />
    </ItemGroup>

</Project>

The PureDISeverity property has several options available:

Severity Description
Hidden Debug information.
Info Information that does not indicate a problem (i.e. not prescriptive).
Warning Something suspicious, but allowed. This is the default value.
Error Something not allowed by the rules of the language or other authority.

Benchmarks

Transient
Method MeanErrorStdDev MedianRatioRatioSD
'Hand Coded'0.0044 ns0.0068 ns0.0100 ns0.0000 ns
'Pure.DI composition root'3.3776 ns0.1252 ns0.2226 ns3.2929 ns
Pure.DI4.4854 ns0.1452 ns0.4118 ns4.3006 ns
'Pure.DI non-generic'7.0763 ns0.1496 ns0.1249 ns7.0460 ns
LightInject11.1969 ns0.2515 ns0.6073 ns10.9401 ns
DryIoc17.8456 ns0.3844 ns0.9060 ns17.6295 ns
SimpleInjector21.0332 ns0.4494 ns1.0594 ns20.7954 ns
MicrosoftDependencyInjection23.9191 ns0.5034 ns0.7687 ns23.6052 ns
Autofac7,403.1694 ns75.8546 ns63.3421 ns7,403.8330 ns
Singleton
Method MeanErrorStdDev MedianRatioRatioSD
'Hand Coded'0.0204 ns0.0233 ns0.0334 ns0.0000 ns
'Pure.DI composition root'3.5315 ns0.1535 ns0.4429 ns3.3817 ns
Pure.DI4.6739 ns0.1621 ns0.4755 ns4.5309 ns
'Pure.DI non-generic'7.6031 ns0.2085 ns0.5672 ns7.4964 ns
DryIoc18.3447 ns0.3970 ns0.7057 ns17.9864 ns
SimpleInjector21.8540 ns0.4615 ns0.7047 ns21.5849 ns
MicrosoftDependencyInjection22.5521 ns0.4796 ns0.8400 ns22.2849 ns
LightInject29.9004 ns0.3955 ns0.3506 ns29.8662 ns
Autofac6,099.4316 ns115.2830 ns295.5144 ns6,022.7119 ns
Func
Method MeanErrorStdDevMedianRatioRatioSD
'Pure.DI composition root'70.54 ns1.152 ns1.183 ns70.87 ns0.900.05
Pure.DI76.32 ns1.713 ns5.024 ns73.97 ns1.010.08
'Hand Coded'76.88 ns1.557 ns3.701 ns75.23 ns1.000.00
'Pure.DI non-generic'90.67 ns4.649 ns13.561 ns88.75 ns1.230.18
DryIoc91.71 ns1.401 ns2.053 ns91.13 ns1.190.06
LightInject410.44 ns10.895 ns30.731 ns400.49 ns5.420.53
Autofac7,392.18 ns186.910 ns542.261 ns7,226.87 ns95.438.62
Array
Method MeanErrorStdDevMedianRatioRatioSD
'Hand Coded'83.25 ns3.199 ns9.128 ns80.64 ns1.000.00
Pure.DI86.42 ns2.273 ns6.701 ns85.16 ns1.050.14
'Pure.DI non-generic'92.44 ns2.890 ns8.523 ns91.41 ns1.120.17
'Pure.DI composition root'92.67 ns3.598 ns10.552 ns92.37 ns1.120.15
LightInject93.13 ns3.298 ns9.357 ns92.00 ns1.130.17
DryIoc100.80 ns3.600 ns10.614 ns102.84 ns1.230.20
Autofac10,095.91 ns241.064 ns679.926 ns9,899.44 ns122.6316.00
Enum
Method MeanErrorStdDevMedianRatioRatioSD
'Hand Coded'180.3 ns3.97 ns11.70 ns176.1 ns1.000.00
Pure.DI192.8 ns4.23 ns12.41 ns186.9 ns1.070.09
'Pure.DI composition root'196.1 ns3.88 ns8.01 ns193.4 ns1.080.08
'Pure.DI non-generic'199.2 ns3.63 ns4.59 ns197.7 ns1.070.06
MicrosoftDependencyInjection207.6 ns4.10 ns3.63 ns207.5 ns1.120.06
LightInject209.7 ns4.94 ns14.40 ns203.8 ns1.170.10
DryIoc224.0 ns6.84 ns19.73 ns218.6 ns1.240.12
Autofac10,355.1 ns206.58 ns486.94 ns10,186.9 ns57.614.24
Benchmarks environment

BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19045.2846/22H2/2022Update)
Intel Core i7-10850H CPU 2.70GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.100
  [Host]     : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2
  DefaultJob : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2