Service Registration
This guide covers all the ways to register services in Pragmatic.Composition: the [Service] attribute family, decorators, factories, property injection, and assembly scanning.
[Service] — Attribute-Based Registration
Section titled “[Service] — Attribute-Based Registration”The simplest way to register a service. The source generator emits compile-time registration code — no reflection at runtime.
Basic Usage
Section titled “Basic Usage”[Service]public class OrderService : IOrderService { /* ... */ }Default behavior:
- Lifetime:
Scoped - Service type: First implemented interface
- If the class implements no interfaces, the SG reports a diagnostic. Use
AsSelf = truefor concrete registration.
Generic Attribute (Explicit Interface)
Section titled “Generic Attribute (Explicit Interface)”When a class implements multiple interfaces, use [Service<TInterface>] to pick one:
[Service<IPaymentProvider>(Key = "stripe")]public class StripePaymentProvider : IPaymentProvider, IDisposable { /* ... */ }This registers as IPaymentProvider only, ignoring IDisposable.
ServiceAttribute Properties
Section titled “ServiceAttribute Properties”| Property | Type | Default | Description |
|---|---|---|---|
Lifetime | ServiceLifetime | Scoped | Singleton, Scoped, or Transient |
As | Type? | null | Register as this type (prefer ServiceAttribute<T>) |
AsSelf | bool | false | Register as concrete type instead of interface |
Key | string? | null | Keyed service registration (.NET 8+) |
ServiceLifetime Enum
Section titled “ServiceLifetime Enum”Defined in Pragmatic.Composition.Attributes (mirrors Microsoft.Extensions.DependencyInjection.ServiceLifetime):
| Value | Behavior |
|---|---|
Singleton | One instance shared across all requests |
Scoped | One instance per scope (HTTP request) — default |
Transient | New instance every time |
Examples
Section titled “Examples”// Scoped (default), registered as IOrderService[Service]public class OrderService : IOrderService { }
// Singleton, registered as concrete type[Service(AsSelf = true, Lifetime = ServiceLifetime.Singleton)]public class CacheWarmupService { }
// Keyed service (explicit interface via generic)[Service<IPaymentProvider>(Key = "stripe")]public class StripePaymentProvider : IPaymentProvider { }
// Transient[Service(Lifetime = ServiceLifetime.Transient)]public class NotificationFormatter : INotificationFormatter { }[Decorator] — Service Decoration
Section titled “[Decorator] — Service Decoration”Decorators wrap an existing service, adding cross-cutting behavior. They must implement the same interface as the service they decorate.
[Decorator(Order = 1)]public class LoggingReservationPricingService( IReservationPricingService inner, ILogger<LoggingReservationPricingService> logger) : IReservationPricingService{ public decimal CalculatePrice(Reservation reservation) { logger.LogInformation("Calculating price for reservation {Id}", reservation.Id); var price = inner.CalculatePrice(reservation); logger.LogInformation("Price calculated: {Price}", price); return price; }}Order Semantics
Section titled “Order Semantics”The Order property determines decoration order:
- Lower order = closer to the real service (applied first, called last on the way in)
- Higher order = outermost (first to receive calls)
Call flow: Outer (Order=2) --> Inner (Order=1) --> RealServiceReturn: Outer (Order=2) <-- Inner (Order=1) <-- RealServiceRequirements
Section titled “Requirements”- The decorator must implement the same interface as the decorated service.
- The constructor must accept the inner service as a parameter (by its interface type).
- Additional constructor parameters are resolved from DI.
Manual Decoration
Section titled “Manual Decoration”For cases where attribute-based decoration is not suitable, use the Decorate<TService, TDecorator>() extension method:
// In IStartupStep.ConfigureServicesservices.Decorate<IOrderService, CachingOrderService>();Or with a factory:
services.Decorate<IOrderService>((inner, sp) =>{ var logger = sp.GetRequiredService<ILogger<LoggingOrderService>>(); return new LoggingOrderService(inner, logger);});The Decorate methods live in ServiceCollectionDecorateExtensions in Pragmatic.Abstractions, so domain modules can use them without referencing ASP.NET Core.
[ServiceFactory] / [Factory] — Factory Methods
Section titled “[ServiceFactory] / [Factory] — Factory Methods”For services that need custom construction logic, use factory classes:
[ServiceFactory]public class InfrastructureFactories{ [Factory(Lifetime = ServiceLifetime.Singleton)] public IDbConnection CreateConnection(IConfiguration config) { var connStr = config.GetConnectionString("Default"); return new SqlConnection(connStr); }
[Factory(Lifetime = ServiceLifetime.Scoped)] public IFileStorage CreateStorage(IHostEnvironment env, ILogger<LocalDiskFileStorage> logger) { var basePath = Path.Combine(env.ContentRootPath, "uploads"); return new LocalDiskFileStorage(basePath, logger); }}Rules:
[ServiceFactory]marks the container class. The class itself is registered as a singleton.[Factory]marks individual factory methods.- The return type of each method becomes the registered service type.
- Method parameters are resolved from DI.
FactoryAttribute.Lifetimecontrols the lifetime of the created service (default:Scoped).
[Inject] — Property and Method Injection
Section titled “[Inject] — Property and Method Injection”For optional dependencies or post-construction initialization. The class must also be marked with [Service].
Property Injection
Section titled “Property Injection”[Service]public class NotificationService : INotificationService{ // Optional -- null if IEmailSender is not registered [Inject] public IEmailSender? EmailSender { get; set; }
// Required -- throws if not available [Inject(Required = true)] public IMessageQueue Queue { get; set; } = null!;
// Keyed service [Inject(Key = "priority")] public IMessageQueue? PriorityQueue { get; set; }}InjectAttribute Properties
Section titled “InjectAttribute Properties”| Property | Type | Default | Description |
|---|---|---|---|
Required | bool | false | Throw if service not resolvable |
Key | string? | null | Keyed service key |
When to Use
Section titled “When to Use”- Prefer constructor injection for required dependencies (the normal pattern).
- Use
[Inject]for optional dependencies that may or may not be registered. - Use
[Inject]to break circular dependency chains. - Use
[Inject(Key = "...")]for keyed services on properties.
Assembly Scanning
Section titled “Assembly Scanning”For convention-based registration without attributes. The fluent API is accessible via services.Scan():
services.Scan(scan => scan .FromAssemblyOf<OrderService>() .AddClasses(filter => filter.AssignableTo<ICommandHandler>()) .AsImplementedInterfaces() .WithScopedLifetime());Assembly Sources
Section titled “Assembly Sources”| Method | Description |
|---|---|
FromAssemblyOf<T>() | Assembly containing type T |
FromAssemblies(params Assembly[]) | Explicit assembly list |
FromAssembliesMatching(string pattern) | Glob pattern on DLL filenames (e.g., "MyApp.*") |
FromCallingAssembly() | Calling assembly |
FromEntryAssembly() | Entry assembly |
FromDependencyContext(predicate?) | DependencyContext.Default runtime libraries |
FromDependencyContext(string prefix) | Libraries whose name starts with prefix |
Type Filters
Section titled “Type Filters”Applied within AddClasses(filter => ...):
| Method | Description |
|---|---|
AssignableTo<T>() | Implements T (supports open generics like IRepository<>) |
AssignableTo(Type type) | Same, non-generic |
WithAttribute<T>() | Has specific attribute |
InNamespace(string ns) | Exact namespace match |
InNamespaceOf<T>() | Namespace starts with T’s namespace |
Where(Func<Type, bool>) | Custom predicate |
NotWhere(Func<Type, bool>) | Exclude by predicate |
Filters are composable — they chain as AND conditions:
scan.FromAssemblyOf<OrderService>() .AddClasses(f => f .AssignableTo<IEventHandler>() .InNamespace("MyApp.Booking.Handlers") .NotWhere(t => t.Name.Contains("Test"))) .AsImplementedInterfaces() .WithScopedLifetime();Registration Modes
Section titled “Registration Modes”| Method | Description |
|---|---|
AsImplementedInterfaces() | Register for all implemented interfaces |
AsSelf() | Register as concrete type |
AsSelfWithInterfaces() | Register as concrete type AND all interfaces |
As<T>() | Register as specific type |
As(Type type) | Register as specific type (non-generic) |
As(Func<Type, Type> selector) | Custom type selector |
AsMatchingInterface() | Register as I{ClassName} (convention-based) |
Registration Strategies
Section titled “Registration Strategies”Control what happens when a service type is already registered:
scan.FromAssemblyOf<OrderService>() .AddClasses(f => f.AssignableTo<IRepository>()) .AsImplementedInterfaces() .UsingRegistrationStrategy(RegistrationStrategy.Skip) .WithScopedLifetime();| Strategy | Behavior |
|---|---|
Append | Always add (default). Multiple implementations for same interface. |
Skip | Skip if service type already registered |
Replace | Remove existing registrations, add new one |
Throw | Throw InvalidOperationException on duplicate |
Direct Type Registration
Section titled “Direct Type Registration”For registering specific types without filtering:
scan.FromAssemblyOf<OrderService>() .AddType<SpecialService>() .AsSelf() .WithSingletonLifetime();
scan.FromAssemblyOf<OrderService>() .AddTypes<ServiceA, ServiceB, ServiceC>() .AsImplementedInterfaces() .WithScopedLifetime();Error Handling
Section titled “Error Handling”The Scan method accepts an optional error callback for logging assembly load failures:
services.Scan( scan => scan.FromAssembliesMatching("MyApp.*").AddClasses().AsImplementedInterfaces().WithScopedLifetime(), onScanError: (message, ex) => logger.LogWarning(ex, message));TryAddService
Section titled “TryAddService”For conditional registration (add only if not already registered):
services.TryAddService<IOrderService, OrderService>(ServiceLifetime.Scoped);This is useful for default implementations that should be overridable.
Source-Generated vs Runtime Registration
Section titled “Source-Generated vs Runtime Registration”| Feature | Attribute-Based (SG) | Scanning (Runtime) |
|---|---|---|
| When | Compile-time | Runtime (startup) |
| Reflection | None | Assembly scanning uses reflection |
| Cross-assembly | Via [PragmaticMetadata] | Via assembly loading |
| Best for | Known services | Convention-based bulk registration |
| Performance | Zero overhead | Small startup cost |
Recommendation: Use [Service] and [Decorator] for all known services. Use scanning only for convention-based scenarios where attribute decoration is impractical.