CallumRose.EasyDI 1.1.0

dotnet add package CallumRose.EasyDI --version 1.1.0
                    
NuGet\Install-Package CallumRose.EasyDI -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CallumRose.EasyDI" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CallumRose.EasyDI" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="CallumRose.EasyDI" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CallumRose.EasyDI --version 1.1.0
                    
#r "nuget: CallumRose.EasyDI, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CallumRose.EasyDI@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CallumRose.EasyDI&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=CallumRose.EasyDI&version=1.1.0
                    
Install as a Cake Tool

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.sh and 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> or IEnumerable<T> (which you shouldn't but) you'll have to resolve that explicitly using resolver.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: CanResolve only checks whether a type is registered, not whether its dependencies are constructible, so it can return true for 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-in ValidateOnBuild-style pass rather than changing CanResolve itself (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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .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