CallumRose.EasyDI
1.1.0
dotnet add package CallumRose.EasyDI --version 1.1.0
NuGet\Install-Package CallumRose.EasyDI -Version 1.1.0
<PackageReference Include="CallumRose.EasyDI" Version="1.1.0" />
<PackageVersion Include="CallumRose.EasyDI" Version="1.1.0" />
<PackageReference Include="CallumRose.EasyDI" />
paket add CallumRose.EasyDI --version 1.1.0
#r "nuget: CallumRose.EasyDI, 1.1.0"
#:package CallumRose.EasyDI@1.1.0
#addin nuget:?package=CallumRose.EasyDI&version=1.1.0
#tool nuget:?package=CallumRose.EasyDI&version=1.1.0
EasyDI
EasyDI is a minimal and portable dependency injection framework designed for our Unity, Godot and C# projects.
⚠️ The Unity package ships prebuilt DLLs. If you change core, re-run
./scripts/vendor-unity-dlls.shand commit the result — see VENDORED.md.
Why
There are loads of DI frameworks out there. Here are some we've tried:
| Zenject | VContainer | Microsoft DI | Autofac | |
|---|---|---|---|---|
| Supports Unity | ✅ | ✅ | ⛔ | ⛔ |
| Supports Godot | ⛔ | ⛔ | ⛔ | ⛔ |
| Supports C# Projects | 〰️ | ⛔ | ✅ | ✅ |
| Simple | ⛔ | ✅ | ✅ | ⛔ |
| Currently Supported | ⛔ | ✅ | ✅ | ✅ |
| Container Hierarchies | ✅ | ✅ | ⛔ | ✅ |
| Post-Startup Registrations | ✅ | ✅ | ⛔ | ✅ |
None of these frameworks tick all our boxes. There are plenty more frameworks that aren't listed here, and most would be forgiven for not supporting Unity or Godot if they allowed post-startup registrations or hierarchies (i.e. supports new registrations on scene loads). But very few do because this is a very game-dev-specific use case, where most C# applications only need to register one layer of services at startup.
So we decided to make our own. EasyDI has a similar API to VContainer but with a leaner feature set and better portability.
What
- Constructor injection
- Fluent registration API
- Support for transient, scoped and singleton lifetimes
- Instantiation via reflection or factories
- Support for multiple resolvers in a hierarchy
- Can resolve lists of services
- Open generics
How
Basic Usage
EasyDI is simple to get up and running
var registry = ObjectRegistry.CreateRoot();
registry.RegisterSingleton<ServiceA>();
IObjectResolver resolver = resolverBuilder.Build();
var serviceA = resolver.Resolve<ServiceA>();
Lifetimes
registry.RegisterTransient<ServiceA>(); // New instance every time
registry.RegisterScoped<ServiceB>(); // One instance per resolver
registry.RegisterSingleton<ServiceC>(); // One instance shared by the resolver and its children
Instance Providers
// Just provides an existing instance
registry.RegisterInstance(new ServiceB());
// Creates a new instance using reflection, where the public constructor with the most resolvable parameters is used
registry.RegisterSingleton<ServiceA>();
// Uses a Func to create the instance
registry.RegisterSingleton<ServiceC>(_ => new ServiceC());
registry.RegisterSingleton<ServiceD>(resolver => new ServiceD(resolver.Resolve<ServiceC>()));
Registering As
These patterns apply to all lifetimes and instance providers.
You can register a type as an interface.
registry.RegisterSingleton<ServiceA>().As<IServiceA>();
// ...
var serviceInterface = resolver.Resolve<IServiceA>();
You can register a type as itself.
registry.RegisterSingleton<ServiceA>();
// ...
var serviceA = resolver.Resolve<ServiceA>();
If you want to register a type as itself and other interfaces, you need to explicitly include itself.
registry.RegisterSingleton<ServiceA>()
.As<ServiceA>()
.As<IDisposable>();
// ...
var service = resolver.Resolve<ServiceA>();
var disposable = resolver.Resolve<IDisposable>();
You can register a type as multiple interfaces.
registry.RegisterSingleton<ServiceA>()
.As<IServiceA>()
.As<IDisposable>();
// ...
var serviceInterface = resolver.Resolve<IServiceA>();
var disposable = resolver.Resolve<IDisposable>();
If you don't know if the actual instance type is assignable to an interface, you can use TryAs which will ignore any incompatible types.
public class ServiceA : IDisposable { };
public interface IServiceB;
public class ServiceB : IServiceB;
ServiceA service = GetService();
registry.RegisterInstance(service)
.TryAs<IServiceB>()
.TryAs<IDisposable>();
Assert.IsTrue(resolver.CanResolve<IDisposable>());
if (service is IServiceB)
{
Assert.IsTrue(resolver.CanResolve<IServiceB>());
}
else
{
Assert.IsFalse(resolver.CanResolve<IServiceB>());
}
Registration Arguments
You can provide specific arguments to be used when constructing an instance. Internally, these are treated the same as any other resolvable type so order doesn't matter. However, because of this they also cannot override existing registrations: see Duplicate Registrations
public class ServiceA(string Name, IDisposable Disposable) { }
public class DiposableService : IDisposable { }
registry.RegisterSingleton<ServiceA>()
.WithArgument<IDisposable>(new DiposableService())
.WithArgument("Dennis");
Hierarchies
Resolvers inherit registrations from a parent. Singleton services are shared, scoped are unique per resolver, and transient are always new.
var rootRegistry = ObjectRegistry.CreateRoot();
rootRegistry.RegisterSingleton<ServiceA>();
IObjectResolver rootResolver = rootRegistry.Build();
var childRegistry = ObjectRegistry.CreateChild(rootResolver);
childRegistry.RegisterSingleton<ServiceB>();
IObjectResolver childResolver = childRegistry.Build();
var serviceAFromRoot = rootResolver.Resolve<ServiceA>();
var canResolveServiceBFromRoot = rootResolver.CanResolve<ServiceB>();
Assert.IsFalse(canResolveServiceBFromRoot);
var serviceAFromChild = childResolver.Resolve<ServiceA>();
var serviceBFromChild = childResolver.Resolve<ServiceB>();
Assert.AreSame(serviceAFromRoot, serviceAFromChild);
Duplicate Registrations
In the Same Registry
You can register multiple registrations that are resolvable to the same type. When resolved they're returned in the order they were registered. However, this has some caveats:
- The resolvable type must be explicitly marked using
MarkResolvableAsMany<T>, otherwise the registry will throw on build. - You can no longer resolve the type as a single instance, only as a collection.
- If no registrations are found, an empty list will be returned.
public interface IService { }
public class ServiceA : IService { }
public class ServiceB : IService { }
registry.MarkResolvableAsMany<IService>();
registry.RegisterSingleton<ServiceA>().As<IService>();
registry.RegisterSingleton<ServiceB>().As<IService>();
// ...
var enumerable = resolver.Resolve<IEnumerable<IService>>();
var collection = resolver.Resolve<ICollection<IService>>();
var readonlyList = resolver.Resolve<IReadOnlyList<IService>>();
Assert.That(() => resolver.Resolve<IService>(), Throws.Exception);
If you've explicitly registered an
IReadOnlyList<T>,ICollection<T>orIEnumerable<T>(which you shouldn't but) you'll have to resolve that explicitly usingresolver.TryResolve(new SingleInstanceQuery(typeof(IReadOnlyList<T>), out var instance)).
In Hierarchy
To avoid any ambiguity, you cannot have duplicate registrations in a resolver hierarchy.
The only exception to this is you can register a "many" type in a child resolver that is also "many" in its parent(s). This will only return instances from the local resolver.
public interface IService { }
public class ServiceA : IService { }
public class ServiceB : IService { }
public class ServiceC : IService { }
public class ServiceD : IService { }
var rootRegistry = ObjectRegistry.CreateRoot();
rootRegistry.MarkResolvableAsMany<IService>();
rootRegistry.RegisterSingleton<ServiceA>().As<IService>();
rootRegistry.RegisterSingleton<ServiceB>().As<IService>();
IObjectResolver rootResolver = rootRegistry.Build();
var childRegistry = ObjectRegistry.CreateChild(rootResolver);
childRegistry.MarkResolvableAsMany<IService>();
childRegistry.RegisterSingleton<ServiceC>().As<IService>();
childRegistry.RegisterSingleton<ServiceD>().As<IService>();
IObjectResolver childResolver = childRegistry.Build();
var rootServices = rootResolver.Resolve<IEnumerable<IService>>(); // ServiceA, ServiceB
Assert.AreEqual(2, rootServices.Count());
var childServices = childResolver.Resolve<IEnumerable<IService>>(); // ServiceC, ServiceD
Assert.AreEqual(2, childServices.Count());
Open Generics
Open generic types can be registered and a closed version of that type can be resolved.
public interface ILogger<T> { }
public class Logger<T> : ILogger<T> { }
registry.RegisterSingleton(typeof(Logger<>));
// ...
var logger = resolver.Resolve<Logger<ServiceA>>();
registry.RegisterSingleton(typeof(Logger<>)).As(typeof(ILogger<>));
// ...
var logger = resolver.Resolve<ILogger<ServiceA>>();
registry.RegisterSingleton(typeof(Logger<>), (resolver, type) => LoggerFactory.Create(type)).As(typeof(ILogger<>));
// ...
var logger = resolver.Resolve<ILogger<ServiceA>>();
Instantiate
You can instantiate types directly via the resolver. This is useful for types that are not registered but need dependencies injected.
public class ServiceB(ServiceA ServiceA) { }
registry.RegisterSingleton<ServiceA>();
// ...
var serviceB = resolver.Instantiate<ServiceB>();
Resolving IObjectResolver
You can resolve the current resolver via IObjectResolver. This is useful for factories or types that need to create child resolvers.
public class Factory(IObjectResolver Resolver) { }
var factory = resolver.Resolve<Factory>();
Assert.AreSame(resolver, factory.Resolver);
Resolver Built Callbacks
You can register a callback to be invoked when a resolver is built. This is useful for any logic that requires a finalised dependency graph.
registry.RegisterBuildCallback(resolver =>
{
var service = resolver.Resolve<IService>();
service.Run();
});
Improvements
- Better error handling: if something goes wrong the exceptions thrown could be more descriptive.
- Circular dependency detection: currently this will just result in a stack overflow.
- Compile / Built time checks: currently everything is resolved at runtime, it would be nice to have some way of checking registrations at compile or build time.
- Deep resolvability checking:
CanResolveonly checks whether a type is registered, not whether its dependencies are constructible, so it can returntruefor a type that throws at construction. A deep check would walk the registration graph transitively (without constructing anything) to verify every constructor parameter is resolvable. This is best-effort only: factory registrations are opaque delegates, open generics can't be validated in the abstract, and constructor parameters supplied as runtime arguments aren't known ahead of time — those nodes would be skipped rather than checked. The natural home for this is an opt-inValidateOnBuild-style pass rather than changingCanResolveitself (which feeds the hot resolution path). - Performance: this probably won't be an issue for us but there are some optimisations that could be made.
| 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 was computed. 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 was computed. 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.
-
net8.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on CallumRose.EasyDI:
| Package | Downloads |
|---|---|
|
CallumRose.EasyDI.LifecycleHooks
Frameworks for setting up application lifecycle hooks using EasyDI |
|
|
CallumRose.EasyDI.Configuration
Configure Microsoft.Extensions.Options from Microsoft.Extensions.Configuration with EasyDI |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.0 | 199 | 8/11/2026 |