RM-01 to RM-12 — Detailed registration model notes
This page explains each registration-model item with:
- what it solves
- when to use it
- where it usually appears in real projects
- a concrete example
If you are evaluating GenDI for production adoption, this is the reference page for real-world usage decisions.
Quick decision map
| Need | Use |
|---|---|
| Optional/non-critical dependency | RM-01 [InjectOptional] |
| Different implementations per environment | RM-02 [ConditionalInjectable] |
| Wrap service behavior (logging, caching, retry) | RM-03 [DecoratorFor<T>] |
| Define contract lifetime fallback once | RM-04 [ServiceInjection(...)] |
| Auto-discover implementation from dependency graph | RM-05 / RM-06 [Inject] indirect registration |
| Thread-aware reuse policy | RM-07 ThreadIsolation |
| Discover services in referenced projects | RM-08 cross-assembly scanning |
| Infer closed generic from open implementation | RM-09 closed-generic inference |
Control registration policy (Single/Multiple, Add/TryAdd) | RG-01 / RG-02 registration strategy |
Bind config to IOptions<T> automatically with optional key fallback | RM-10 [OptionConfig] + OP evolution |
| Centralized service creation logic | RM-11 [InjectableFactory<T>] |
| Load only selected bounded contexts | RM-12 modules |
RM-01 — [InjectOptional]
What it solves
Some dependencies are optional (observability adapters, secondary integrations, feature plugins). You want "use if available" behavior without throwing.
When to use
- plugin-style extensions
- telemetry/audit sinks
- non-critical integrations
Example
[Injectable]
public sealed class OrderPublisher
{
[Inject] public required IMessageBus Bus { get; init; }
[InjectOptional]
public required IAuditSink? Audit { get; init; }
public async Task PublishAsync(Order order, CancellationToken ct)
{
await Bus.PublishAsync(order, ct);
await (Audit?.WriteAsync($"Order {order.Id} published") ?? Task.CompletedTask);
}
}
RM-02 — ConditionalInjectable(environmentName)
What it solves
A single codebase often needs different implementations by environment (Dev, Staging, Production).
When to use
- fake gateway in development
- production-only external adapter
- sandbox vs live integrations
Example
[Injectable<IPaymentGateway>(ServiceLifetime.Scoped)]
[ConditionalInjectable("Development")]
public sealed class FakePaymentGateway : IPaymentGateway
{
public Task ChargeAsync(Guid orderId, CancellationToken ct = default) => Task.CompletedTask;
}
[Injectable<IPaymentGateway>(ServiceLifetime.Scoped)]
[ConditionalInjectable("Production")]
public sealed class StripePaymentGateway : IPaymentGateway
{
public Task ChargeAsync(Guid orderId, CancellationToken ct = default) => Task.CompletedTask;
}
RM-03 — DecoratorFor<TService> / DecoratorFor(Order = ...)
What it solves
Cross-cutting behavior should not pollute core service logic.
When to use
- logging
- metrics
- retry/circuit-breaker wrappers
- authorization checks around an existing service
Example
[ServiceInjection]
public interface IInventoryService
{
Task ReserveAsync(Guid orderId, CancellationToken ct = default);
}
[Injectable(ServiceLifetime.Scoped)]
public sealed class InventoryService : IInventoryService
{
public Task ReserveAsync(Guid orderId, CancellationToken ct = default) => Task.CompletedTask;
}
[DecoratorFor<IInventoryService>(Order = 0)]
public sealed class InventoryLoggingDecorator(
IInventoryService inner,
ILogger<InventoryLoggingDecorator> logger) : IInventoryService
{
public async Task ReserveAsync(Guid orderId, CancellationToken ct = default)
{
logger.LogInformation("Reserve start {OrderId}", orderId);
await inner.ReserveAsync(orderId, ct);
logger.LogInformation("Reserve end {OrderId}", orderId);
}
}
[DecoratorFor(Order = 1)]
public sealed class InventoryValidationDecorator : IInventoryService
{
[Inject] public required IInventoryService Inner { get; init; }
public Task ReserveAsync(Guid orderId, CancellationToken ct = default) =>
Inner.ReserveAsync(orderId, ct);
}
Decorator pipelines are generated statically in ascending Order; ties fall back to ordinal
decorator type-name ordering. The non-generic attribute resolves exactly one
[ServiceInjection] contract from the decorator inheritance/implementation chain. Every decorator
must expose a public constructor parameter or [Inject] init-only property matching that
contract, otherwise the analyzer fails the build.
To avoid misconfiguration, AddGenDIServices() must be called after all other service registrations in the composition root. This ensures the decorator chain is fully discoverable and correctly ordered in generated output.
So, ensure you are calling AddGenDIServices() at the end of your service registration block, before building the service provider, like this:
// Host (any type of service)
var builder = Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
{
// other registrations here
services.AddGenDIServices();
});
// Web
var builder = WebApplication.CreateBuilder(args);
// other registrations here
builder.Services.AddGenDIServices();
var app = builder.Build();
// WebAssembly
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// other registrations here
builder.Services.AddGenDIServices();
var app = builder.Build();
// Manual ServiceCollection
var services = new ServiceCollection();
// other registrations here
services.AddGenDIServices();
var provider = services.BuildServiceProvider();
RM-04 — ServiceInjection lifetime fallback
What it solves
Large systems with many implementations of the same contract can become inconsistent in lifetime choices.
When to use
- define team-wide default lifetime at contract level
- keep implementations concise
Example
[ServiceInjection(ServiceLifetime.Scoped)]
public interface IOrderRepository { }
[Injectable]
public sealed class SqlOrderRepository : IOrderRepository { }
Fallback precedence remains: Injectable > ServiceInjection > Transient.
RM-05 — indirect registration from [Inject]
What it solves
Sometimes you consume a contract, but the implementation itself is not explicitly annotated. GenDI can infer and register it through usage.
When to use
- migration phases from manual DI
- internal implementations where direct annotation is not practical
Example
public interface ITokenSigner { }
public sealed class JwtTokenSigner : ITokenSigner { }
[Injectable]
public sealed class AuthService
{
[Inject]
public required ITokenSigner TokenSigner { get; init; }
}
RM-06 — lifetime override in [Inject]
What it solves
Indirect discovery can require explicit lifetime control at dependency request site.
When to use
- same contract consumed in different composition needs
- override inferred/default lifetime for specific graph semantics
Example
[Injectable]
public sealed class ReportService
{
[Inject(ServiceLifetime.Scoped)]
public required IReportFormatter Formatter { get; init; }
}
Precedence: Inject > Injectable > ServiceInjection > Transient.
Tie-break: Scoped > Singleton > Transient.
RM-07 — thread-isolation policy
What it solves
Some scenarios require thread-local reuse behavior to avoid sharing state unexpectedly.
When to use
- thread-affine resources
- context-sensitive caches
- specialized parallel workloads
Example
[Injectable<IExecutionContext>(
ServiceLifetime.Singleton,
ThreadIsolation = ThreadIsolationPolicy.Scoped)]
public sealed class ExecutionContext : IExecutionContext { }
RM-08 — scanning across referenced libraries
What it solves
Monoliths and modular solutions often place implementations in separate projects.
When to use
- layered architecture (
Api -> Application -> Infrastructure) - shared business modules referenced by multiple hosts
Typical layout
MyApireferencesMyCompany.OrdersMyCompany.Orderscontains[Injectable]servicesMyApistill uses onlyservices.AddGenDIServices()
This keeps composition root clean while preserving compile-time generation.
RM-09 — closed-generic indirect inference
What it solves
Closed generic dependencies are frequent (IRepository<Order>). GenDI can infer them from open generic implementation patterns when the mapping is unambiguous.
When to use
- repository/specification patterns
- generic handlers and adapters
Example
public interface IRepository<T> { }
public sealed class EfRepository<T> : IRepository<T> { }
[Injectable]
public sealed class OrderReadService
{
[Inject]
public required IRepository<Order> Orders { get; init; }
}
GenDI infers IRepository<Order> -> EfRepository<Order> in generated output.
RG-01 / RG-02 — explicit registration strategy
RegistrationMultiplicity and RegistrationEmissionStrategy let you define single/multiple registration intent and Add* vs TryAdd* emission in generated code.
[ServiceInjection(
RegistrationMultiplicity = RegistrationMultiplicity.Single,
RegistrationEmission = RegistrationEmissionStrategy.TryAdd)]
public interface IClock { }
RM-10 — OptionConfigAttribute + IOptions<>
What it solves
Configuration binding is repetitive when done manually across many option types.
OP evolution delivered in Phase 6
- Optional key fallback to options type name when omitted.
- Eligibility rules for options type shapes.
- Fast-path registration via
AddOptions<TOptions>().BindConfiguration(section).
When to use
- multiple options sections
- apps with many feature flags/settings blocks
NOTE: You must refer to
Microsoft.Extensions.Options.ConfigurationExtensionspackage to useAddOptions<TOptions>()in generated code. GenDI does not take a hard dependency on it, but will emit the correct registration code when the package is present in the project.
Example
[OptionConfig("Payments:Stripe")]
public sealed class StripeOptions
{
public required string ApiKey { get; init; }
public bool EnableRetries { get; init; }
}
[Injectable]
public sealed class StripeClient
{
[Inject]
public required IOptions<StripeOptions> Options { get; init; }
}
[OptionConfig]
public sealed class CheckoutOptions
{
public required string Currency { get; init; }
}
RM-11 — factory registration ([InjectableFactory<TService>])
What it solves
Some dependencies require controlled creation logic (third-party SDK builder, conditional setup, precomputed objects).
When to use
- construction cannot be represented by simple
new - centralize creation and keep services lean
Example
[InjectableModule("Billing")]
public static class BillingFactories
{
[InjectableFactory<IPaymentGateway>(
ServiceLifetime.Singleton,
Key = "stripe",
Module = "Billing")]
public static IPaymentGateway CreateStripeGateway() => new StripePaymentGateway();
}
Metadata supported: Lifetime, Group, Order, Key, ThreadIsolation, Module.
RM-12 — module grouping ([InjectableModule] + Module)
What it solves
Large applications need selective loading by bounded context.
When to use
- plugin architecture
- feature toggles by module
- hosts that load only subsets of shared components
Example
builder.Services.AddGenDIServices("Billing", "Orders");
This loads only registrations associated with selected modules.
Open-generic bypass and warning GENDISG001
Open-generic generation paths are intentionally bypassed and not registered. GenDI emits warning GENDISG001 so the omission is explicit.
Where this applies:
- injectable classes/contracts/decorators
- indirect
[Inject]discovery paths - factory registrations
This keeps generated output closed-generic and safe for NativeAOT/trimming scenarios.