The PoliNorError.Extensions.DependencyInjection package extends PoliNorError library to provide integration with Microsoft Dependency Injection.
Get up and running in 3 simple steps:
// Program.cs
services.AddPoliNorError(
Assembly.GetExecutingAssembly());This scans your assembly for all IPolicyBuilder<> implementations and wires up IPolicy<T> automatically.
public class SomePolicyBuilder : IPolicyBuilder<SomePolicyBuilder>
{
private readonly ILogger<SomePolicyBuilder> _logger;
public SomePolicyBuilder(ILogger<SomePolicyBuilder> logger)
{
_logger = logger;
}
public IPolicyBase Build()
{
return new RetryPolicy(3)
.WithPolicyName("SomeRetryPolicy")
.WithErrorProcessor(new RetryLoggingErrorProcessor(_logger))
.WithWait(new TimeSpan(0, 0, 3))
.AddPolicyResultHandler(pr =>
{
Log.PolicyFailedToHandleException(
_logger,
pr.UnprocessedError,
pr.PolicyName);
});
}
}Another example:
public class AnotherPolicyBuilder : IPolicyBuilder<AnotherPolicyBuilder>
{
private readonly ILogger<AnotherPolicyBuilder> _logger;
public AnotherPolicyBuilder(ILogger<AnotherPolicyBuilder> logger)
{
_logger = logger;
}
public IPolicyBase Build()
{
return new RetryPolicy(2)
.WithPolicyName("AnotherRetryPolicy")
.WithErrorProcessor(new RetryLoggingErrorProcessor(_logger))
.WithWait(new TimeSpan(0, 0, 1))
.AddPolicyResultHandler(pr =>...);
}
}, where RetryLoggingErrorProcessor:
public class RetryLoggingErrorProcessor : ErrorProcessor
{
private readonly ILogger _logger;
public RetryLoggingErrorProcessor(ILogger logger)
{
_logger = logger;
}
public override void Execute(Exception error,
ProcessingErrorInfo? catchBlockProcessErrorInfo = null,
CancellationToken token = default)
{
_logger.LogError(error,
"An error occurred while doing work on {Attempt} attempt.",
catchBlockProcessErrorInfo.GetAttemptCount());
}
}public class Worker
{
private readonly IPolicy<SomePolicyBuilder> _somePolicy;
private readonly IPolicy<AnotherPolicyBuilder> _anotherPolicy;
public Worker(IPolicy<SomePolicyBuilder> somePolicy,
IPolicy<AnotherPolicyBuilder> anotherPolicy)
{
_somePolicy = somePolicy;
_anotherPolicy = anotherPolicy;
}
public async Task DoWorkAsync(CancellationToken token)
{
await _somePolicy.HandleAsync(MightThrowAsync, token);
await _anotherPolicy.HandleAsync(MightThrowAsync, token);
}
private async Task MightThrowAsync(CancellationToken token)
{
await Task.Delay(100, token);
throw new SomeException("Something went wrong.");
}
}✅ That’s it!
- Builders encapsulate configuration.
- Consumers inject
IPolicy<T>and just use it. - DI takes care of wiring everything together.
-
IPolicyBuilder
- Implemented only in your builders.
- A builder abstraction for creating policies.
- Encapsulates configuration (retry count, wait strategy, error processors, etc.).
- Registered automatically into DI via assembly scanning.
-
IPolicy
- Consumed only in your services.
- A closed generic wrapper that represents a policy built by a specific builder.
- Resolved directly from DI, giving consumers a type-safe handle to the correct policy.
- Internally backed by
ProxyPolicy<T>which delegates to the builder’sBuild()result.
-
Automatic DI Registration
AddPoliNorError()scans assemblies for allIPolicyBuilder<>implementations.- Registers them and wires up
IPolicy<T>→ProxyPolicy<T>automatically.
-
Keyed Services Support
AddKeyedPolicyregisters policies under string keys (builder-based or lambda-based).- Resolved via
[FromKeyedServices]orGetRequiredKeyedService<IPolicy>.
-
Policy Registry
AddPolicyRegistryopt-in registration for resolution viaIPolicyRegistry.GetPolicy<TBuilder>().- Keeps constructors clean when a consumer needs many policies.
- You create builder classes that implement
IPolicyBuilder<TBuilder>. AddPoliNorErrorregisters the open generic mappingIPolicy<> -> ProxyPolicy<>.- When a consumer requests
IPolicy<TBuilder>, DI resolvesProxyPolicy<TBuilder>. - The proxy calls the builder’s
Build()method to produce the actual policy. - All calls (
Handle,HandleAsync, etc.) are delegated to the built policy.
- Type-safe DI: No string keys or manual lookups.
- Separation of concerns: Builders configure, consumers execute.
- Discoverable: Constructor injection makes dependencies explicit.
- Testable: Swap out builders or inject fake policies in tests.
- Extensible: Add new PoliNorError policies by just adding new builders.
Register policies under string keys using the standard .NET keyed services API.
Builder-based overload — reuses the existing IPolicy<TBuilder> pipeline:
services
.AddPoliNorError(Assembly.GetExecutingAssembly())
// The same builder under two keys, with independent lifetimes
.AddKeyedPolicy<SomePolicyBuilder>("retry-primary")
.AddKeyedPolicy<SomePolicyBuilder>("retry-relaxed", ServiceLifetime.Singleton);Lambda-based overload — ad-hoc policy without a builder class:
services.AddKeyedPolicy("orders-policy",
sp => new RetryPolicy(2)
.WithPolicyName("OrdersPolicy")
.WithErrorProcessor(new RetryLoggingErrorProcessor(sp.GetRequiredService<ILogger<Program>>())),
ServiceLifetime.Singleton);Consume keyed policies via [FromKeyedServices] or GetRequiredKeyedService:
public class Worker
{
public Worker([FromKeyedServices("retry-primary")] IPolicy primaryPolicy) { }
public async Task DoWorkAsync(IServiceProvider sp, CancellationToken token)
{
var ordersPolicy = sp.GetRequiredKeyedService<IPolicy>("orders-policy");
// ...
}
}✅ Use when: you need multiple named variants of the same builder, config-driven policy selection, or ad-hoc policies.
Register policies for resolution through IPolicyRegistry — useful when a consumer needs many policies (e.g., a resilience orchestrator) and constructor bloat becomes a problem.
services
.AddPoliNorError(Assembly.GetExecutingAssembly())
.AddPolicyRegistry(registry =>
{
registry.AddPolicy<SomePolicyBuilder>();
registry.AddPolicy<AnotherPolicyBuilder>();
registry.AddPolicy<ThirdPolicyBuilder>();
});Resolve policies by builder type:
public class Worker
{
private readonly IPolicyBase _somePolicy;
private readonly IPolicyBase _anotherPolicy;
private readonly IPolicyBase _thirdPolicy;
public Worker(IPolicyRegistry policyRegistry)
{
_somePolicy = policyRegistry.GetPolicy<SomePolicyBuilder>();
_anotherPolicy = policyRegistry.GetPolicy<AnotherPolicyBuilder>();
_thirdPolicy = policyRegistry.GetPolicy<ThirdPolicyBuilder>();
}
// ...
}✅ Use when: a consumer needs 5+ policies, or you need dynamic policy selection at runtime.
See ADR-0001 for a comparison of all three registration approaches (direct resolution, keyed services, registry).
For more complex scenarios, PoliNorError.Extensions.DependencyInjection supports an advanced pattern that separates policy creation from policy configuration.
PolicyConfigurator<TPolicy>— an abstract base class for encapsulating cross‑cutting configuration logic (logging, enrichment, etc.).PolicyBuilder<TPolicy, TConfigurator, TBuilder>— an abstract base class that encapsulates policy creation and optional configurator wiring.
Create a subclass of PolicyConfigurator<TPolicy> and override the Configure method, where TPolicy is a policy from PoliNorError library.
Inheritors of PolicyConfigurator are automatically resolved from DI.
Create a subclass of PolicyBuilder<TPolicy, TConfigurator, TBuilder> and override the CreatePolicy method, where TPolicy is a policy from PoliNorError library, and TConfigurator inherits from PolicyConfigurator<TPolicy>. TBuilder is the concrete builder type itself (self-referential CRTP parameter).
Use custom builders + configurators when:
- You want policy creation and policy configuration to be separate concerns.
- You want to reuse the same configurator across multiple builders.
- You want to keep your builder classes minimal and declarative.
public class RetryPolicyConfigurator : PolicyConfigurator<RetryPolicy>
{
private readonly ILoggerFactory _loggerFactory;
public RetryPolicyConfigurator(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
}
public override void Configure(RetryPolicy policy)
{
var logger = _loggerFactory.CreateLogger(policy.PolicyName);
policy.WithErrorProcessor(new RetryLoggingErrorProcessor(logger))
.AddPolicyResultHandler(pr =>
{
Log.PolicyFailedToHandleException(
logger,
pr.UnprocessedError,
pr.PolicyName);
});
}
}This configurator:
- Receives dependencies via DI (here:
ILoggerFactory) - Adds a logging error processor to the policy
- Uses the policy name to create a dedicated logger
public class SomePolicyBuilder : PolicyBuilder<RetryPolicy, RetryPolicyConfigurator, SomePolicyBuilder>
{
protected override RetryPolicy CreatePolicy() =>
new RetryPolicy(3, retryDelay: ConstantRetryDelay.Create(new TimeSpan(0, 0, 3)))
.WithPolicyName("SomeRetryPolicy");
}This builder:
- Creates a
RetryPolicywith a fixed delay. - Assigns a policy name (used later by the configurator).
- Delegates configuration to
RetryPolicyConfigurator.
Once created, the configurator (a subclass of PolicyConfigurator) can be shared across multiple builders:
public class AnotherPolicyBuilder : PolicyBuilder<RetryPolicy, RetryPolicyConfigurator, AnotherPolicyBuilder>
{
protected override RetryPolicy CreatePolicy() =>
new RetryPolicy(2, retryDelay: ConstantRetryDelay.Create(new TimeSpan(0, 0, 1)))
.WithPolicyName("AnotherRetryPolicy");
}- Single Responsibility Principle: Each class has one clear responsibility
- Reusability: Configurators can be shared across multiple policy builders
- Testability: Configurators and builders can be tested independently
- Maintainability: Changes to configuration logic don't affect creation logic and vice versa
See samples folder for concrete example.
