Table of Contents

Ploch.Common.DependencyInjection

Modular service registration pattern for Microsoft.Extensions.DependencyInjection.

Overview

Ploch.Common.DependencyInjection introduces the ServicesBundle pattern: a structured way to organise DI registrations into cohesive, reusable units. The concept is directly analogous to Autofac Modules, applied to the standard Microsoft.Extensions.DependencyInjection container.

Each bundle encapsulates all the IServiceCollection registrations for a feature or layer. Bundles can declare ordered dependencies on other bundles, ensuring that foundational registrations always run before dependent ones — without the calling code needing to know the order.

The library also provides ScopedService<T>, a small utility that resolves a service inside its own dedicated IServiceScope and disposes the scope alongside itself.

Installation

dotnet add package Ploch.Common.DependencyInjection

The package targets netstandard2.0 and is compatible with .NET Framework 4.6.1+, .NET Core 2.0+, and all modern .NET versions.

Key Types

Type Kind Description
IServicesBundle Interface Core contract: Configure(IServiceCollection services). Also exposes an optional Configuration property (IConfiguration?) for bundles that need access to application configuration.
ServicesBundle Abstract class Base implementation; subclasses override DoConfigure()
ConfigurableServicesBundle Abstract class Variant that requires IConfiguration; subclasses override Configure(IConfiguration)
DelegatingServicesBundle Concrete class Ad-hoc bundle built from delegate actions rather than a subclass. Warning: inherits from ConfigurableServicesBundle, so omitting IConfiguration when registering will throw InvalidOperationException at runtime.
ServicesBundleRegistration Static class Extension methods: AddServicesBundle(...) on IServiceCollection
IConfigurationConsumer Interface Write-only Configuration setter; used internally when injecting config
IOptionalConfigurationProvider Interface Read-only Configuration getter; implemented by IServicesBundle
IScopedService / IScopedService<T> Interfaces Scoped service wrapper contract
ScopedService / ScopedService<T> Concrete classes Creates and owns a scope; disposes it on Dispose() / DisposeAsync()

Usage Examples

Basic bundle

Define a bundle by inheriting ServicesBundle and implementing DoConfigure():

public class LoggingBundle : ServicesBundle
{
    public override void DoConfigure()
    {
        Services.AddSingleton<ILoggerFactory, LoggerFactory>();
        Services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
    }
}

Register it during application startup:

services.AddServicesBundle<LoggingBundle>();
// or with an instance
services.AddServicesBundle(new LoggingBundle());

Bundle with dependencies

Declare dependencies so they are configured before the current bundle runs:

public class ApplicationBundle : ServicesBundle
{
    protected override IEnumerable<IServicesBundle>? Dependencies { get; } =
        [new LoggingBundle(), new DatabaseBundle()];

    public override void DoConfigure()
    {
        Services.AddScoped<IApplicationService, ApplicationService>();
    }
}

Configuration-aware bundle

Inherit ConfigurableServicesBundle when registration logic depends on IConfiguration:

public class DatabaseBundle : ConfigurableServicesBundle
{
    protected override void Configure(IConfiguration configuration)
    {
        var connectionString = configuration.GetConnectionString("DefaultConnection");
        Services.AddDbContext<AppDbContext>(opt => opt.UseSqlite(connectionString));
    }
}

Pass configuration when registering:

services.AddServicesBundle<DatabaseBundle>(configuration);
// or
services.AddServicesBundle(new DatabaseBundle(), configuration);

Inline delegate bundle

Use DelegatingServicesBundle for ad-hoc registration without creating a subclass:

var bundle = new DelegatingServicesBundle()
    .Configure((services, config) => services.AddScoped<IMyService, MyService>())
    .Configure((services, config) =>
    {
        if (config?["Feature:Enabled"] == "true")
            services.AddSingleton<IFeatureService, FeatureService>();
    });

services.AddServicesBundle(bundle, configuration);

Scoped service wrapper

Use ScopedService<T> when a singleton needs to resolve a scoped dependency on demand:

public class BackgroundProcessor(IServiceProvider provider)
{
    public async Task ProcessAsync(CancellationToken ct)
    {
        await using var scoped = new ScopedService<IWorkItemRepository>(provider);
        var items = await scoped.Service.GetPendingAsync(ct);
        // ...
    }
}

Configuration

No additional configuration is required. The two AddServicesBundle extension methods on IServiceCollection cover both common call sites:

// By type (requires a public parameterless constructor)
services.AddServicesBundle<TBundle>(configuration);

// By instance
services.AddServicesBundle(bundleInstance, configuration);

Both overloads return IServiceCollection for fluent chaining.