Skip to main content

register-and-replace-services

Apply ABP's advanced dependency-injection conventions and controlled service customization. USE FOR: precise conventional exposure and lifetime rules; ExposeServices and Dependency attributes; replacing registrations; OnExposing, OnRegistered, and OnActivated callbacks; LazyServiceProvider/IAbpLazyServiceProvider; keeping constructor graphs cheap with Lazy dependencies; IObjectAccessor; keyed or string-named services; preserving lifetimes while decorating a service. DO NOT USE FOR: defining an AbpModule, its dependencies, and basic lifecycle or lifetime-marker registration (use define-application-modules); adding cross-cutting method interception (use use-interceptors-and-dynamic-proxy); replacing services specifically inside an installed pre-built module together with its entity/UI extensions (use customize-application-modules).

Source facts

Repository
abpframework/abp-skills
Last source activity
October 5, 2026 at 09:37
Detected SKILL.md language
English
Stars
23
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
register-and-replace-services
description
Apply ABP's advanced dependency-injection conventions and controlled service customization. USE FOR: precise conventional exposure and lifetime rules; ExposeServices and Dependency attributes; replacing registrations; OnExposing, OnRegistered, and OnActivated callbacks; LazyServiceProvider/IAbpLazyServiceProvider; keeping constructor graphs cheap with Lazy dependencies; IObjectAccessor; keyed or string-named services; preserving lifetimes while decorating a service. DO NOT USE FOR: defining an AbpModule, its dependencies, and basic lifecycle or lifetime-marker registration (use define-application-modules); adding cross-cutting method interception (use use-interceptors-and-dynamic-proxy); replacing services specifically inside an installed pre-built module together with its entity/UI extensions (use customize-application-modules).
license
MIT
# Registering and Replacing Services ABP builds on `IServiceCollection`: conventional registrars choose a lifetime and exposed service types, while attributes and registration callbacks let a module change those choices. Use constructor injection for normal dependencies; use the advanced mechanisms here only when exposure, replacement, late resolution, keys, activation hooks, or decoration requires them. ## When to Use - Restrict or expand which service types a conventionally registered class exposes. - Replace an existing interface/base-class registration without editing the original module. - Observe or modify service exposure and registration during assembly scanning. - Resolve optional services lazily or pass an object into startup/runtime code through `IObjectAccessor<T>`. - Register several implementations under keys, including string keys used as names. - Wrap an existing implementation while preserving its registered lifetime. ## When Not to Use - **Create a module or learn its lifecycle/basic DI markers** — use define-application-modules. - **Attach method interceptors** — use use-interceptors-and-dynamic-proxy. - **Customize a packaged module across entity, DTO, service, and UI extension points** — use customize-application-modules. ## How it works ### Conventional registration and default exposure `ITransientDependency`, `IScopedDependency`, and `ISingletonDependency` select the lifetime. Without an explicit exposure provider, ABP exposes the implementation type itself plus matching default interfaces. A default interface is one whose name matches the implementation suffix, ignoring an initial `I` and generic arity. ```csharp public interface IInvoiceCalculator { decimal Calculate(decimal netAmount); } public class InvoiceCalculator : IInvoiceCalculator, ITransientDependency { public decimal Calculate(decimal netAmount) => netAmount; } ``` This exposes `InvoiceCalculator` and `IInvoiceCalculator`. An unrelated implemented interface is not a default service unless explicitly exposed. ### Control exposure with `[ExposeServices]` ```csharp [ExposeServices(typeof(IInvoiceCalculator))] public class InvoiceCalculator : IInvoiceCalculator, ITransientDependency { public decimal Calculate(decimal netAmount) => netAmount; } ``` An explicit `[ExposeServices]` replaces default exposure unless `IncludeDefaults = true`; set `IncludeSelf = true` when the concrete class must also resolve. Multiple attributes are allowed. ### Control lifetime, try-add, and replacement `[Dependency]` has `Lifetime`, `TryRegister`, and `ReplaceServices`. The conventional registrar applies them in this order: replace, try-add, otherwise add. ```csharp [Dependency(ReplaceServices = true)] [ExposeServices(typeof(IInvoiceCalculator))] public class DiscountInvoiceCalculator : IInvoiceCalculator, ITransientDependency { public decimal Calculate(decimal netAmount) => netAmount * 0.9m; } ``` The imperative equivalent is useful when the replacement is selected by configuration: ```csharp context.Services.Replace( ServiceDescriptor.Transient<IInvoiceCalculator, DiscountInvoiceCalculator>()); ``` Match the old lifetime unless there is a reviewed reason to change it. `Replace` replaces the first descriptor with the same service type; inspect the collection when multiple registrations are intentional. ### Registration callbacks Install callbacks in `PreConfigureServices` so they observe conventionally scanned services. ```csharp public override void PreConfigureServices(ServiceConfigurationContext context) { context.Services.OnExposing(exposingContext => { if (typeof(IInternalService).IsAssignableFrom(exposingContext.ImplementationType)) { exposingContext.ExposedTypes.RemoveAll( service => service.ServiceType == exposingContext.ImplementationType); } }); context.Services.OnRegistered(registrationContext => { // Inspect ImplementationType and ServiceKey here. // Add IAbpInterceptor types only when method interception is intended. }); } ``` - `OnExposing` runs while conventional exposure is being built; its mutable `ExposedTypes` contains `ServiceIdentifier` entries. - `OnRegistered` receives `ImplementationType`, `ServiceKey`, and the interceptor type list. Use use-interceptors-and-dynamic-proxy before adding interceptors. - `OnActivated(descriptor, callback)` is descriptor-specific. With the Autofac integration, its callback receives the created `Instance`; it observes activation but does not replace that instance. ### Lazy resolution ABP has no `ILazyServiceProvider` type. ABP base classes expose a property named `LazyServiceProvider` whose type is `IAbpLazyServiceProvider`: ```csharp var clock = LazyServiceProvider.LazyGetRequiredService<IClock>(); ``` `IAbpLazyServiceProvider` remains for compatibility; its own source recommends `ITransientCachedServiceProvider` for new code. Both are cached service providers: the first resolution is stored, including transient services, for that provider instance. ```csharp public class ReportBuilder : ITransientDependency { private readonly ITransientCachedServiceProvider _services; public ReportBuilder(ITransientCachedServiceProvider services) { _services = services; } public IOptionalFormatter? GetFormatter() { return _services.GetService<IOptionalFormatter>(); } } ``` Prefer constructor injection when the dependency is required. Lazy lookup hides dependencies and can defer lifetime/cycle failures until runtime. ### Keep constructor graphs cheap A transient service is built again on every path that reaches it, together with its own constructor dependencies. When application services, managers, and registries inject each other, one request can build the same services many times over. Under load that can show up as high allocation and garbage-collection pauses that slow every endpoint at once, rather than as one slow query. - Inject a dependency that only some methods use lazily. With the Autofac integration (`UseAutofac()`), a `Lazy<T>` constructor parameter builds `T` on the first `.Value`, and later reads on the same holder reuse that instance. For a transient `T`, another holder builds its own; a scoped or singleton `T` keeps its registered sharing. - An injected `IEnumerable<T>` has already built every implementation before the constructor runs; enumerating it later saves nothing. Inject `Lazy<IEnumerable<T>>` when the collection is only needed on some paths. - Do not change a service to singleton or scoped only to save allocations; that changes its behavior, not just its cost. - Do not inject another application service to call one method. Move the shared part into a smaller class in the layer it belongs to (see separate-domain-and-application-logic). ```csharp public class InvoiceAppService : ApplicationService, IInvoiceAppService { private readonly IRepository<Invoice, Guid> _invoices; private readonly Lazy<IInvoicePdfRenderer> _pdfRenderer; // only ExportAsync needs it public InvoiceAppService(IRepository<Invoice, Guid> invoices, Lazy<IInvoicePdfRenderer> pdfRenderer) { _invoices = invoices; _pdfRenderer = pdfRenderer; } public async Task<byte[]> ExportAsync(Guid id) { return await _pdfRenderer.Value.RenderAsync(await _invoices.GetAsync(id)); } } ``` A lazy dependency resolves from the scope that built its holder. Read it while that scope is still alive; see manage-units-of-work for callbacks that run after commit. ### Share an object with `IObjectAccessor<T>` ```csharp var accessor = context.Services.AddObjectAccessor(new StartupState()); accessor.Value.IsConfigured = true; ``` `AddObjectAccessor` inserts the same `ObjectAccessor<T>` instance as singleton registrations for both `ObjectAccessor<T>` and `IObjectAccessor<T>`. It throws if one was already registered; `TryAddObjectAccessor<T>()` returns the existing accessor instead. During configuration, `GetObject<T>()` / `GetObjectOrNull<T>()` retrieves a previously registered accessor value from the service collection. ### Keyed and named services ABP supports .NET keyed services in conventional registration: ```csharp [ExposeKeyedService<IInvoiceCalculator>("standard")] public class StandardInvoiceCalculator : IInvoiceCalculator, ITransientDependency { public decimal Calculate(decimal netAmount) => netAmount; } public class CheckoutService { private readonly IInvoiceCalculator _calculator; public CheckoutService( [FromKeyedServices("standard")] IInvoiceCalculator calculator) { _calculator = calculator; } } ``` Resolve imperatively with `GetRequiredKeyedService<T>(key)`. A string key is the named-service pattern in this version; there is no separate ABP `INamedServiceProvider` abstraction in the verified dependency-injection sources. If a class has only `[ExposeKeyedService]` and no `[ExposeServices]`, ABP does not add its default unkeyed services. ### Decorating a service (know the limits) ABP core provides **no** general `Decorate<T>` extension. The descriptor-wrapping pattern below preserves only the original **lifetime value** — it does not preserve the original ABP registration. The inner service is rebuilt with `ActivatorUtilities.CreateInstance`, which bypasses the Autofac/ABP registration pipeline, so that inner instance loses ABP's dynamic-proxy interceptors (validation, unit of work, authorization, auditing, feature checks), convention-based property injection, and `OnActivated` callbacks. It also breaks the shared-instance identity when the type is exposed as both its concrete type and its interface (`ConventionalRegistrarBase` normally redirects those to one instance). So use this pattern **only** for a simple implementation-type service that does not rely on any of those ABP behaviors. When you need to decorate a service that keeps ABP interception/property injection, use container-specific decoration (Autofac's `RegisterDecorator`) instead: ```csharp var descriptor = context.Services.LastOrDefault( service => service.ServiceType == typeof(IInvoiceCalculator)); if (descriptor?.ImplementationType == null) { throw new InvalidOperationException( "IInvoiceCalculator must use an implementation-type registration."); } context.Services.Replace(ServiceDescriptor.Describe( typeof(IInvoiceCalculator), serviceProvider => new LoggingInvoiceCalculator( (IInvoiceCalculator)ActivatorUtilities.CreateInstance( serviceProvider, descriptor.ImplementationType)), descriptor.Lifetime)); ``` Do not apply this exact pattern to factory, instance, or keyed descriptors; their implementation data lives in different `ServiceDescriptor` properties. ## Validation - Resolve every intended service type (interface, concrete type, and key) and assert its runtime implementation. - Inspect `IServiceCollection` after configuration to confirm replacement count and lifetime. - For singleton/scoped services exposed through multiple types, verify all paths resolve the same instance within the expected scope. - Verify lazy/cached resolution twice, especially if the underlying service is transient. - For a `Lazy<T>` dependency, count constructions in a test: resolving the holder and calling methods that do not need it build nothing, and the first method that needs it builds it once. - For decoration, test behavior, exception propagation, and preservation of the original lifetime. ## Common Pitfalls - **Assuming every implemented interface is exposed** — default exposure is name-based. Use `[ExposeServices]` for unrelated interfaces. - **Forgetting `IncludeSelf`** — explicit exposure can make the concrete class unresolvable. - **Replacing under the wrong service type** — replace every injection path that consumers actually use. - **Treating `IAbpLazyServiceProvider` as a fresh resolve each time** — it caches resolved services, including transients. - **Using service location for required dependencies** — constructor injection makes requirements and cycles visible at startup. - **Injecting heavy services that most methods never use** — every transient in the graph is built on every resolve; make rarely used dependencies `Lazy<T>`. - **Expecting a late loop over an injected `IEnumerable<T>` to defer construction** — the container builds every element when it builds the holder; use `Lazy<IEnumerable<T>>`. - **Expecting a keyed-only class to resolve unkeyed** — add `[ExposeServices]` explicitly if both forms are required. - **Decorating a factory/instance descriptor as if it had `ImplementationType`** — branch by descriptor shape or use container-specific decoration.
View on GitHub