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
                    
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="Atzonix.DependencyInjection" Version="2.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Atzonix.DependencyInjection" Version="2.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Atzonix.DependencyInjection" />
                    
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 Atzonix.DependencyInjection --version 2.1.0
                    
#r "nuget: Atzonix.DependencyInjection, 2.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 Atzonix.DependencyInjection@2.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=Atzonix.DependencyInjection&version=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Atzonix.DependencyInjection&version=2.1.0
                    
Install as a Cake Tool

Atzonix.DependencyInjection

NuGet Version License: MIT Build Status

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 IServiceProvider so it is not rebuilt on every use
  • Validate that all required services are registered before first use

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:

  1. A fresh IServiceCollection is created
  2. Each stored extension's AddServices method is called — consumer registrations go in first
  3. Your library's AddCoreServices runs — library defaults fill in anything the consumer did not override
  4. ValidateCoreServicesAdded checks that all required services are present
  5. IServiceProvider is 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.0 104 8/5/2026
2.0.0 124 7/2/2026

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.