Welcome to ManualDi – a fast and extensible C# dependency injection library.
- Unified API to create, inject, initialize and startup the application.
- 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.
BenchmarkDotNet Sync and Async benchmarks between Microsoft and ManualDi
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
|----------------- |----------:|----------:|----------:|-------:|-------:|----------:|
| ManualDi.Sync | 4.244 us | 0.0823 us | 0.0948 us | 0.2747 | 0.0076 | 13.73 KB |
| ManualDi.Async | 6.858 us | 0.1267 us | 0.1185 us | 0.3128 | 0.0153 | 15.51 KB |
| MicrosoftDi | 39.258 us | 0.2415 us | 0.2259 us | 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
-
Plain C#: Install it using nuget (netstandard2.1)
-
Unity3d 2022.3.29 or later
- (Recommended) OpenUPM (instructions)
- Directly from git (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: A limitation of the source generator is that it does not run for partial classes defined across multiple declarations. It will only operate on partial classes that are declared once.
- Binding Phase: Container binding configuration is defined
- Building Phase: Binding configurastion 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, check the ApplicationEntryPoint 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
using _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 { }
public class OtherClass : IOtherClass
{
// Runs first because the class does not depend on anything else
public void Initialize() {
Console.WriteLine("OtherClass.Initialize");
}
}
public class SomeClass(IOtherClass otherClass)
{
// SomeClass.Initialize runs after OtherClass.Initialize
public void Initialize() {
Console.WriteLine("SomeClass.Initialize");
}
}
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 void Execute()
{
Console.WriteLine("Startup.Execute");
}
}await using _diContainer = await 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 async (startup, ct) => startup.Execute(ct));
})
.Build(CancellationToken.None);
public interface IOtherClass { }
public class OtherClass : IOtherClass
{
// Runs first because the class does not depend on anything else
public Task InitializeAsync(CancellationToken ct) {
Console.WriteLine("OtherClass.InitializeAsync");
return Task.CompletedTask;
}
}
public class SomeClass(IOtherClass otherClass)
{
// SomeClass.Initialize runs after OtherClass.InitializeAsync
public void Initialize() {
Console.WriteLine("SomeClass.Initialize");
}
}
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 async Task Execute(CancellationToken ct)
{
Console.WriteLine("Startup.Execute");
return Task.CompletedTask;
}
}Let's briefly discuss a few concepts from the library to get a high level overview
- The container is created using a builder
DiContainerBindings - Container instance creation are configured through
Bindmethod overloads. - The type exposed will depend on the
Bind<TConcrete>()/Bind<TApparent, TConcrete>()method used - Configuration of the binding is performed on the
Bindingobject returned by theBindmethod. - The library achieves speed via source generated, reflection-free,
DefaultandFromConstructormethods - Using the source-generated methods is optional but the container's recommended pattern
- The
FromConstructormethod configures the Binding to useTConcrete's constructor, resolving parameters from the container - There are many other construction methods other than
FromConstructoravailable. - Instances are created in inverse dependency order.
- Method injection is accomplished by adding an
Injectmethod to your instances. - Instance initialization is accomplished by adding an
InitializeorInitializeAsync** method. - The
Defaultmethod configures theBindingto useTConcrete'sInject,InitializeandInitializeAsync** methods - Instances are injected and initialized in the same order they are created
- Once the object graph is created and initialized,
QueueStartupcallbacks are invoked
The container configuration is done through a fluent API available on DiContainerBindings.
This process can only be done during the initial phase and should not be altered afterwards.
The fluent binding API begins by calling Binding<TApparent, TConcrete>.
- TApparent: It's the type that can be used when resolving the container.
- TConcrete: It's type of the actual instance behind the scenes.
Let's see an example
interface IInterface { }
class Implementation : IInterface {}
b.Bind<IInterface, Implementation>()...
...
c.Resolve<IInterface>() // Succeeds
c.Resolve<Implementation>() // Runtime errorUse the returned value of the bind method, to configure the binding and tell the container how it should create, inject, initialize and dispose the instance. The specific binding configuration is done through 9 categories of methods that, by convention, should be done in the order specified below.
// * means source generated
// ** means ManualDi.Async only
// TApparent is optional and will be equal to TConcrete when undefined
Bind<(TApparent,)? TConcrete>()
.Default*
.From[Constructor*|Instance|Method|MethodAsync**|...] //There are many more
.DependsOn**
.Inject
.Initialize
.Dispose
.WithId
.When([InjectedIntoId|InjectedIntoType])
.[Any other custom extension method your project implements]Keep in mind that you can bind multiple implementations to the same TApparent type.
When resolving, if multiple bindings match, the first one that satisfies the resolution rules will be returned.
b.Bind<SomeClass>().Default().FromConstructor(); // When calling c.Resolve<SomeClass>() this one is returned
b.Bind<SomeClass>().Default().FromConstructor();This source generated method is where most of the "magic" of the library happens.
The source generator will inspect the TConcrete type and generate code to register the Inject, Initialize and InitializeAsync** methods when available.
It will also inspect the type for IDisposable or IAsyncDisposable** and disable the automatic disposal behaviour when it does not implement it.
Think of this system as "duck typing" via source generation: if a method matches the expected name and signature, it will be invoked, regardless of interface inheritance.
For detailed behavior of each of the registered methods, refer to the sections below.
When there are multiple candidates for a given method:
- public methods are preferred over internal ones.
- in case multiple methods with the same visibility exist, the first one found top-to-bottom is selected.
public class A { }
public class B {
public void Inject(A a) { } //Sample only, prefer using the constructor
}
public class C {
public void Initialize() { }
}
public class D {
public D(A a) { }
public void Inject(C c, A a) { } //Sample only, prefer using the constructor
public Task InitializeAsync(CancellationToken ct) { ... }
}
b.Bind<A>().Default().FromConstructor(); // Default does not call anything
b.Bind<B>().Default().FromConstructor(); // Default calls Inject
b.Bind<C>().Default().FromConstructor(); // Default calls Initialize
b.Bind<D>().Default().FromConstructor(); // Default calls Inject and InitializeAsyncUsing Default is optional, but it is the recommended pattern in this library as it accelerates development.
By using it, developers only need to implement standardized DI boilerplate once.
Any subsequent changes to the types are automatically handled by the source generator.
For this reason, it’s best to always include it—even if the type doesn’t initially define any of the methods.
The From methods define the instantiation strategy the container should use for each binding.
This method is source generated ONLY when there is a single public/internal accessible constructor.
The container creates the instance using the constructor of the TConcrete type.
Dependencies necessary on the constructor will get resolved from the container.
b.Bind<T>().Default().FromConstructor();The container does not create the instance—it is provided externally and used as-is.
b.Bind<T>().Default().FromInstance(new T())The instance is created using the provided delegate. The container is passed in as a parameter, allowing it to resolve any required dependencies.
Note: All synchronous From configuration methods end up calling this one
b.Bind<T>().Default().FromMethod(c => new T(c.Resolve<SomeService>()))(**ManualDi.Async only)
The instance is created using the provided async delegate. The container and a cancellation token is passed in as a parameter, allowing it to resolve any required dependencies and handle cancellation.
Note: All asynchronous From configuration methods end up calling this one
b.Bind<T>().Default().FromMethodAsync(async (c, ct) => {
// Work can be delayed for any reason
// For example: HttpRequests, Loading from disk, etc
await Task.Delay(300, ct);
return new T(c.Resolve<SomeService>())
});You will rarely need to call this one.
Used for apparent type remapping. Or in other words, used to reexpose some binding as another binding.
Don't call Default when using this, otherwise you get multiple calls on Inject, Initialize and InitializeAsync** more than once
In the example below, there is SomeClass that is bound individually and then two more bindings expose the SomeClass
interface IFirst { }
interface ISecond { }
public class SomeClass : IFirst, ISecond { }
b.Bind<SomeClass>().Default().FromConstructor();
b.Bind<IFirst, SomeClass>().FromContainerResolve();
b.Bind<ISecond, SomeClass>().FromContainerResolve();However the recommended pattern when implementing this is to use the Bind overload with multiple TApparent
b.Bind<IFirst, ISecond, SomeClass>().Default().FromConstructor();The Inject method allows for post-construction injection of types.
Binding injection is done in reverse dependency order. Injected objects will already be injected themselves.
The injection can be used to hook into the object creation lifecycle and run custom code. Each individual binding can any amount of Inject calls and will run in order added.
As stated on the Default section, calling the source generated method will handle the registration of theInject method on TConcrete automatically.
The inject method has two usecases
- Inject dependencies when the constructor can't be used (this happens for instance in unity3d)
- Workaround Cyclic dependencies that prevent the object graph from being wired Warning: Cyclic dependencies usually highlight a problem in the design of the code. If you find such a problem in your codebase, consider redesigning the code before applying the following proposal.
This next example will fail
public class A(B b);
public class B(A a);In order to fix this, the chain must be broken with Inject.
When using ManualDi.Async adding the CyclicDependency** attribute is also necessary in order to break down cyclic dependencies. Without the attribute it might work, but just due to chance. Using it, will update the creation order of dependencies to avoid issues.
public class A(B b);
public class B
{
public void Inject(A a) { } //ManualDi.Sync this will work
public void Inject([CyclicDependency] A a) { } //ManualDi.Async
}Note: When dealing with cyclic dependencies, the usual method execution order may not hold. Normally, Initialize is called on a type’s dependencies before being called on the type itself. However, with cyclic dependencies, this order is not guaranteed, and additional synchronization logic may be required to ensure correct behavior. That said, cyclic dependencies are often a sign of flawed design. If you encounter them, it’s usually better to refactor the architecture rather than patch around the issue.
The Initialize method allows for post-injection initialization of types.
Binding initialization is done in reverse dependency order. Injected objects will already be injected themselves.
The injection can be used to hook into the object creation lifecycle and run custom code. Each individual binding can any amount of Initialize calls and will run in order added.
The initialization will not happen more than once for any instance.
As stated on the Default section, calling the source generated method will handle the Initialize method automatically.
b.Bind<object>()
.FromInstance(new object())
.Initialize((o, c) => Console.WriteLine("1")) // When object is resolved this is called first
.Initialize((o, c) => Console.WriteLine("2")); // And then this is calledpublic class A
{
public void Initialize() { }
}
public class B
{
public B(A a) { }
public void Initialize() { }
}
//This is the manual implementation without Default
b.Bind<A>().FromConstructor().Initialize((o, c) => o.Initialize()));
b.Bind<B>().FromConstructor().Initialize((o, c) => o.Initialize());
//And this is the equivalent and simpler implementation with Default
b.Bind<A>().Default().FromConstructor();
b.Bind<B>().Default().FromConstructor();Creating custom extension methods that call Initialize is the recommended way to supercharge the container.
class SomeFeature : IFeature { }
b.Bind<SomeFeature>()
.Default()
.FromConstructor()
.LinkFeature();Imagine you have an IFeature interface in your project and you want to add some shared initialization code to the ones that have it. You can add this code in an "Link" extension method. Internally this extension method should just call the Initialize method and add whatever extra logic the feature requires.
You may find further Link examples already present in the library here
The Dispose extension method allows defining behavior that will run when the object is disposed. The container will dispose of the objects when itself is disposed. The objects will be disposed in reverse dependency order.
If an object implements the IDisposable or IAsyncDisposable** interface, it doesn't need a manual Dispose call in the binding; it will be disposed of automatically.
Using the Default source-generated method provides a slight optimization by skipping a runtime check for IDisposable and IAsyncDisposable.
class A : IDisposable
{
public void Dispose() { }
}
class B
{
public B(A a) { }
public void DoCleanup() { }
}
b.Bind<A>().Default().FromConstructor(); // No need to call Dispose because the object is IDisposable
b.Bind<B>().Default().FromConstructor().Dispose((o,c) => o.DoCleanup());
// ...
B b = c.Resolve<B>();
c.Dispose(); // A is the first object disposed, then BWhen this extension method is used, the container skips the runtime check for IDisposable and IAsyncDisposable** during the disposal phase for the instance where it is used and, consequently, does not dispose the instance.
Delegates registered using the Dispose method will still be invoke.
These extension methods allow defining an id, enabling the filtering of elements during resolution.
b.Bind<int>().FromInstance(1).WithId("Potato");
b.Bind<int>().FromInstance(5).WithId("Banana");
// ...
c.Resolve<int>(x => x.Id("Potato")); // returns 1
c.Resolve<int>(x => x.Id("Banana")); // returns 5Note: This feature can be nice to use, prefer using it sparingly. This is because it introduces the need for the provider and consumer to share two points of information (Type and Id) instead of just one (Type)
An alternative to it is to register delegates instead. Delegates used this way encode the two concepts into one.
delegate int GetPotatoInt();
delegate int GetBananaInt();
b.Bind<GetPotatoInt>().FromInstance(() => 1);
b.Bind<GetPotatoInt>().FromInstance(() => 2);
int value1 = c.Resolve<GetPotatoInt>()(); // 1
int value2 = c.Resolve<GetBananaInt>()(); // 2The id functionality can be used on method and property dependencies by using the Inject attribute and providing a string id to it
class A
{
public void Inject(int a, [Id("Other")] object b) { ... }
}
//Will resolve the object with the requested Id
o.Inject(
c.Resolve<int>(),
c.Resolve<object>(x => x.Id("Other"))
)The When extension method allows defining filtering conditions as part of the bindings.
Allows filtering bindings using the injected Concrete type
class SomeValue(int Value) { }
class OtherValue(int Value) { }
class FailValue(int Value) { }
b.Bind<int>().FromInstance(1).When(x => x.InjectedIntoType<SomeValue>())
b.Bind<int>().FromInstance(2).When(x => x.InjectedIntoType<OtherValue>())
b.Bind<SomeValue>().Default().FromConstructor(); // will be provided 1
b.Bind<OtherValue>().Default().FromConstructor(); // will be provided 2
b.Bind<FailValue>().Default().FromConstructor(); // will fail at runtime when resolvedAllows filtering bindings using the injected Concrete type
class SomeValue(int Value) { }
b.Bind<int>().FromInstance(1).When(x => x.InjectedIntoId("1"));
b.Bind<int>().FromInstance(2).When(x => x.InjectedIntoId("2"));
b.Bind<SomeValue>().Default().FromConstructor().WithId("1"); // will be provided 1
b.Bind<SomeValue>().Default().FromConstructor().WithId("2"); // will be provided 2
b.Bind<FailValue>().Default().FromConstructor(); // will fail at runtime when resolvedThe instance is created using a sub-container built via the provided installer. This is useful for encapsulating parts of the object graph into isolated sub-containers. The sub-container inherits from the main container, allowing its bindings to depend on types registered in the parent. When using this approach, do not call Default on the main binding—Default should be invoked within the sub-container’s installation instead.
Question: When would I do this? Answer: For instance, think of a Unity3d game that has many enemies on a scene and you want to bind all enemies to the container so that their dependencies are setup and it is properly initialized.
class Enemy : MonoBehaviour
{
public void Inject(ParentDependency parentDependency, SubDependency subDependency) { }
public void Initialize() { }
}
class ParentDependency { }
class SubDependency {}
b.Bind<ParentDependency>().Default().FromConstructor();
foreach(var enemy in enemiesInScene) //enemiesInScene
{
b.BindSubContainer<Enemy>(sub => {
sub.Bind<Enemy>().Default().FromInstance(enemy);
sub.Bind<SubDependency>().Default().FromConstructor();
});
}Works just like BindSubContainer but the subcontainer will not inherit from the main container.
Thus nothing from the main container will be resolvable.
class Enemy : MonoBehaviour
{
public void Inject(SubDependency subDependency) { }
}
class SubDependency {}
foreach(var enemy in enemiesInScene)
{
b.BindIsolatedSubContainer<Enemy>(sub => {
sub.Bind<Enemy>().Default().FromInstance(enemy);
sub.Bind<SubDependency>().Default().FromConstructor();
});
}In order to group features in a sensible way, bindings will usually be grouped on Installer classes.
These classes may be implemented either as object instances that implement IInstaller or as extension methods.
Unless you explicitly need to use actual instances, this library recommends to prefer extension methods.
class A { }
class B { }
interface IC { }
class C : IC { }
//Extension method (recommended)
static class SomeFeatureInstaller
{
public static DiContainerBindings InstallSomeFunctionality(this DiContainerBindings b)
{
b.Bind<A>().Default().FromInstance(new A());
b.Bind<B>().Default().FromConstructor();
b.Bind<IC, C>().Default().FromConstructor();
return b;
}
}
//Or
class SomeFeatureInstaller : IInstaller
{
public static DiContainerBindings Install(DiContainerBindings b)
{
b.Bind<A>().Default().FromInstance(new A());
b.Bind<B>().Default().FromConstructor();
b.Bind<IC, C>().Default().FromConstructor();
return b;
}
}A common requirement when implementing gamemodes, doing A/B tests or taking any other data driven approach is to build different object graphs.
This can be accomplished in ManualDi by running different Bind statements depending on the data.
The available resolution methods are available on DiContainerBinding are:
- ResolveInstance
- TryResolveInstance
- ResolveInstanceNullable
- ResolveInstanceNullableValue
Bindings that are created using FromInstance can be resolved from DiContainerBindings after they have been bound. Keep in mind that instances are provided 'as is,' without any initialization or callbacks performed, because at the time of installation nothing will have been triggered yet.
When WithParentContainer is used, all bindings on the parent can container can be resolved
When BindSubContainer is used, the subcontainer can access the same the base installer could
Keep in mind that using this is not always necessary you can also provide parameters on extension method / instance installers. However this is not always possible and can introduce a lot of boilerplate thus adding complexity for little gain. Using this feature trades compilation safety for fewer bolierplate, thus you need to weight what is the best approach for your use case.
This could be some sample feature implemented using this
//On some installer do
b.Bind<SomeConfig>().FromInstance(new SomeConfig(IsEnabled: true))
//On another installer for the same container do
var config = b.ResolveInstance<SomeConfig>();
if(config.IsEnabled)
{
b.Bind<ISomeFeature, EnabledSomeFeature>().Default().FromConstructor();
}
else
{
b.Bind<ISomeFeature, DisabledSomeFeature>().Default().FromConstructor();
}When doing A/B tests and doing continuous integration, my recommendation is that you implement some feature flag source that is always available and allows you to conditionally toggle features on and off easily without needing to have a custom config for each one
//On some installer for the parent container do
b.Bind<IFeatureFlags, FeatureFlags>().Default().FromConstructor();
//On another installer for child container
var featureFlags = b.ResolveInstance<IFeatureFlags>();
if(featureFlags.IsEnabled(FeatureFlagConstants.SomeFeature))
{
b.Bind<ISomeFeature, EnabledSomeFeature>().Default().FromConstructor();
}
else
{
b.Bind<ISomeFeature, DisabledSomeFeature>().Default().FromConstructor();
}(ManualDi.Async**)
An async object graph might sometimes be complicated to understand the order things will run. When an exception happens during the DiContainer creation and initialization it can sometimes be difficult to understand why.
The failure debug report adds more data when an exception to the exception so that you can better understand the order in which things run.
This feature is opt in and can be used by enabling it on DiContainerBindings
try
{
await using var container = await new DiContainerBindings()
.Install(b =>
{
b.Bind<object>()
.DependsOn(x => x.ConstructorDependency<int>())
.FromMethod(x => throw new Exception());
b.Bind<int>();
})
.WithFailureDebugReport() // enable the report
.Build(CancellationToken.None);
}
catch (Exception e)
{
var report = (string)e.Data[DiContainer.FailureDebugReportKey]!; // get the report
//use this report to check the order of dependencies
return;
}The report will return the order of creation, injection and initialization. The example above returns
Apparent: System.Int32, Concrete: System.Int32, Id:
Apparent: System.Object, Concrete: System.Object, Id: Note: If you think there is some other piece of data that should be added open a discussion with the suggestion.
The source generator will take into account the nullability of the dependencies. If a dependency is nullable, the resolution will not fail if it is missing. If a dependency is not nullable, the resolution will fail if it is missing.
public class A
{
//object MUST be registered on the container
//int may or may not be registered on the container
public A(object obj, int? nullableValue) { }
}
b.Bind<A>().Default().FromConstructor();The source generator will inject all bound instances of a type if the dependency declared is one of the following types List<T> IList<T> IReadOnlyList<T> IEnumerable<T>
The resolved dependencies will be resolved using ResolveAll<T>
If the collection itself is declared nullable (e.g., List<T>?), it will be null if no matching bindings are found. Otherwise (e.g. List<T>), an empty list will be injected if no matching bindings are found. In both cases, when no matching bindings exist, the list will contain those instances found.
If the generic type argument T is nullable (e.g., List<T?>), the source generator will accommodate this. This is generally not recommended, as bindings are always expected to return non-null instances.
public class A
{
public A(
List<object> listObj,
IList<int> iListInt,
IReadOnlyList<obj> iReadOnlyListObj,
IEnumerable<int> iEnumerableInt,
List<object>? nullableList, //Either null or Count > 0
List<object?> nullableGenericList //Valid but NOT recommended
)
{
}
}
b.Bind<A>().Default().FromConstructor();Notice that resolution can only be done on apparent types, not concrete types. Concrete types are there so the container can provide a type safe fluent API.
If you use the source generated methods, you will usually not interact with the Resolution methods.
Resolutions can be done in the following ways:
Resolve an instance from the container. An exception is thrown if it can't be resolved.
SomeService service = container.Resolve<SomeService>();Resolve a reference type instance from the container. Returns null if it can't be resolved.
SomeService? service = container.ResolveNullable<SomeService>();Resolve a value type instance from the container. Returns null if it can't be resolved.
int? service = container.ResolveNullableValue<int>();Resolve an instance from the container. Returns true if found and false if not.
bool found = container.TryResolve<SomeService>(out SomeService someService);Resolve all the registered instance from the container. If no instances are available the list is empty.
List<SomeService> services = container.ResolveAll<SomeService>();The container provides functionality that queues work to be done once the container is built and ready. By using this you can define the entry points of your application declaratively during the installation of the container.
class Startup
{
public Startup(...) { ... }
public void Start() { ... }
}
class SomeService
{
public void Initialize() { ... }
}
b.Bind<SomeService>().Default().FromConstructor();
b.Bind<Startup>().Default().FromConstructor();
b.QueueStartup<Startup>(o => o.Start());In the snippet above, the following will happen when the container is built:
SomeServiceis createdStartupis createdSomeService'sInitializemethod is calledStartup'sStartmethod is called
When using the container in unity, do not rely on Awake / Start. Instead, rely on Inject / Initialize / InitializeAsync. You may still use Awake / Start if the classes involved are not injected through the container.
By relying on the container and not on native Unity3d callbacks you can be certain that the dependencies of your classes are Injected and Initialized in the proper order.
The container provides two specialized Installers
MonoBehaviourInstallerScriptableObjectInstaller
This is the idiomatic Unity way to have both the configuration and engine object references in the same place.
These classes just implement the IInstaller interface, there is no requirement for these classes to be used, so feel free to use IInstaller directly if you want.
public class SomeFeatureInstaller : MonoBehaviourInstaller
{
public Image Image;
public Toggle Toggle;
public Transform Transform;
public override void Install(DiContainerBindings b)
{
b.Bind<Image>().FromInstance(Image);
b.Bind<Toggle>().FromInstance(Toggle);
b.Bind<Transform>().FromInstance(Transform);
}
}When using the container in the Unity3d game engine the library provides specialized extensions for object construction
FromGameObjectGetComponent: Retrieves a component directly from a given GameObject.FromGameObjectGetComponentInChildren: Retrieves a component from the children of a given GameObject.FromGameObjectGetComponentInParent: Retrieves a component from the parent of a given GameObject.FromGameObjectAddComponent: Adds a new component to a GameObject and optionally schedules it for destruction on disposal.FromInstantiateComponent: Instantiates a given component, optionally setting a parent and scheduling it for destruction on disposal.FromInstantiateGameObjectGetComponent: Instantiates a GameObject and retrieves a specific component from it.FromInstantiateGameObjectGetComponentInChildren: Instantiates a GameObject and retrieves a component from one of its children.FromInstantiateGameObjectAddComponent: Instantiates a GameObject and adds a new component to it.FromAsyncInstantiateOperation**: Binds using a user-supplied asynchronous component instantiation operation.FromAsyncInstantiateOperationGetComponent**: Asynchronously instantiates a GameObject and retrieves a specific component from it.FromLoadSceneAsyncGetComponent**: Loads a scene additively and retrieves a specific component from the root GameObjects of the scene.FromLoadSceneAsyncGetComponentInChildren**: Loads a scene additively and retrieves a specific component from any children in the root GameObjects of the scene.FromAddressablesLoadAssetAsync**: Asynchronously loads an asset from the Addressables system using a key.FromAddressablesLoadAssetAsyncGetComponent**: Loads a GameObject asset from Addressables and retrieves a component from it.FromAddressablesLoadAssetAsyncGetComponentInChildren**: Loads a GameObject asset from Addressables and retrieves a component from its children.FromAddressablesLoadSceneAsyncGetComponent**: Loads a scene via Addressables and retrieves a specific component from the root GameObjects of the scene.FromAddressablesLoadSceneAsyncGetComponentInChildren**: Loads a scene via Addressables and retrieves a specific component from the root GameObjects of the scene.FromObjectResource: Loads an object from the Unity Resources folder.FromInstantiateGameObjectResourceGetComponent: Instantiates a GameObject from the Resources folder and retrieves a component from it.FromInstantiateGameObjectResourceGetComponentInChildren: Instantiates a GameObject from the Resources folder and retrieves a component from one of its children.FromInstantiateGameObjectResourceAddComponent: Instantiates a GameObject from the Resources folder and adds a new component to it.
Use them like this.
public class SomeFeatureInstaller : MonoBehaviourInstaller
{
public Transform canvasTransform;
public string ResourcePath;
public Toggle TogglePrefab;
public GameObject SomeGameObject;
public AddressableReference SceneReference;
public override Install(DiContainerBindings b)
{
b.Bind<Toggle>().FromInstantiateComponent(TogglePrefab, canvasTransform);
b.Bind<Image>().FromInstantiateGameObjectResourceGetComponent(ResourcePath);
b.Bind<SomeFeature>().Default().FromGameObjectGetComponent(SomeGameObject);
b.Bind<SceneReferences>().Default().FromAddressablesLoadAssetAsyncGetComponent(SceneReference);
}
}Serialize UnityEngine.Object dependancies should be serialized on installers and bound during installation.
Using public member variables to serialize references instead of [SerializeField] private ones should be prefered to avoid boilerplate.
Most of the From methods that do instantiation, have several optional parameters. For instance:
Transform? parent = nulldefines the parent transform used when instantiating new instances.bool destroyOnDispose = truewill cleanup instanciated instances upon disposal of the container
An entry point is the place where some context of your application is meant to start. In the case of ManualDi, it is where the object graph is configured and then the container is started.
The last binding of an entry point will usually make use of QueueStartup, to actually initiate the behaviour for the context it represents.
In simple terms, an EntryPoint is a root Installer where you call other Installers from
Root entry points will not depend on any other container. Root entry points may be started either manually or on the Unity Start callback. This is configured through the inspector.
Use the appropriate type depending on how you want to structure your application:
MonoBehaviourRootEntryPointScriptableObjectRootEntryPoint
public class Startup
{
public Startup(Dependency1 d1, Dependency2 d2) { ... }
public void Start() { ... }
}
class InitialSceneEntryPoint : MonoBehaviourRootEntryPoint
{
public Dependency1 dependency1;
public Dependency2 dependency2;
public override void Install(DiContainerBindings b)
{
b.Bind<Dependency1>().Default().FromInstance(dependency1);
b.Bind<Dependency2>().Default().FromInstance(dependency2);
b.Bind<Startup>().Default().FromConstructor();
b.QueueStartup<Startup>(o => o.Start());
}
}Subordinate entry points cannot are entry points that can not be started by themselves.
They need to be started by some other part of your application because they depend on external data / container.
These entry points may optionally also return some TFacade to the caller.
The data provided to the container is available on the entrypoint through the Data property.
When the data implements IInstaller it is also installed to the container.
When access to a parent container is necessary, doing it on the data type is the recommended pattern.
public class EntryPointData : IInstaller
{
public IDiContainer ParentDiContainer { get; set; }
public void Install(DiContainerBindings b)
{
b.WithParentContainer(ParentDiContainer);
}
}These entry points may also optionally return a TContext object resolved from the container.
That TContext can be used as a facade for the external system to interact with it.
Use the appropriate type depending on how you want to structure your application:
MonoBehaviourSubordinateEntryPoint<TData>MonoBehaviourSubordinateEntryPoint<TData, TContext>ScriptableObjectSubordinateEntryPoint<TData>ScriptableObjectSubordinateEntryPoint<TData, TContext>
Note: MonoBehaviour ones will probably be the most common
public class Startup
{
public Startup(Dependency1? d1, Dependency2 d2) { ... }
public void Start() { ... }
}
public class EntryPointData : IInstaller
{
public Dependency1? Dependency1 { get; set; }
public void Install(DiContainerBindings b)
{
if(Dependency1 is not null)
{
b.Bind<Dependency1>().FromInstance(Dependency1).SkipDisposable();
}
}
}
public class Facade : MonoBehaviour
{
private Dependency1? _dependency1;
private Dependency2 _dependency2;
public void Inject(Dependency1? dependency1, Dependency2 dependency2)
{
_dependency1 = dependency1;
_dependency2 = dependency2;
}
public void DoSomething1()
{
_dependency1?.DoSomething1();
}
public void DoSomething2()
{
_dependency2.DoSomething2();
}
}
class InitialSceneEntryPoint : MonoBehaviourSubordinateEntryPoint<EntryPointData, Facade>
{
public Dependency2 dependency2;
public Facade Facade;
public override void Install(DiContainerBindings b)
{
b.Bind<Dependency2>().Default().FromInstance(dependency2);
b.Bind<Facade>().Default().FromInstance(Facade);
b.Bind<Startup>().Default().FromConstructor();
b.QueueStartup<Startup>(o => o.Start());
}
}And this is an example of how a subordinate entry point on a scene or as a prefab could be initiated
public class Data
{
public string Name { get; set; }
}
public class SceneFacade
{
private readonly Data _data;
public SceneFacade(Data _data)
{
_data = data;
}
public void DoSomething()
{
Console.WriteLine(data.Name);
}
}
public class SceneEntryPoint : MonoBehaviourSubordinateEntryPoint<Data, SceneFacade>
{
public override void Install(DiContainerBindings b)
{
b.Bind<Data>().Default().FromInstance(Data);
b.Bind<SceneFacade>().Default().FromConstructor();
}
}
class Example
{
IEnumerator Run()
{
yield return SceneManager.LoadSceneAsync("TheScene", LoadSceneMode.Additive);
var entryPoint = Object.FindObjectOfType<SceneEntryPoint>();
var data = new Data() { Name = "Charles" };
var facade = entryPoint.Initiate(data)
facade.DoSomething();
}
}and this is an example of how you could initiate a subordinate entry point that is part of a prefab
class Example : MonoBehaviour
{
public SceneEntryPoint EntryPointPrefab;
void Start()
{
var data = new Data() { Name = "Charles" };
var entryPoint = Instantiate(EntryPointPrefab, transform);
var facade = entryPoint.Initiate(data)
facade.DoSomething();
}
}The container provides you with the puzzle pieces necessary. The actual composition of these pieces is up to you to decide. Feel free to ignore the container classes and implement your custom entry points if you have any special need.
Link methods are a great way to interconnect different features right from the container. The library provides a few, but adding your own custom ones for your use cases is a great way to speed up development.
LinkDontDestroyOnLoad: The GameObject associated with the bound component will haveDontDestroyOnLoadcalled on it when the container is bound. Behaviour can be customized with the optional parameters
class Installer : MonoBehaviourInstaller
{
public SomeService SomeService;
public override void Install(DiContainerBindings b)
{
b.Bind<SomeService>()
.Default()
.FromInstance(SomeService)
.LinkDontDestroyOnLoad();
}
}Note: There is a sample in the package that provides a Tickable system and a LinkTickable extension. This system allows for having Update like behaviour on any class.