GroveGames.DependencyInjection
0.8.1
dotnet add package GroveGames.DependencyInjection --version 0.8.1
NuGet\Install-Package GroveGames.DependencyInjection -Version 0.8.1
<PackageReference Include="GroveGames.DependencyInjection" Version="0.8.1" />
<PackageVersion Include="GroveGames.DependencyInjection" Version="0.8.1" />
<PackageReference Include="GroveGames.DependencyInjection" />
paket add GroveGames.DependencyInjection --version 0.8.1
#r "nuget: GroveGames.DependencyInjection, 0.8.1"
#:package GroveGames.DependencyInjection@0.8.1
#addin nuget:?package=GroveGames.DependencyInjection&version=0.8.1
#tool nuget:?package=GroveGames.DependencyInjection&version=0.8.1
GroveGames.DependencyInjection
A lightweight dependency injection framework for .NET, Unity, and Godot with build-time validation and lifecycle phases. Built for games, where startup order and per-frame cost matter.
Features
- Build-Time Validation: Missing registrations, duplicate service types and circular dependencies fail when the container is built, not during gameplay
- Lifecycle Phases: Sync and async pre-initialize, initialize and post-initialize phases; Unity and Godot add per-frame updates
- Automatic Detection: Lifecycle interfaces and
IDisposableare picked up from the implementation; there is nothing extra to register - Scoped Containers: Child containers per scene that resolve from their parent and are disposed with the scene
- Low Overhead: Constructors are discovered once at build time and singleton resolves do not allocate
- Source Generated: Registered constructors and
[Inject]methods are called through generated code instead of reflection, so IL2CPP code stripping and Native AOT trimming keep them - Native AOT Compatible: Trimming and AOT analyzers enabled on .NET 8 and later
- Unity Integration: Available as a Unity package
- Godot Integration: Available as a Godot addon
.NET
Install via NuGet:
dotnet add package GroveGames.DependencyInjection
Registering Services
using GroveGames.DependencyInjection;
var builder = new ContainerBuilder();
builder.AddSingleton<IAudioPlayer, AudioPlayer>();
builder.AddSingleton<GameLoop>();
builder.AddTransient<Enemy>();
builder.AddSingleton(settings);
builder.AddSingleton<IGateway>(resolver => new Gateway(resolver.Resolve<IClock>(), settings.Url));
using var root = builder.Build();
await root.InitializeAsync(cancellationToken);
Each registration binds exactly one service type: AddSingleton<T>() binds T and AddSingleton<TService, TImplementation>() binds TService. A singleton is one instance per container, so a child container can register its own. Value types work like any other service, for example builder.AddSingleton(new GameModeConfig(...)) and resolver.Resolve<GameModeConfig>(). To expose one instance under another type, register an alias factory:
builder.AddSingleton<ElephantAds>();
builder.AddSingleton<IAds>(resolver => resolver.Resolve<ElephantAds>());
A container never disposes or initializes an instance that it or a parent container already owns, so aliases are safe.
Lifecycle
| Order | Interface | Method |
|---|---|---|
| 1 | IPreInitializable |
void PreInitialize() |
| 2 | IAsyncPreInitializable |
ValueTask PreInitializeAsync(CancellationToken) |
| 3 | IInitializable |
void Initialize() |
| 4 | IAsyncInitializable |
ValueTask InitializeAsync(CancellationToken) |
| 5 | IPostInitializable |
void PostInitialize() |
| 6 | IAsyncPostInitializable |
ValueTask PostInitializeAsync(CancellationToken) |
Each phase finishes before the next one starts: its sync methods run first, then its async methods are awaited one at a time. Entries run in registration order within a step. Every entry point is constructed, with its dependencies injected, before any phase runs, so a constructor only receives dependencies and must not use them. To use another service's initialized state, initialize in a later step than it: an InitializeAsync can rely on any Initialize, but an Initialize that needs an InitializeAsync result belongs in PostInitialize. Lifecycle interfaces are detected on singletons only.
Updates
| Interface | Method |
|---|---|
IPreFrameUpdatable |
void PreFrameUpdate(float deltaTime) |
IFrameUpdatable |
void FrameUpdate(float deltaTime) |
IPostFrameUpdatable |
void PostFrameUpdate(float deltaTime) |
IPhysicsUpdatable |
void PhysicsUpdate(float deltaTime) |
The core only declares these interfaces; the Unity and Godot packages call them on the singletons of every initialized container until it is disposed. Each frame every container runs PreFrameUpdate before any container runs FrameUpdate, PostFrameUpdate runs after the engine's own per-frame scripts, and PhysicsUpdate runs on the physics step.
Child Containers
using var scene = root.CreateChild(builder => builder.AddSingleton<Level>());
await scene.InitializeAsync(cancellationToken);
Disposing a container cancels a pending initialization, disposes its children and then disposes every instance it created in reverse creation order. Instances passed to AddSingleton(instance) are owned by the caller and are not disposed.
Resolving All Implementations
ResolveAll<T>() returns every singleton of a container whose implementation is a T, in registration order, without instances owned by a parent container. The engine integrations use it to collect per-frame objects, and it works the same for your own interfaces:
var tickables = container.ResolveAll<ITickable>();
Method Injection
Objects the container does not construct, such as engine components, receive dependencies through [Inject] methods:
public sealed class HealthBar
{
[Inject]
public void Construct(IPlayer player)
{
}
}
container.Inject(healthBar);
Instances passed to AddSingleton(instance) are injected once when the container is built. Inject does nothing for an instance that the container or a parent already manages, so injecting a whole scene never injects a registered instance twice.
Source Generation
The package includes a source generator that runs in every project referencing it, with nothing to configure. For each type registered with AddSingleton<T>(), AddTransient<T>(), their two type argument forms or typeof(...), and for each class with [Inject] methods, it generates code that calls the constructor and the methods directly. The container picks the generated code up when it is built.
Anything the generator cannot call directly still works through reflection: private types and types nested in them, types registered only from an assembly other than the one that defines them, and registrations inside generic methods such as AddSingleton<T>() with a type parameter. On IL2CPP with a high managed stripping level, preserve those types with [Preserve] or a link.xml. [Inject] constructors and methods never need it, because InjectAttribute derives from a PreserveAttribute, which Unity's linker keeps.
Analyzers
The package also reports mistakes the compiler can already see, instead of leaving them to fail when the container is built or to be skipped silently:
| ID | Severity | Reported for |
|---|---|---|
| GGDI001 | Error | An abstract class or interface registered as an implementation |
| GGDI002 | Error | A registered implementation without a public constructor |
| GGDI003 | Warning | An [Inject] constructor or method that is never called because it is not public, is static or is generic |
| GGDI004 | Error | A constructor or [Inject] method with a ref, in or out parameter |
| GGDI005 | Warning | More than one [Inject] constructor on a type |
| GGDI006 | Disabled | A registered type that is constructed through reflection instead of generated code |
GGDI006 lists the types that the source generator cannot cover, which are the ones to preserve for IL2CPP code stripping. Turn it on in .editorconfig:
[*.cs]
dotnet_diagnostic.GGDI006.severity = suggestion
Missing registrations and circular dependencies are only known once installers are combined at runtime, so they are still reported when the container is built.
Core Components
ContainerBuilder: Collects registrations and builds a validated root containerIContainer: Resolves services, runs lifecycle phases and owns created instances;AddDisposablehands any other cleanup to the containerIObjectResolver:Resolve,ResolveAll,InjectandAddDisposable; it is what factories and services receiveIInstaller: Groups registrations for a container or child container
Unity
There are two installation steps required to use it in Unity.
Install
GroveGames.DependencyInjectionfrom NuGet using NuGetForUnity. Open Window from NuGet → Manage NuGet Packages, search "GroveGames.DependencyInjection" and press Install.Install the
GroveGames.DependencyInjection.Unitypackage by referencing the git URL:https://github.com/grovegs/DependencyInjection.git?path=src/GroveGames.DependencyInjection.Unity/Packages/com.grovegames.dependencyinjectionCreate a
csc.rspfile in yourAssets/directory with the following content to enable C# 10 features:-langversion:10 -nullable:enable
The NuGet package also contains the source generator. Unity runs it only when GroveGames.DependencyInjection.Generator.dll under Assets/Packages has the RoslynAnalyzer asset label; add the label in the Inspector if NuGetForUnity did not. Without it everything still works through reflection.
Root Installer
Derive from RootInstaller, create an asset for it and assign it in Edit → Project Settings → GroveGames → Dependency Injection. A project has one root installer; register everything that lives for the whole application in it. Settings are stored as a ScriptableObject in Assets/Settings/Resources/GroveGames/DependencyInjectionSettings.asset and loaded with Resources.Load, so every build profile ships them.
using GroveGames.DependencyInjection;
using GroveGames.DependencyInjection.Unity;
using UnityEngine;
[CreateAssetMenu(menuName = "Game/Root Installer")]
public sealed class GameRootInstaller : RootInstaller
{
[SerializeField] private AudioOutput _audioOutputPrefab;
public override void Install(IContainerBuilder builder)
{
builder.AddSingleton(resolver => resolver.Instantiate(_audioOutputPrefab));
builder.AddSingleton<IAudioPlayer, AudioPlayer>();
}
}
The root container is built before the first scene loads and initialized asynchronously.
Scene Installers
Add components derived from SceneInstaller to an active root GameObject of a scene. In the first installer's Awake, which runs at execution order int.MinValue before other scripts, every active root GameObject of the scene is deactivated so no other Awake, OnEnable or Start runs yet. Once the root has finished initializing and the scene has started, a child container of the root is built from every installer in the scene, [Inject] methods are called on every MonoBehaviour of the scene, and the container is fully initialized. Only then are the root GameObjects reactivated, so scene scripts start with their dependencies injected and every initializer complete. If initialization fails, the error is logged and the scene is started anyway. The container is disposed as soon as the scene's installers are destroyed, so nothing updates against destroyed objects while the scene unloads. Subclasses that override Awake or OnDestroy must call the base method. Scripts that another package or Project Settings → Script Execution Order places at int.MinValue too may run first; they are not injected and do not wait for initialization.
public sealed class BattleInstaller : SceneInstaller
{
[SerializeField] private BattleView _view;
public override void Install(IContainerBuilder builder)
{
builder.AddSingleton(_view);
builder.AddSingleton<BattleController>();
}
}
The update interfaces are called from the player loop, so no MonoBehaviour lifecycle methods are needed: PreFrameUpdate and FrameUpdate at the start of Update, PostFrameUpdate after all MonoBehaviour.Update calls, and PhysicsUpdate on the fixed timestep.
Loading Scenes
A scene that is already the active scene when its installer wakes up starts right away. Any other scene starts as soon as it becomes the active scene, or at the beginning of the next frame, whichever comes first. Calling ContainerBootstrapper.WhenSceneReadyAsync does not start it, so a loader may start waiting before making the scene active. A loader that loads a scene additively and then makes it active therefore initializes its container with that scene active, so objects created during initialization, even with new GameObject, land in the new scene and not in the loading scene.
Scene loaders do not need to know about containers, but a loader that should keep its loading screen up until the new scene is ready can wait for it, after making the scene active. ContainerBootstrapper.WhenSceneReadyAsync completes with true once the scene's container is initialized and its objects are reactivated, with true immediately for a loaded scene without installers, and with false if initialization failed or the scene was unloaded first.
public async Task ChangeSceneAsync(string sceneName)
{
var previous = SceneManager.GetActiveScene();
await SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
var scene = SceneManager.GetSceneByName(sceneName);
SceneManager.SetActiveScene(scene);
await ContainerBootstrapper.WhenSceneReadyAsync(scene);
await SceneManager.UnloadSceneAsync(previous);
}
Objects created through Instantiate by a scene container, or by a container created from it, are placed in that container's scene, whichever scene is active.
Code Outside Containers
Scene objects and objects created through Instantiate are injected automatically. For objects created elsewhere, such as by another package's Object.Instantiate, look up the container of their scene and inject them, or resolve from it directly. ContainerRegistry.TryGet returns the scene's container, or the root container for a scene without installers, and only once that container is initialized.
if (ContainerRegistry.TryGet(instance.scene, out var resolver))
{
resolver.InjectGameObject(instance);
}
Services do not need the registry: every container registers itself as IObjectResolver, so a service that creates objects takes it as a constructor parameter. Code without a scene, such as a ScriptableObject, can use ContainerRegistry.Root, which is null until the root container is initialized.
Unity Components
Unity types live in the GroveGames.DependencyInjection.Unity namespace.
RootInstaller: ScriptableObject installer for the root containerSceneInstaller: MonoBehaviour installer for a scene containerContainerBootstrapper: Builds and initializes the root and scene containers.WhenSceneReadyAsync(Scene)waits for a scene's containerContainerRegistry: Finds initialized containers:RootandTryGet(Scene). Containers are returned asIObjectResolver, so they cannot be disposed from outsideDependencyInjectionSettings: ScriptableObject holding the root installerInstantiate: Creates a component from a prefab or on a new GameObject and injects[Inject]methods on everyMonoBehaviourof it beforeAwakeruns. The container that created it owns the GameObject and destroys it when disposed, after the component itself. Without a parent, it is placed in the scene of a scene container, or kept across scene loads for the root containerInjectGameObject: Injects[Inject]methods on everyMonoBehaviourof an existing hierarchy
Godot
Install via NuGet:
dotnet add package GroveGames.DependencyInjection
Download the Godot addon from the latest release and extract it to your project's addons folder. Enable the addon in Project Settings → Plugins, which registers the ContainerBootstrapper autoload.
res://
├── addons/
│ └── GroveGames.DependencyInjection/
│ ├── plugin.cfg
│ └── ...
└── ...
Root Installer
Derive from RootInstaller, create a resource for it and assign it as the RootInstaller of a DependencyInjectionSettingsResource. A project has one root installer. The settings resource path is stored in project settings under grove_games/dependency_injection/settings_resource.
using Godot;
using GroveGames.DependencyInjection;
using GroveGames.DependencyInjection.Godot;
[GlobalClass]
public partial class GameRootInstaller : RootInstaller
{
public override void Install(IContainerBuilder builder)
{
builder.AddSingleton<IAudioPlayer, AudioPlayer>();
}
}
Scene Installers
Add a node derived from SceneInstaller to a scene. The installer belongs to the scene of its owner, or to itself when it has no owner. When it enters the tree, before any node of the scene runs _Ready:
- a child container of the root is built from every installer of that scene,
[Inject]methods are called on every node of the scene, except nodes of nested scenes with their own installers, which their own container injects,- the scene root's
ProcessModeis set toDisabled.
Once the root and the scene container finish initializing, the previous ProcessMode is restored, so _Process, _PhysicsProcess and input only start after every initializer is complete. _Ready still runs immediately, as Godot calls it when nodes enter the tree: dependencies are injected by then, but their initializers may still be running. If initialization fails, the error is pushed and processing starts anyway. The container is disposed when the scene root exits the tree.
public sealed partial class MainInstaller : SceneInstaller
{
[Export] private PlayerNode _player;
public override void Install(IContainerBuilder builder)
{
builder.AddSingleton(_player);
}
}
Godot Components
Godot types live in the GroveGames.DependencyInjection.Godot namespace.
RootInstaller: Resource installer for the root containerSceneInstaller: Node installer for a scene containerContainerBootstrapper: Autoload that builds containers and calls the update interfaces on their singletons:PreFrameUpdateandFrameUpdatebefore any other node's_Process,PostFrameUpdateafter every node's_Process, andPhysicsUpdatefrom_PhysicsProcess.WhenSceneReadyAsync(Node)completes withtrueonce the scene containing the node is initializedContainerRegistry: Finds initialized containers:Root, which isnulluntil the root container is initialized, andTryGet(Node), which returns the container of the node's scene, or the root for nodes outside scenes with installers, once it is initializedDependencyInjectionSettingsResource: Resource holding the root installerInjectTreeandInstantiate: Inject[Inject]methods on a node tree
Performance
Measured with BenchmarkDotNet on .NET 10 against Microsoft.Extensions.DependencyInjection (sandbox/DotnetBenchmark):
| Benchmark | GroveGames | Microsoft | Allocated (GroveGames / Microsoft) |
|---|---|---|---|
| Singleton resolve | 5.0 ns | 3.2 ns | 0 B / 0 B |
| Transient resolve, 3 deps | 32.8 ns | 6.4 ns | 24 B / 24 B |
| Factory resolve | 9.6 ns | 8.3 ns | 24 B / 24 B |
| Build, 4 registrations | 906 ns | 783 ns | 2736 B / 5664 B |
These numbers were measured before constructors were source generated; run sandbox/DotnetBenchmark for current results. Generated code is plain C#, so the same code runs on IL2CPP and Native AOT, where runtime code generation is not available.
Testing
Run tests:
dotnet test
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Write tests for new functionality
- Submit a pull request
License
This project is licensed under the MIT License - see the LICENSE file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- No dependencies.
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on GroveGames.DependencyInjection:
| Package | Downloads |
|---|---|
|
GroveGames.DependencyInjection.Godot
A lightweight dependency injection framework developed by Grove Games for .NET and Godot |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.8.1 | 115 | 10/12/2026 |
| 0.8.0 | 99 | 10/12/2026 |
| 0.7.1 | 91 | 10/9/2026 |
| 0.7.0 | 95 | 10/9/2026 |
| 0.6.8 | 104 | 10/8/2026 |
| 0.6.6 | 94 | 10/8/2026 |
| 0.6.5 | 96 | 10/8/2026 |
| 0.6.4 | 96 | 10/8/2026 |
| 0.6.3 | 92 | 10/8/2026 |
| 0.6.2 | 95 | 10/7/2026 |
| 0.6.1 | 95 | 10/7/2026 |
| 0.6.0 | 99 | 10/7/2026 |
| 0.5.0 | 97 | 10/7/2026 |
| 0.4.2 | 832 | 2/9/2025 |
| 0.4.1 | 242 | 2/9/2025 |
| 0.4.0 | 222 | 2/7/2025 |
| 0.3.2 | 284 | 1/23/2025 |
| 0.3.1 | 620 | 1/13/2025 |
| 0.3.0 | 209 | 12/10/2024 |