Skip to content

Decorators ​

SourceGen.Ioc supports the decorator pattern for service registration.

Basic Decorator ​

csharp
public interface IHandler
{
    void Handle();
}

// Decorator implementation: implements the service type and receives the inner instance
public class LoggingDecorator(IHandler inner, ILogger logger) : IHandler
{
    public void Handle()
    {
        logger.Log("Before");
        inner.Handle();
        logger.Log("After");
    }
}

// Register with decorator
[IocRegister<IHandler>(Decorators = [typeof(LoggingDecorator)])]
internal class MyHandler : IHandler
{
    public void Handle() => Console.WriteLine("Handling");
}
Generated Code
csharp
// <auto-generated/>
services.AddTransient<global::MyNamespace.MyHandler, global::MyNamespace.MyHandler>();
services.AddKeyedTransient<global::MyNamespace.IHandler>("SourceGen.Ioc.Decorated:MyNamespace.IHandler|MyNamespace.MyHandler", (global::System.IServiceProvider sp, object? key) =>
{
    var s0 = sp.GetRequiredService<global::MyNamespace.MyHandler>();
    var s1_p1 = sp.GetRequiredService<global::MyNamespace.ILogger>();
    var s1 = new global::MyNamespace.LoggingDecorator(s0, s1_p1);
    return s1;
});
services.AddTransient<global::MyNamespace.IHandler>((global::System.IServiceProvider sp) => sp.GetRequiredKeyedService<global::MyNamespace.IHandler>("SourceGen.Ioc.Decorated:MyNamespace.IHandler|MyNamespace.MyHandler"));

NOTE

Resolving the implementation type (MyHandler) returns the undecorated instance; only the service type (IHandler) is decorated. For singleton and scoped services, the decorators wrap that same instance. The generated container behaves the same way.

A decorator is only applied to service types it implements. An open generic decorator is closed from the decorated service type (see Type Constraint Validation), so a generic decorator such as LoggingDecorator<T> : IHandler cannot be closed for the non-generic IHandler and is skipped.

Which Parameter Receives the Inner Instance ​

The inner instance is passed to the first constructor parameter whose type is the decorated service type, at any position. All other parameters are resolved from the container:

csharp
// inner receives the decorated instance; logger is resolved
public class LoggingDecorator(ILogger logger, IHandler inner) : IHandler;

// Only inner receives the decorated instance; fallback is resolved as a keyed IHandler (SGIOC034)
public class FallbackDecorator(IHandler inner, [FromKeyedServices("fallback")] IHandler fallback) : IHandler;

A decorator without a parameter of the service type is not applied to that service type, and the analyzer reports SGIOC035.

Multiple Decorators ​

Decorators are applied in order (first = outermost):

csharp
[IocRegister<IHandler>(
    Decorators = [typeof(LoggingDecorator), typeof(CachingDecorator)])]
internal class MyHandler : IHandler;

// Execution order: Logging -> Caching -> MyHandler
Generated Code
csharp
// <auto-generated/>
services.AddTransient<global::MyNamespace.MyHandler, global::MyNamespace.MyHandler>();
services.AddKeyedTransient<global::MyNamespace.IHandler>("SourceGen.Ioc.Decorated:MyNamespace.IHandler|MyNamespace.MyHandler", (global::System.IServiceProvider sp, object? key) =>
{
    var s0 = sp.GetRequiredService<global::MyNamespace.MyHandler>();
    var s1 = new global::MyNamespace.CachingDecorator(s0);
    var s2_p1 = sp.GetRequiredService<global::MyNamespace.ILogger>();
    var s2 = new global::MyNamespace.LoggingDecorator(s1, s2_p1);
    return s2;
});
services.AddTransient<global::MyNamespace.IHandler>((global::System.IServiceProvider sp) => sp.GetRequiredKeyedService<global::MyNamespace.IHandler>("SourceGen.Ioc.Decorated:MyNamespace.IHandler|MyNamespace.MyHandler"));

Default Decorators ​

Apply decorators to all implementations of an interface:

csharp
[IocRegisterDefaults<IHandler>(
    ServiceLifetime.Transient,
    Decorators = [typeof(LoggingDecorator), typeof(MetricsDecorator)])]
public interface IHandler;

// All handlers get logging and metrics decorators
[IocRegister]
internal class Handler1 : IHandler;

