Atzonix.DependencyInjection
2.1.0
Prefix Reserved
dotnet add package Atzonix.DependencyInjection --version 2.1.0
NuGet\Install-Package Atzonix.DependencyInjection -Version 2.1.0
<PackageReference Include="Atzonix.DependencyInjection" Version="2.1.0" />
<PackageVersion Include="Atzonix.DependencyInjection" Version="2.1.0" />
<PackageReference Include="Atzonix.DependencyInjection" />
paket add Atzonix.DependencyInjection --version 2.1.0
#r "nuget: Atzonix.DependencyInjection, 2.1.0"
#:package Atzonix.DependencyInjection@2.1.0
#addin nuget:?package=Atzonix.DependencyInjection&version=2.1.0
#tool nuget:?package=Atzonix.DependencyInjection&version=2.1.0
Atzonix.DependencyInjection
A lightweight framework for .NET library authors who need to manage their library's internal dependency injection while giving consumers a clean, structured way to customize or extend it.
Who Is This For?
This library is not for application developers wiring up their own app's DI container. It is for developers who are building a .NET library or framework — such as an ORM, an HTTP client library, or any component that has its own internal services — and want to:
- Manage internal service registrations cleanly
- Let consumers override or extend those registrations
- Cache the built
IServiceProviderso it is not rebuilt on every use - Validate that all required services are registered before first use
The Recommended Design Pattern
Before diving into the API, it is important to understand the design pattern this library is built around, because getting this right is what makes your library feel polished and intuitive to consumers.
Give Your Library a Facade
Your library should expose a single facade class as the main entry point for consumers. Think of DbContext in Entity Framework Core — consumers create an instance of it, and everything else happens internally. They never deal with service registrations directly.
// This is what your consumer writes
var context = new MyDataContext();
var results = context.Customers.ToList();
The consumer should not need to know anything about dependency injection. It all happens behind the scenes inside your facade.
All API Paths Lead to IServiceProvider
Every public property or method on your facade that needs an internal service should resolve it from a single IServiceProvider. This provider is initialized lazily — only when the consumer first uses your API — and then cached for all subsequent calls.
public class DataContext
{
private IServiceProvider _serviceProvider;
private IServiceProvider ServiceProvider
=> _serviceProvider ??= BuildServiceProvider();
// Every API property resolves from the same provider
public IRepository<Customer> Customers
=> ServiceProvider.GetRequiredService<IRepository<Customer>>();
public IRepository<Order> Orders
=> ServiceProvider.GetRequiredService<IRepository<Order>>();
}
This means no matter which property the consumer touches first, the provider is built once and reused everywhere.
Use OnConfiguring for Consumer Customization
Rather than asking consumers to pass configuration through a constructor, expose an OnConfiguring method they can override. This is the same pattern used by Entity Framework Core and feels natural to most .NET developers.
public class MyDataContext : DataContext
{
protected override void OnConfiguring(DataContextConfiguration config)
{
config.UseSqlServer("Server=.;Database=MyDb;Trusted_Connection=True;");
}
}
Installation
dotnet add package Atzonix.DependencyInjection
Or via the NuGet Package Manager in Visual Studio, search for Atzonix.DependencyInjection.
Core Concepts
Before looking at code, here is a quick overview of each type and its role:
| Type | Role |
|---|---|
ServiceBuilderBase |
You subclass this in your library. It defines what services your library needs and registers them. |
ServiceManagerBase |
You subclass this in your library. It builds and caches the IServiceProvider. |
ServiceCharacteristic |
Describes a service — its lifetime (Transient, Scoped, Singleton) and whether multiple registrations are allowed. |
IServiceContextConfiguration |
The configuration object passed to OnConfiguring. Consumers use this to register extensions. |
ServiceContextConfiguration |
The default implementation of IServiceContextConfiguration. Ready to use as-is. |
IServiceContextExtension |
Consumers implement this to register their custom services into your library's container. |
CoreServicesNotInitializedException |
Thrown when a required service was not registered, helping catch mistakes early. |
How It All Works — The Three Phases
Understanding the three phases is the key to understanding this library. It is important to know that calling OnConfiguring does not immediately register any services. Registration is deferred until the IServiceProvider is actually needed.
Phase 1 — Collection (your consumer's code runs)
When the consumer creates your facade, OnConfiguring is called. The consumer calls something like config.UseSqlServer(...) which internally calls config.AddOrUpdateExtension(new SqlServerExtension(...)).
At this point, the SqlServerExtension instance is simply stored inside the configuration object. No services are registered yet. Think of this phase as the consumer writing down their intentions.
new MyDataContext()
└── OnConfiguring(config) called
└── config.UseSqlServer("connection string")
└── config.AddOrUpdateExtension(new SqlServerExtension(...))
└── SqlServerExtension stored internally ← nothing registered yet
Phase 2 — Build (triggered on first API use)
When the consumer accesses any API property for the first time, your facade's IServiceProvider getter fires. Since the provider has not been built yet, it calls ServiceManagerBase.GetOrAdd(config).
This is where the actual service registration happens, in this exact order:
- A fresh
IServiceCollectionis created - Each stored extension's
AddServicesmethod is called — consumer registrations go in first - Your library's
AddCoreServicesruns — library defaults fill in anything the consumer did not override ValidateCoreServicesAddedchecks that all required services are presentIServiceProvideris built and cached
context.Customers ← first API access
└── ServiceProvider getter fires
└── ServiceManagerBase.GetOrAdd(config)
├── SqlServerExtension.AddServices(services) ← consumer services registered first
├── AddCoreServices() ← library defaults registered second
├── ValidateCoreServicesAdded() ← all required services present?
└── serviceCollection.BuildServiceProvider() ← provider built and cached
The reason consumer extensions run before library defaults is intentional — it gives consumers the ability to override any service your library would otherwise register by default.
Phase 3 — Cache (all subsequent API uses)
Every subsequent API call finds the cached IServiceProvider immediately. The build process never runs again for the same configuration.
context.Orders ← second API access
└── ServiceProvider getter fires
└── ServiceManagerBase.GetOrAdd(config)
└── provider found in cache → returned immediately
Implementation Guide — Library Author
This section walks through building a fictional ORM library called DataLib that uses Atzonix.DependencyInjection internally.
Step 1 — Define Your Internal Service Interfaces
These are your library's internal contracts. Consumers will never see most of these — they are implementation details.
// Internal services your ORM needs
public interface IQueryExecutor { ... }
public interface IConnectionFactory { ... }
public interface IChangeTracker { ... }
Step 2 — Implement ServiceBuilderBase
Subclass ServiceBuilderBase to declare what services your library owns and how they should be registered.
public class DataLibServiceBuilder : ServiceBuilderBase
{
// Maps each service type to its characteristics
private static readonly Dictionary<Type, ServiceCharacteristic> _characteristics
= new Dictionary<Type, ServiceCharacteristic>
{
{ typeof(IQueryExecutor), new ServiceCharacteristic(ServiceLifetime.Transient) },
{ typeof(IConnectionFactory), new ServiceCharacteristic(ServiceLifetime.Singleton) },
{ typeof(IChangeTracker), new ServiceCharacteristic(ServiceLifetime.Scoped) },
};
public DataLibServiceBuilder(IServiceCollection serviceCollection)
: base(serviceCollection)
{
}
// Tell the base class the characteristics of each service
protected override ServiceCharacteristic GetServiceCharacteristic(Type serviceType)
{
if (!_characteristics.TryGetValue(serviceType, out var characteristic))
throw new InvalidOperationException(
$"No characteristic defined for '{serviceType.FullName}'.");
return characteristic;
}
// Tell the base class which types must be registered for validation
protected override IReadOnlyCollection<Type> GetCoreServiceTypes()
{
return _characteristics.Keys;
}
// Register your library's default implementations
// These run after consumer extensions, so consumers can override any of these
public override void AddCoreServices()
{
this.TryAdd<IQueryExecutor, DefaultQueryExecutor>();
this.TryAdd<IConnectionFactory, DefaultConnectionFactory>();
this.TryAdd<IChangeTracker, DefaultChangeTracker>();
}
}
Step 3 — Implement ServiceManagerBase
Subclass ServiceManagerBase to connect it to your builder.
public class DataLibServiceManager : ServiceManagerBase
{
protected override ServiceBuilderBase CreateServiceBuilder(IServiceCollection serviceCollection)
{
return new DataLibServiceBuilder(serviceCollection);
}
}
Step 4 — Create Your Configuration Class
This is what gets passed to OnConfiguring. It extends ServiceContextConfiguration so consumers can call AddOrUpdateExtension on it, and you can add your own extension methods on top.
public class DataContextConfiguration : ServiceContextConfiguration
{
// Extension methods like UseSqlServer will be added here
}
Step 5 — Create Your Facade
This is the class consumers will actually use. It wires everything together.
public abstract class DataContext
{
private static readonly DataLibServiceManager _serviceManager = new DataLibServiceManager();
private readonly DataContextConfiguration _config;
private IServiceProvider? _serviceProvider;
// No-arg constructor — chains to the overload with a default config
protected DataContext()
: this(new DataContextConfiguration())
{
}
// Overload for consumers who want to pass a pre-built configuration
protected DataContext(DataContextConfiguration config)
{
_config = config; // just store it — nothing is built yet
}
// Lazy — built once on first API access, cached for all subsequent calls
private IServiceProvider ServiceProvider
{
get
{
if (_serviceProvider == null)
{
OnConfiguring(_config); // Phase 1 — runs once
_serviceProvider = _serviceManager.GetOrAdd(_config); // Phase 2 — build or retrieve from cache
}
return _serviceProvider;
}
}
// Consumers override this to configure the context
protected virtual void OnConfiguring(DataContextConfiguration config) { }
// All API properties resolve from the same provider
public IRepository<T> Set<T>() where T : class
=> ServiceProvider.GetRequiredService<IRepository<T>>();
}
Step 6 — Create Extension Methods for Clean Configuration
This is what makes your library feel polished. Instead of exposing AddOrUpdateExtension directly, wrap it in meaningful extension methods.
public static class DataContextConfigurationExtensions
{
public static DataContextConfiguration UseSqlServer(
this DataContextConfiguration config,
string connectionString)
{
config.AddOrUpdateExtension(new SqlServerExtension(connectionString));
return config;
}
public static DataContextConfiguration UsePostgreSql(
this DataContextConfiguration config,
string connectionString)
{
config.AddOrUpdateExtension(new PostgreSqlExtension(connectionString));
return config;
}
}
Each extension class implements IServiceContextExtension and registers provider-specific services.
Inside AddServices, you have access to both the raw IServiceCollection and a ServiceBuilder instance you can create from it. Understanding which one to use is important:
Use ServiceBuilder for framework services — services that your library's ServiceBuilderBase knows about (i.e. they have a defined ServiceCharacteristic). The builder enforces the correct lifetime automatically, so the extension does not need to know whether a service should be Transient, Scoped, or Singleton.
Use IServiceCollection directly for non-framework services — services that are specific to the extension and are not declared in your library's ServiceBuilderBase. For these, the extension is responsible for specifying the lifetime explicitly.
public class SqlServerExtension : IServiceContextExtension
{
private readonly string _connectionString;
public SqlServerExtension(string connectionString)
{
_connectionString = connectionString;
}
// This is called by ServiceManagerBase during provider build
// This is where actual DI registrations happen
public void AddServices(IServiceCollection services)
{
// Use ServiceBuilder for framework services — lifetime is enforced automatically
var serviceBuilder = new DataLibServiceBuilder(services);
serviceBuilder.TryAdd<IConnectionFactory>(
_ => new SqlServerConnectionFactory(_connectionString));
serviceBuilder.TryAdd<IQueryExecutor, SqlServerQueryExecutor>();
// Use IServiceCollection directly for non-framework services —
// these are extension-specific and ServiceBuilderBase does not know about them,
// so the extension must specify the lifetime explicitly
services.AddSingleton<SqlServerDiagnosticsListener>();
}
}
The key benefit of going through ServiceBuilder for framework services is that if the lifetime of IQueryExecutor ever changes in your library, the change only needs to happen in one place — ServiceBuilderBase. Every extension that uses ServiceBuilder picks it up automatically, without any code changes.
Implementation Guide — Library Consumer
Once your library is built using the pattern above, consuming it is straightforward.
Basic Usage
public class AppDataContext : DataContext
{
protected override void OnConfiguring(DataContextConfiguration config)
{
config.UseSqlServer("Server=.;Database=AppDb;Trusted_Connection=True;");
}
}
// Usage
var context = new AppDataContext();
var customers = context.Set<Customer>().GetAll();
Overriding a Library Service
If a consumer wants to replace one of your library's internal services with their own implementation, they implement IServiceContextExtension directly.
// Consumer's custom implementation
public class MyCustomQueryExecutor : IQueryExecutor
{
// custom implementation
}
// Consumer's extension that registers it
public class MyCustomExtension : IServiceContextExtension
{
public void AddServices(IServiceCollection services)
{
// Use ServiceBuilder to override a framework service — lifetime is handled automatically
// Registers before library defaults, so this takes precedence
var serviceBuilder = new DataLibServiceBuilder(services);
serviceBuilder.TryAdd<IQueryExecutor, MyCustomQueryExecutor>();
}
}
// Wired up in OnConfiguring
public class AppDataContext : DataContext
{
protected override void OnConfiguring(DataContextConfiguration config)
{
config.UseSqlServer("Server=.;Database=AppDb;Trusted_Connection=True;");
config.AddOrUpdateExtension(new MyCustomExtension());
}
}
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 was computed. 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 | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. 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.0
- Microsoft.Bcl.HashCode (>= 6.0.0)
- Microsoft.Extensions.DependencyInjection (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
v2.1.0 — Upgraded Microsoft.Extensions.DependencyInjection and .Abstractions to 10.0.10 and Microsoft.Bcl.HashCode to 6.0.0. No public API changes; the package still targets .NET Standard 2.0.