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.
interface IBox<out T> { T Content { get; } }
interface ICat { State State { get; } }
enum State { Alive, Dead }
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.
Add a package reference to
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.
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.
- Composition root
- Resolve methods
- Factory
- Injection
- Generics
- Arguments
- Tags
- Auto-bindings
- Child composition
- Multi-contract bindings
- Field Injection
- Property Injection
- Complex Generics
- Resolve Hint
- ThreadSafe Hint
- OnDependencyInjection Hint
- OnCannotResolve Hint
- OnInstanceCreation Hint
- ToString Hint
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.
This is the default value. If this value is specified, a composition class will be created.
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.
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
Everything is quite banal, this constructor simply initializes the internal state.
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.
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.
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.
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
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.
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 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")
...
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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();
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");
Overrides modifiers of the method public T Resolve<T>()
.
Overrides name of the method public T Resolve<T>()
.
Overrides modifiers of the method public T Resolve<T>(object? tag)
.
Overrides name of the method public T Resolve<T>(object? tag)
.
Overrides modifiers of the method public object Resolve(Type type)
.
Overrides name of the method public object Resolve(Type type)
.
Overrides modifiers of the method public object Resolve(Type type, object? tag)
.
Overrides name of the method public object Resolve(Type type, object? tag)
.
Overrides modifiers of the method public void Dispose()
.
- .NET and .NET Core 1.0+
- .NET Standard 1.0+
- Native AOT
- .NET Framework 2.0+
- UWP/XBOX
- .NET IoT
- Xamarin
- .NET Multi-platform App UI (MAUI)
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. |
Transient
Method | Mean | Error | StdDev | Median | Ratio | RatioSD |
---|---|---|---|---|---|---|
'Hand Coded' | 0.0044 ns | 0.0068 ns | 0.0100 ns | 0.0000 ns | ||
'Pure.DI composition root' | 3.3776 ns | 0.1252 ns | 0.2226 ns | 3.2929 ns | ||
Pure.DI | 4.4854 ns | 0.1452 ns | 0.4118 ns | 4.3006 ns | ||
'Pure.DI non-generic' | 7.0763 ns | 0.1496 ns | 0.1249 ns | 7.0460 ns | ||
LightInject | 11.1969 ns | 0.2515 ns | 0.6073 ns | 10.9401 ns | ||
DryIoc | 17.8456 ns | 0.3844 ns | 0.9060 ns | 17.6295 ns | ||
SimpleInjector | 21.0332 ns | 0.4494 ns | 1.0594 ns | 20.7954 ns | ||
MicrosoftDependencyInjection | 23.9191 ns | 0.5034 ns | 0.7687 ns | 23.6052 ns | ||
Autofac | 7,403.1694 ns | 75.8546 ns | 63.3421 ns | 7,403.8330 ns |
Singleton
Method | Mean | Error | StdDev | Median | Ratio | RatioSD |
---|---|---|---|---|---|---|
'Hand Coded' | 0.0204 ns | 0.0233 ns | 0.0334 ns | 0.0000 ns | ||
'Pure.DI composition root' | 3.5315 ns | 0.1535 ns | 0.4429 ns | 3.3817 ns | ||
Pure.DI | 4.6739 ns | 0.1621 ns | 0.4755 ns | 4.5309 ns | ||
'Pure.DI non-generic' | 7.6031 ns | 0.2085 ns | 0.5672 ns | 7.4964 ns | ||
DryIoc | 18.3447 ns | 0.3970 ns | 0.7057 ns | 17.9864 ns | ||
SimpleInjector | 21.8540 ns | 0.4615 ns | 0.7047 ns | 21.5849 ns | ||
MicrosoftDependencyInjection | 22.5521 ns | 0.4796 ns | 0.8400 ns | 22.2849 ns | ||
LightInject | 29.9004 ns | 0.3955 ns | 0.3506 ns | 29.8662 ns | ||
Autofac | 6,099.4316 ns | 115.2830 ns | 295.5144 ns | 6,022.7119 ns |
Func
Method | Mean | Error | StdDev | Median | Ratio | RatioSD |
---|---|---|---|---|---|---|
'Pure.DI composition root' | 70.54 ns | 1.152 ns | 1.183 ns | 70.87 ns | 0.90 | 0.05 |
Pure.DI | 76.32 ns | 1.713 ns | 5.024 ns | 73.97 ns | 1.01 | 0.08 |
'Hand Coded' | 76.88 ns | 1.557 ns | 3.701 ns | 75.23 ns | 1.00 | 0.00 |
'Pure.DI non-generic' | 90.67 ns | 4.649 ns | 13.561 ns | 88.75 ns | 1.23 | 0.18 |
DryIoc | 91.71 ns | 1.401 ns | 2.053 ns | 91.13 ns | 1.19 | 0.06 |
LightInject | 410.44 ns | 10.895 ns | 30.731 ns | 400.49 ns | 5.42 | 0.53 |
Autofac | 7,392.18 ns | 186.910 ns | 542.261 ns | 7,226.87 ns | 95.43 | 8.62 |
Array
Method | Mean | Error | StdDev | Median | Ratio | RatioSD |
---|---|---|---|---|---|---|
'Hand Coded' | 83.25 ns | 3.199 ns | 9.128 ns | 80.64 ns | 1.00 | 0.00 |
Pure.DI | 86.42 ns | 2.273 ns | 6.701 ns | 85.16 ns | 1.05 | 0.14 |
'Pure.DI non-generic' | 92.44 ns | 2.890 ns | 8.523 ns | 91.41 ns | 1.12 | 0.17 |
'Pure.DI composition root' | 92.67 ns | 3.598 ns | 10.552 ns | 92.37 ns | 1.12 | 0.15 |
LightInject | 93.13 ns | 3.298 ns | 9.357 ns | 92.00 ns | 1.13 | 0.17 |
DryIoc | 100.80 ns | 3.600 ns | 10.614 ns | 102.84 ns | 1.23 | 0.20 |
Autofac | 10,095.91 ns | 241.064 ns | 679.926 ns | 9,899.44 ns | 122.63 | 16.00 |
Enum
Method | Mean | Error | StdDev | Median | Ratio | RatioSD |
---|---|---|---|---|---|---|
'Hand Coded' | 180.3 ns | 3.97 ns | 11.70 ns | 176.1 ns | 1.00 | 0.00 |
Pure.DI | 192.8 ns | 4.23 ns | 12.41 ns | 186.9 ns | 1.07 | 0.09 |
'Pure.DI composition root' | 196.1 ns | 3.88 ns | 8.01 ns | 193.4 ns | 1.08 | 0.08 |
'Pure.DI non-generic' | 199.2 ns | 3.63 ns | 4.59 ns | 197.7 ns | 1.07 | 0.06 |
MicrosoftDependencyInjection | 207.6 ns | 4.10 ns | 3.63 ns | 207.5 ns | 1.12 | 0.06 |
LightInject | 209.7 ns | 4.94 ns | 14.40 ns | 203.8 ns | 1.17 | 0.10 |
DryIoc | 224.0 ns | 6.84 ns | 19.73 ns | 218.6 ns | 1.24 | 0.12 |
Autofac | 10,355.1 ns | 206.58 ns | 486.94 ns | 10,186.9 ns | 57.61 | 4.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