[IocRegister]
internal class Handler2 : IHandler;
Generated Code
csharp
// <auto-generated/>
services.AddTransient<global::MyNamespace.Handler1, global::MyNamespace.Handler1>();
services.AddKeyedTransient<global::MyNamespace.IHandler>("SourceGen.Ioc.Decorated:MyNamespace.IHandler|MyNamespace.Handler1", (global::System.IServiceProvider sp, object? key) =>
{
    var s0 = sp.GetRequiredService<global::MyNamespace.Handler1>();
    var s1 = new global::MyNamespace.MetricsDecorator(s0);
    var s2_p1 = sp.GetRequiredService<global::MyNamespace.ILogger>();
    var s2 = new global::MyNamespace.LoggingDecorator(s1, s2_p1);
    return s2;
});
services.AddTransient<global::MyNamespace.IHandler>((global::System.IServiceProvider sp) => sp.GetRequiredKeyedService<global::MyNamespace.IHandler>("SourceGen.Ioc.Decorated:MyNamespace.IHandler|MyNamespace.Handler1"));
services.AddTransient<global::MyNamespace.Handler2, global::MyNamespace.Handler2>();
services.AddKeyedTransient<global::MyNamespace.IHandler>("SourceGen.Ioc.Decorated:MyNamespace.IHandler|MyNamespace.Handler2", (global::System.IServiceProvider sp, object? key) =>
{
    var s0 = sp.GetRequiredService<global::MyNamespace.Handler2>();
    var s1 = new global::MyNamespace.MetricsDecorator(s0);
    var s2_p1 = sp.GetRequiredService<global::MyNamespace.ILogger>();
    var s2 = new global::MyNamespace.LoggingDecorator(s1, s2_p1);
    return s2;
});
services.AddTransient<global::MyNamespace.IHandler>((global::System.IServiceProvider sp) => sp.GetRequiredKeyedService<global::MyNamespace.IHandler>("SourceGen.Ioc.Decorated:MyNamespace.IHandler|MyNamespace.Handler2"));

Override Default Decorators ​

csharp
// Override decorators for specific handler
[IocRegister(Decorators = [typeof(CustomDecorator)])]
internal class SpecialHandler : IHandler;

// Or disable decorators
[IocRegister(Decorators = [])]
internal class NoDecoratorsHandler : IHandler;
Generated Code
csharp
// <auto-generated/>
// SpecialHandler uses CustomDecorator instead of default decorators
services.AddTransient<global::MyNamespace.SpecialHandler, global::MyNamespace.SpecialHandler>();
services.AddKeyedTransient<global::MyNamespace.IHandler>("SourceGen.Ioc.Decorated:MyNamespace.IHandler|MyNamespace.SpecialHandler", (global::System.IServiceProvider sp, object? key) =>
{
    var s0 = sp.GetRequiredService<global::MyNamespace.SpecialHandler>();
    var s1 = new global::MyNamespace.CustomDecorator(s0);
    return s1;
});
services.AddTransient<global::MyNamespace.IHandler>((global::System.IServiceProvider sp) => sp.GetRequiredKeyedService<global::MyNamespace.IHandler>("SourceGen.Ioc.Decorated:MyNamespace.IHandler|MyNamespace.SpecialHandler"));

// NoDecoratorsHandler has no decorators
services.AddTransient<global::MyNamespace.NoDecoratorsHandler, global::MyNamespace.NoDecoratorsHandler>();
services.AddTransient<global::MyNamespace.IHandler>((global::System.IServiceProvider sp) => sp.GetRequiredService<global::MyNamespace.NoDecoratorsHandler>());

Async Decorators ​

A decorator can initialize itself asynchronously with an [IocInject] method that returns Task, just like a service (see Async Method Injection; requires the AsyncMethodInject feature).

csharp
public class WarmupDecorator(IHandler inner) : IHandler
{
    [IocInject]
    public async Task WarmupAsync(ICache cache) => await cache.LoadAsync();

    public void Handle() => inner.Handle();
}

[IocRegister<IHandler>(ServiceLifetime.Singleton, Decorators = [typeof(LoggingDecorator), typeof(WarmupDecorator)])]
internal class MyHandler : IHandler
{
    public void Handle() => Console.WriteLine("Handling");
}
Generated Code
csharp
// <auto-generated/>
services.AddSingleton<global::MyNamespace.MyHandler, global::MyNamespace.MyHandler>();
services.AddSingleton<global::System.Threading.Tasks.Task<global::MyNamespace.IHandler>>((global::System.IServiceProvider sp) =>
{
    async global::System.Threading.Tasks.Task<global::MyNamespace.IHandler> Init()
    {
        var s0 = sp.GetRequiredService<global::MyNamespace.MyHandler>();
        var s1_m0 = sp.GetRequiredService<global::MyNamespace.ICache>();
        var s1 = new global::MyNamespace.WarmupDecorator(s0);
        await s1.WarmupAsync(s1_m0);
        var s2_p0 = sp.GetRequiredService<global::MyNamespace.ILogger>();
        var s2 = new global::MyNamespace.LoggingDecorator(s1, s2_p0);
        return s2;
    }
    return Init();
});

