ManualDi
Fast and extensible C# dependency injection library without reflection. Works seamlessly in both Unity3d and plain C# projects
Install / Use
/learn @PereViader/ManualDiREADME
Welcome to ManualDi – a fast and extensible C# dependency injection library
- Unified API to create, inject, initialize and startup the application.
- Focuses on reducing boilerplate
- Synchronous and asynchronous library variants.
- Supercharge the container with tailored extensions for your application.
- Source generation, no reflection - Faster and more memory efficient than most other dependency injection containers.
- Seamless Unity3D game engine integration.
Benchmark and Comparison
BenchmarkDotNet Sync and Async benchmarks between Microsoft and ManualDi
| Method | Mean [ns] | Error [ns] | StdDev [ns] | Gen0 | Gen1 | Allocated [KB] |
|------------ |----------:|------------:|------------:|-------:|-------:|---------------:|
| NoContainer | 2.598 ns | 0.0811 ns | 0.1137 ns | 0.0005 | 0.0000 | 0.02 KB |
| ManualDi.Sync | 4,047 ns | 76.3106 ns | 74.9472 ns | 0.2747 | 0.0076 | 13.77 KB |
| ManualDi.Async | 6,787 ns | 91.7127 ns | 85.7881 ns | 0.3128 | 0.0153 | 15.4 KB |
| MicrosoftDi | 40,357 ns | 796.7055 ns | 1,142 ns | 2.5024 | 0.6714 | 122.87 KB |
Unity3d Sync and Async benchmarks between Zenject, VContainer, Reflex and ManualDi
- Zenject performance measured with Reflection Baking enabled
- VContainer performance measured with source generation enabled
- Performance measured on a windows standalone build
| |ManualDi.Sync|ManualDi.Async|Reflex|VContainer|Zenject| |---------------------------------|:-------------------------:|:--------------:|:--------------------:|:-------------------------------:|:-------------------------------:| | Lifetimes |Single<br/>Transient *(1)|Single *(2)|Single<br/>Transient|Single<br/>Transient<br/>Scoped|Single<br/>Transient<br/>Scoped| | Runtime (lower is better) |0.11|0.16|0.15|0.36|1| | Memory (lower is better) |0.12|0.14|0.21|0.59|1| | Object Injection |✅|✅|✅|✅|✅| | Scopes |✅|✅|✅|✅|✅| | Resolution During Installation |✅|✅|✅|❌|❌| | Object Initialization |✅|✅|❌|❌|❌| | Object Lifecycle Hooks |✅|✅|❌|❌|❌| | Startup Hooks |✅|✅|❌|❌|❌| | Lazy |❌ *(3)|❌ *(3)|✅|❌|✅| | Avoids Reflection |✅|✅|❌ *(4)|❌ *(4)|❌ *(4)|
*This table is still WIP, please open a discussion if you have any suggestion.
-
(1) ManualDi.Sync does not have Scoped scope.
- Scoped can be achived by setting up the binding on the child container. That child container is effectively another scope.
-
(2) ManualDi.Async only works with Single scope.
- Transient can be achived by setting up a factory class. The factory class can be used to create the instance at runtime.
- Scoped can be achived by setting up the binding on the child container. That child container is effectively another scope.
-
(3) ManualDi does not support lazy binding. All bound instances will get created and injected.
- Lazy bindings are usually a source of bugs and confusion.
-
(4) Reflex, VContainer, Zenject don't avoid Reflection by default but.
- They do work on IL2CPP (some have some caveats).
- Reflex only uses reflection on a few places.
- VContainer has an optional Source Generator that can replace the reflection based execution.
Installation
-
Plain C#: Install it using nuget (netstandard2.1)
- Sync: https://www.nuget.org/packages/ManualDi.Sync/
- Async: https://www.nuget.org/packages/ManualDi.Async/
-
Unity3d 2022.3.29 or later
- (Recommended) OpenUPM (instructions)
- Sync: https://openupm.com/packages/com.pereviader.manualdi.sync.unity3d/
- Async: https://openupm.com/packages/com.pereviader.manualdi.async.unity3d/
- Directly from git (instructions)
- Git URL: https://github.com/PereViader/ManualDi.Unity3d.git
- (Recommended) OpenUPM (instructions)
Note: * .Net Compact Framework is not compatible because of an optimization
Note: Source generation will only happen in csproj that are linked both with the source generator and the library.
- In a regular C# project, this requires referencing the library on the csproj as a nuget package
- In a Unity3d project, this requires adding the library to the project through the Package Manager and then referencing ManualDi on each assembly definition where you want to use it
Note: Source generator will never run on 3rd party libraries and System classes because they won't reference the generator.
Note: Source generation is opt-in. You must decorate your classes with [ManualDi] attribute for the generator to process them. This ensures better performance and avoids generating code for unrelated classes.
Limitation of the source generator:
- Does not run for partial classes defined across multiple declarations. It will only operate on partial classes that are declared once.
- Does not run for classes that use the
requiredkeyword
Container Lifecycle
- Binding Phase: Container binding configuration is defined
- Building Phase: Binding configuration is used to create the object graph
- Startup Phase: Startup callbacks are run.
- Alive Phase: Container is returned to the user and it can be kept until it is no longer necessary.
- Disposal Phase: The container and its resources are released.
In the section below we will add two examples to see an example of the lifecycle
- Creating the builder and installing the bindings is where the binding phase happens
- Within the execution of the
Buildmethod both the Building and Startup phase will happen- The container and object graph is be created
- Startup callbacks of the application are invoked
- Notice that the container variable is
usingorawait usingin order to ensure that the container is disposed of when it is no longer needed.
ManualDi.Sync sample
using DiContainer diContainer = new DiContainerBindings()
.Install(b => {
// Setup the instances involved in the object graph
// The order of instantiation, injection and subsequent initialization is the reverse of the dependency graph
// A consistent and reliable order of execution prevents issues that happen when instances are used when not yet properly initialized
b.Bind<SomeClass>().Default().FromConstructor();
b.Bind<IOtherClass, OtherClass>().Default().FromConstructor();
b.Bind<Startup>().Default().FromConstructor();
// Instruct the container the Startup logic to run once all dependencies created and initialized.
b.QueueStartup<Startup>(static startup => startup.Execute());
})
.Build();
public interface IOtherClass { }
[ManualDi]
public class OtherClass : IOtherClass, IDisposable
{
// Runs first because the class does not depend on anything else
public void Initialize() => Console.WriteLine("OtherClass.Initialize");
// Runs last because the class does not depend on anything else
public void Dispose() => Console.WriteLine("SomeClass.Dispose");
}
[ManualDi]
public class SomeClass(IOtherClass otherClass) : IDisposable
{
// SomeClass.Initialize runs after OtherClass.Initialize
public void Initialize() => Console.WriteLine("SomeClass.Initialize");
// SomeClass.Dispose runs before OtherClass.Dispose
public void Dispose() => Console.WriteLine("SomeClass.Dispose");
}
[ManualDi]
public class Startup(SomeClass someClass)
{
private IOtherClass otherClass;
//Inject runs after the constructor
public void Inject(IOtherClass otherClass) => this.otherClass = otherClass;
// Runs after SomeClass.Initialize and OtherClass.InitializeAsync
public vo
