This is a minimal implementation of the Microsoft.Extensions.DependencyInjection package, including the ActivatorUtilities class.
This is designed as a very lightweight, and minimalistic, solution, but this may come at a cost. This package does not include many of the safety measures and fallbacks that the full package contains. It was written from the ground up, other than the ActivatorUtilities class. Therefore, it might not play well with third-party IOC wrappers like AutoFac or CastleWindsor. It should not be seen as a replacement for Microsoft.Extensions.DependencyInjection.
The original purpose of this package was to act as an IOC container for game mods, where a full enterprise level package simply comes with too much bloat. This is ideal for those kinds of scenarios, when you just need a quick and simple IOC container, without all the bloat.
Inspired by Nick Chapsas' video tutorial on bespoke dependency injection solutions.
https://www.youtube.com/watch?v=NSVZa4JuTl8
This repository provides a compact, focused implementation of dependency injection primitives suitable for small applications, tools and game mods where the official Microsoft dependency injection package would be overly heavy. It targets .NET 8 and uses modern C# features while keeping the public API surface familiar to users of Microsoft.Extensions.DependencyInjection.
Key components include:
IServiceCollection- a lightweight service collection interface (justIList<ServiceDescriptor>), intended to feel familiar to users ofMicrosoft.Extensions.DependencyInjection.ServiceCollection- a simple list-backed implementation ofIServiceCollectionbacked byList<ServiceDescriptor>.ServiceDescriptor- describes a service registration (service type, implementation, factory and lifetime).ActivatorUtilities- helpers to instantiate types using constructor injection from anIServiceProvider.- Attribute-based registration helpers in the
Annotationnamespace:SingletonAttribute,TransientAttribute, and scanning helpers to register annotated types from assemblies. ServiceCollectionDescriptorExtensions- extension helpers for adding, trying to add and replacing descriptors (e.g.TryAddTransient,TryAddSingleton).ServiceProviderServiceExtensions- helpers to resolve services from anIServiceProvider(GetRequiredService,GetServices,CreateInstance,CreateScope).
This project is intentionally small and dependency-light. It focuses purely on dependency injection primitives (service collection, descriptors, provider utilities) without hosting, configuration, or logging infrastructure. It aims to provide the core features most commonly needed for composition and injection without the additional complexity of the full Microsoft implementation.
- Register transient, scoped and singleton services using descriptors or extension methods.
- Register instances and factories for custom construction logic.
- Mix explicit constructor arguments with container-resolved services using
ActivatorUtilities. - Scan assemblies and register types annotated with
SingletonAttributeorTransientAttribute. - Lightweight, zero-dependency implementation intended for constrained environments (e.g. game mods, tools, scripting hosts).
- Familiar-feeling API surface modelled on
Microsoft.Extensions.DependencyInjection, but with a much smaller implementation.
- This implementation does not include all the diagnostics, fallback behaviour or extension points present in the official Microsoft DI.
- Behaviour may differ from the official DI in edge cases - test carefully before replacing the official DI in production systems.
- There is no guarantee of compatibility with third-party IOC containers or wrappers.
ServiceCollectiononly exposesAddvia theICollection<ServiceDescriptor>interface; most code should use the provided extension methods (Add,TryAdd,TryAddTransient,TryAddScoped,TryAddSingleton, keyed variants, etc.) instead of manipulating the underlying list directly.
The library exposes a simple API surface for registering services and creating instances. The code below demonstrates common tasks.
- Add Services to a
ServiceCollection:
var services = new ApacheTech.Common.DependencyInjection.ServiceCollection();
// Register concrete type as singleton
services.TryAddSingleton<MyService>();
// Register scoped service
services.TryAddScoped<IMyScopedService, MyScopedService>();
// Register service with implementation
services.TryAddTransient(typeof(IMyService), typeof(MyService));
// Register using factory
services.TryAddSingleton(typeof(IMyService), sp => new MyService(sp.GetRequiredService<IOther>()));- Resolve Services via an
IServiceProvider:
This repository provides a minimal set of IServiceProvider extension helpers. How you obtain a runtime service provider instance depends on your composition root or hosting environment. The examples below use the extension helpers for resolution where a provider is available.
IServiceProvider provider = /* obtain provider from your composition root */;
// Resolve required service (throws if missing)
var svc = provider.GetRequiredService<MyService>();
// Resolve optional service
var maybe = provider.GetService<MyOptionalService>();
// Resolve all implementations
IEnumerable<IMyService> all = provider.GetServices<IMyService>();- Create Instances with
ActivatorUtilities:
// Create an instance of a type and let the container satisfy constructor dependencies
var instance = ActivatorUtilities.CreateInstance(provider, typeof(MyType), someExplicitArg);
// Generic helper
var typed = ActivatorUtilities.CreateInstance<MyType>(provider, someExplicitArg);
// Get service or create instance if not registered
var maybeInstance = ActivatorUtilities.GetServiceOrCreateInstance(provider, typeof(MyOtherType));This section documents the public API surface in more detail and includes short examples showing common usages.
ServiceDescriptor represents a registration. It records the service type, the implementation type or instance, an optional factory, and the service lifetime.
Common factory methods and constructors are shown in the table below.
| Method | Description | Example |
|---|---|---|
Transient<TService, TImplementation>() |
Register TImplementation as a transient implementation for TService. |
var d = ServiceDescriptor.Transient<IMyService, MyService>(); |
Transient<TService>(Func<IServiceProvider, TService> factory) |
Register a transient using a generic factory. | var d = ServiceDescriptor.Transient(sp => new MyService()); |
Transient(Type service, Type implementationType) |
Non-generic transient overload. | var d = ServiceDescriptor.Transient(typeof(IMyService), typeof(MyService)); |
Transient(Type service, Func<IServiceProvider, object> implementationFactory) |
Non-generic transient using a factory. | var d = ServiceDescriptor.Transient(typeof(IMyService), sp => new MyService()); |
Scoped<TService, TImplementation>() |
Register TImplementation as a scoped implementation for TService. |
var d = ServiceDescriptor.Scoped<IMyService, MyService>(); |
Scoped(Type service, Type implementationType) |
Non-generic scoped overload. | var d = ServiceDescriptor.Scoped(typeof(IMyService), typeof(MyService)); |
Scoped<TService>(Func<IServiceProvider, TService> factory) |
Register a scoped service with a factory. | var d = ServiceDescriptor.Scoped(sp => new MyService()); |
Singleton<TService, TImplementation>() |
Register TImplementation as a singleton implementation for TService. |
var d = ServiceDescriptor.Singleton<IMyService, MyService>(); |
Singleton(Type service, Type implementationType) |
Non-generic singleton overload. | var d = ServiceDescriptor.Singleton(typeof(IMyService), typeof(MyService)); |
Singleton(Type service, object instance) |
Register an existing instance as a singleton. | var d = ServiceDescriptor.Singleton(typeof(IMyService), instance); |
Singleton<TService>(Func<IServiceProvider, TService> factory) |
Register a singleton with a factory. | var d = ServiceDescriptor.Singleton(sp => new MyService()); |
Describe(Type serviceType, Func<IServiceProvider, object> factory, ServiceLifetime lifetime) |
Create a descriptor with a factory and explicit lifetime (including Scoped). |
var d = ServiceDescriptor.Describe(typeof(IMyService), sp => new MyService(), ServiceLifetime.Scoped); |
ServiceDescriptor also exposes GetImplementationType() which is useful to determine the concrete implementation type for a descriptor when inspecting or composing descriptors.
IServiceCollection is a minimal contract that extends IList<ServiceDescriptor>. ServiceCollection is the list-backed default implementation.
Common collection operations and helpers are shown in the table below.
| Operation | Description | Example |
|---|---|---|
Add(ServiceDescriptor) |
Append a descriptor unconditionally using the provided extension method. | services.Add(ServiceDescriptor.Transient<IMyService, MyService>()); |
Add(IEnumerable<ServiceDescriptor>) |
Append multiple descriptors unconditionally. | services.Add(new[] { d1, d2 }); |
TryAdd(ServiceDescriptor) |
Add a descriptor only if the service type is not already present. | services.TryAdd(ServiceDescriptor.Singleton<IMyService, MyService>()); |
TryAdd(IEnumerable<ServiceDescriptor>) |
Try to add multiple descriptors, skipping those whose service type already exists. | services.TryAdd(new[] { d1, d2 }); |
TryAddTransient(Type service, Type impl) |
Add a transient registration only if the service type is not already registered. | services.TryAddTransient(typeof(IMyService), typeof(MyService)); |
TryAddScoped(Type service, Type impl) |
Add a scoped registration only if the service type is not already registered. | services.TryAddScoped(typeof(IMyService), typeof(MyService)); |
TryAddSingleton(Type service, Type impl) |
Add a singleton registration only if the service type is not already registered. | services.TryAddSingleton(typeof(IMyService), typeof(MyService)); |
Replace(ServiceDescriptor descriptor) |
Remove the first descriptor with a matching ServiceType and add the supplied descriptor. |
services.Replace(ServiceDescriptor.Singleton(typeof(IMyService), new MyService())); |
RemoveAll(Type serviceType) |
Remove every descriptor for the specified service type. | services.RemoveAll(typeof(IMyService)); |
Note: Add is unconditional, while TryAdd is conditional on the absence of a registration for the same ServiceType.
Use TryAdd or TryAddEnumerable when adding descriptors from scanning to ensure idempotent behaviour.
This static class contains convenience helpers for common registration patterns. Key helpers are shown below.
| Helper | Description | Example |
|---|---|---|
AddTransient(this IServiceCollection, Type service, Type implementationType) |
Add a transient registration with specified implementation type. | services.Add(ServiceDescriptor.Transient(typeof(IMyService), typeof(MyService))); |
AddTransient(this IServiceCollection, Type service, Func<IServiceProvider, object> implementationFactory) |
Add a transient registration using a factory. | services.Add(ServiceDescriptor.Transient(typeof(IMyService), sp => new MyService())); |
TryAddTransient<TService, TImplementation>() |
Register a transient mapping with generic types if not already registered. | services.TryAddTransient<IMyService, MyService>(); |
TryAddTransient<TService>(Func<IServiceProvider, TService> implementationFactory) |
Register a transient with a factory. | services.TryAddTransient(sp => new MyService()); |
TryAddTransient(this IServiceCollection, Type service, Func<IServiceProvider, object> implementationFactory) |
Non-generic transient factory overload. | services.TryAddTransient(typeof(IMyService), sp => new MyService()); |
TryAddScoped<TService, TImplementation>() |
Register a scoped mapping with generic types if not already registered. | services.TryAddScoped<IMyService, MyService>(); |
TryAddScoped<TService>(Func<IServiceProvider, TService> implementationFactory) |
Register a scoped service using a factory if not already registered. | services.TryAddScoped(sp => new MyService()); |
TryAddScoped(this IServiceCollection, Type service, Func<IServiceProvider, object> implementationFactory) |
Non-generic scoped factory overload. | services.TryAddScoped(typeof(IMyService), sp => new MyService()); |
TryAddSingleton<TService, TImplementation>() |
Register a singleton mapping with generic types if not already registered. | services.TryAddSingleton<IMyService, MyService>(); |
TryAddSingleton<TService>(TService instance) |
Register an existing instance as a singleton if not already registered. | services.TryAddSingleton<IMyService>(instance); |
TryAddSingleton<TService>(Func<IServiceProvider, TService> implementationFactory) |
Register a singleton using a factory. | services.TryAddSingleton(sp => new MyService()); |
TryAdd(this IServiceCollection, ServiceDescriptor) |
Add a descriptor only if a registration for the same ServiceType does not exist. |
services.TryAdd(ServiceDescriptor.Singleton(...)); |
TryAddEnumerable(ServiceDescriptor) |
Add a descriptor supporting multiple implementations while avoiding duplicate implementation types. | services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IMyService), typeof(MyImpl))); |
TryAddEnumerable(IEnumerable<ServiceDescriptor>) |
Add multiple enumerable descriptors with duplicate implementation checking. | services.TryAddEnumerable(new[] { d1, d2 }); |
Replace(ServiceDescriptor) |
Replace the first matching registration with the provided descriptor. | services.Replace(ServiceDescriptor.Singleton(...)); |
RemoveAll(Type serviceType) |
Remove all registrations for the given service type. | services.RemoveAll(typeof(IMyService)); |
ActivatorUtilities provides helpers for creating instances where some constructor arguments are supplied explicitly and others are resolved from an IServiceProvider.
| Method | Description | Example |
|---|---|---|
CreateInstance(IServiceProvider provider, Type instanceType, params object[] parameters) |
Create and initialise an instance of instanceType, resolving remaining constructor parameters from the provider. |
var obj = ActivatorUtilities.CreateInstance(provider, typeof(MyController), arg1); |
CreateInstance<T>(IServiceProvider provider, params object[] parameters) |
Generic variant of CreateInstance. |
var obj = ActivatorUtilities.CreateInstance<MyController>(provider, arg1); |
CreateFactory(Type instanceType, Type[] argumentTypes) |
Produce an ObjectFactory delegate that can be reused to instantiate many instances efficiently. |
var factory = ActivatorUtilities.CreateFactory(typeof(MyWorker), new[] { typeof(string), typeof(int) }); |
GetServiceOrCreateInstance(IServiceProvider provider, Type type) |
Resolve a registered service or create an instance if not registered. | var svc = ActivatorUtilities.GetServiceOrCreateInstance(provider, typeof(MyService)); |
If a class has a constructor marked with ActivatorUtilitiesConstructor then that constructor is preferred when selecting which constructor to use.
The library provides a small set of attributes to support declarative registration. The table below summarises the attributes and how to discover annotated types.
| Attribute | Purpose | Example |
|---|---|---|
SingletonAttribute(Type? serviceType = null) |
Mark a class to be registered as a singleton. Optionally specify the service interface type. | [Singleton(typeof(IMyService))] public class MyService : IMyService { } |
TransientAttribute(Type? serviceType = null) |
Mark a class to be registered as transient. Optionally specify the service interface type. | [Transient] public class OtherService { } |
ActivatorUtilitiesConstructor |
Mark the constructor to prefer when creating instances with ActivatorUtilities. |
public MyType([ActivatorUtilitiesConstructor] MyType(...)) { } |
To register annotated types, use the AddAnnotatedServicesFromAssembly extension methods on IServiceCollection.
| Method | Description | Example |
|---|---|---|
AddAnnotatedServicesFromAssembly(this IServiceCollection services, Assembly assembly) |
Scan the supplied assembly for types annotated with ServiceAttribute derivatives and add descriptors. |
services.AddAnnotatedServicesFromAssembly(typeof(MyService).Assembly); |
AddAnnotatedServicesFromAssembly(this IServiceCollection services) |
Scan the calling assembly. | services.AddAnnotatedServicesFromAssembly(); |
AddAnnotatedServicesFromAssembly(this IServiceCollection services, params Type[] assemblyMarkers) |
Scan the assemblies that contain the supplied marker types. | services.AddAnnotatedServicesFromAssembly(typeof(MyService), typeof(OtherMarker)); |
[Singleton(typeof(IMyService))]
public class MyService : IMyService { }
[Transient]
public class OtherService { }Register annotated types from an assembly:
var services = new ApacheTech.Common.DependencyInjection.ServiceCollection();
services.AddAnnotatedServicesFromAssembly(typeof(MyService));This will create ServiceDescriptor instances based on the attributes and add them to the collection.
In addition to standard (unkeyed) registrations, the library supports keyed services. A keyed service is a registration where the same service type can have multiple implementations distinguished by an arbitrary key (for example a string name, enum, or other value).
Keyed services are built around IKeyedServiceProvider, which extends IServiceProvider with:
object? GetService(Type serviceType, object? serviceKey = null)
The default service provider implementation created by this library implements IKeyedServiceProvider, so you can both:
- Resolve unkeyed services via the usual
IServiceProviderAPIs. - Resolve keyed services via
GetService(type, key)or the typed helpersGetKeyedService<T>(key).
Keyed registrations mirror the standard Add* helpers, with an additional serviceKey parameter:
AddKeyedTransient<TService, TImplementation>(object serviceKey)AddKeyedScoped<TService, TImplementation>(object serviceKey)AddKeyedSingleton<TService, TImplementation>(object serviceKey)- Non-generic overloads that accept
Type serviceType,Type implementationTypeand/or factories.
Example:
var services = new ServiceCollection();
// Two different implementations of the same service type, distinguished by key
services.AddKeyedTransient<IMyService, MyServiceA>("a");
services.AddKeyedTransient<IMyService, MyServiceB>("b");
// Keyed singletons cached per key
services.AddKeyedSingleton<IMyService, MyServiceA>("primary");
services.AddKeyedSingleton<IMyService, MyServiceA>("secondary");Once registered, you can resolve keyed services either by using IKeyedServiceProvider directly or the typed helpers exposed by this library:
var provider = services.BuildServiceProvider();
// Using the extended GetService(Type, object) API
var a = ((IKeyedServiceProvider)provider).GetService(typeof(IMyService), "a");
// Or with typed helpers (from the extensions package)
var primary = provider.GetKeyedService<IMyService>("primary");
var secondary = provider.GetKeyedService<IMyService>("secondary");Keyed and unkeyed services are kept logically separate:
- Unkeyed resolution (
GetService(typeof(IMyService)),GetRequiredService<IMyService>(),GetServices<IMyService>()) does not see keyed registrations. - Keyed resolution only returns services matching the requested key.
IEnumerable<T>resolved via unkeyed APIs only contains unkeyed registrations; keyed collections may be resolved withGetKeyedService<IEnumerable<T>>(key).
This separation makes it straightforward to mix standard and keyed registrations without accidental cross-talk.
The project includes a small set of IServiceProvider extension helpers in ServiceProviderServiceExtensions to make consuming services easier.
| Helper | Description | Example |
|---|---|---|
CreateInstance(this IServiceProvider provider, Type serviceType, params object[] args) |
Wrapper around ActivatorUtilities.CreateInstance. |
var obj = provider.CreateInstance(typeof(MyType), someArg); |
CreateInstance<T>(this IServiceProvider provider, params object[] args) |
Generic wrapper around CreateInstance. |
var obj = provider.CreateInstance<MyType>(someArg); |
Resolve<T>(this IServiceProvider provider) |
Alias for GetRequiredService<T>(). |
var s = provider.Resolve<MyService>(); |
GetRequiredService<T>(this IServiceProvider provider) |
Attempt to get a service and throw InvalidOperationException if not present. |
var s = provider.GetRequiredService<MyService>(); |
GetServices<T>(this IServiceProvider provider) |
Attempt to resolve IEnumerable<T>; throws if none are registered (consistent with GetRequiredService). |
var all = provider.GetServices<IMyService>(); |
GetServices(this IServiceProvider provider, Type serviceType) |
Resolve an IEnumerable<serviceType> and return it as IEnumerable<object?>. |
var all = provider.GetServices(typeof(IMyService)); |
CreateScope(this IServiceProvider provider) |
Create a new IServiceScope from an IServiceScopeFactory resolved from the provider. |
using var scope = provider.CreateScope(); |