The decorated service type becomes async-init: it is registered as Task<IHandler> only, so consumers inject Task<IHandler> and await it (requesting IHandler, Lazy<IHandler> or Func<..., IHandler> reports SGIOC030). Decorators are applied from the innermost one, and each decorator's async methods are awaited before the next decorator wraps it.

NOTE

Only the decorated service types change. The implementation type (MyHandler) and service types the decorator does not implement keep their synchronous registrations. A Func<..., IHandler> with input parameters cannot initialize an async-init service, so it is not generated for it.

Type Constraint Validation ​

The source generator automatically validates decorator type constraints at compile time. Decorators that don't satisfy the type constraints for a specific implementation will be skipped.

csharp
public interface IQuery;
public interface ICommand;

// Base decorator - applies to all request handlers
public class LoggingDecorator<TRequest, TResponse>(IRequestHandler<TRequest, TResponse> inner)
    : IRequestHandler<TRequest, TResponse>;

// Query-only decorator - only applies when TRequest implements IQuery
public class QueryCachingDecorator<TRequest, TResponse>(IRequestHandler<TRequest, TResponse> inner)
    : IRequestHandler<TRequest, TResponse>
    where TRequest : IQuery;

// Define defaults with both decorators
[IocRegisterDefaults(typeof(IRequestHandler<,>),
    ServiceLifetime.Scoped,
    Decorators = [typeof(LoggingDecorator<,>), typeof(QueryCachingDecorator<,>)])]
public interface IRequestHandler<TRequest, TResponse>;

// Command handler - only gets LoggingDecorator (QueryCachingDecorator constraint not satisfied)
public record MyCommand : ICommand;

[IocRegister]
internal class MyCommandHandler : IRequestHandler<MyCommand, bool>;

// Query handler - gets both LoggingDecorator and QueryCachingDecorator
public record MyQuery : IQuery;

[IocRegister]
internal class MyQueryHandler : IRequestHandler<MyQuery, string>;
Generated Code
csharp
// <auto-generated/>
// MyCommandHandler: QueryCachingDecorator skipped (MyCommand doesn't implement IQuery)
services.AddScoped<global::MyNamespace.MyCommandHandler, global::MyNamespace.MyCommandHandler>();
services.AddKeyedScoped<global::MyNamespace.IRequestHandler<global::MyNamespace.MyCommand, bool>>("SourceGen.Ioc.Decorated:MyNamespace.IRequestHandler<MyNamespace.MyCommand, bool>|MyNamespace.MyCommandHandler", (global::System.IServiceProvider sp, object? key) =>
{
    var s0 = sp.GetRequiredService<global::MyNamespace.MyCommandHandler>();
    var s1 = new global::MyNamespace.LoggingDecorator<global::MyNamespace.MyCommand, bool>(s0);
    return s1;
});
services.AddScoped<global::MyNamespace.IRequestHandler<global::MyNamespace.MyCommand, bool>>((global::System.IServiceProvider sp) => sp.GetRequiredKeyedService<global::MyNamespace.IRequestHandler<global::MyNamespace.MyCommand, bool>>("SourceGen.Ioc.Decorated:MyNamespace.IRequestHandler<MyNamespace.MyCommand, bool>|MyNamespace.MyCommandHandler"));

// MyQueryHandler: Both decorators applied (MyQuery implements IQuery)
services.AddScoped<global::MyNamespace.MyQueryHandler, global::MyNamespace.MyQueryHandler>();
services.AddKeyedScoped<global::MyNamespace.IRequestHandler<global::MyNamespace.MyQuery, string>>("SourceGen.Ioc.Decorated:MyNamespace.IRequestHandler<MyNamespace.MyQuery, string>|MyNamespace.MyQueryHandler", (global::System.IServiceProvider sp, object? key) =>
{
    var s0 = sp.GetRequiredService<global::MyNamespace.MyQueryHandler>();
    var s1 = new global::MyNamespace.QueryCachingDecorator<global::MyNamespace.MyQuery, string>(s0);
    var s2 = new global::MyNamespace.LoggingDecorator<global::MyNamespace.MyQuery, string>(s1);
    return s2;
});
services.AddScoped<global::MyNamespace.IRequestHandler<global::MyNamespace.MyQuery, string>>((global::System.IServiceProvider sp) => sp.GetRequiredKeyedService<global::MyNamespace.IRequestHandler<global::MyNamespace.MyQuery, string>>("SourceGen.Ioc.Decorated:MyNamespace.IRequestHandler<MyNamespace.MyQuery, string>|MyNamespace.MyQueryHandler"));

Diagnostics ​

IDSeverityDescription
SGIOC034WarningMore than one decorator constructor parameter is of the decorated service type. Only the first receives the inner instance; the others are resolved from the container.
SGIOC035ErrorA decorator implements a decorated service type but has no constructor parameter of that type, so it is not applied to it.

← Back to Overview

Released under the MIT License.