SkillAgentSearch skills...

ManualDi

Fast and extensible C# dependency injection library without reflection. Works seamlessly in both Unity3d and plain C# projects

Install / Use

/learn @PereViader/ManualDi

README

Test and publish Unity version 2022.3.29 OpenUPM ManualDi.Sync OpenUPM ManualDi.Async NuGet ManualDi.Sync NuGet ManualDi.Async Release

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

Unity3d-Container-Benchmark

  • 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

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 required keyword

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 Build method 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 using or await using in 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
View on GitHub
GitHub Stars65
CategoryDevelopment
Updated27d ago
Forks3

Languages

C#

Security Score

100/100

Audited on Mar 4, 2026

No findings