Chapter 9: Registering services with dependency injection
This chapter introduces how to register custom services in ASP.NET Core DI, manage multiple registrations, and choose safe service lifetimes.
After this chapter you will be able to
- Custom service registration. Describe how registering an application's own classes with the DI container lets ASP.NET Core create them instead of requiring manual object creation. State that the chapter covers several registration patterns and limitations of the built-in container.
- Multiple service registrations. Explain how the DI container handles multiple implementations or registrations of a service, including injecting all implementations, selecting a single registration, overriding a default, and conditional registration with TryAdd.
- Service lifetime choices. Distinguish the transient, scoped, and singleton lifetimes by how long service instances are used, and describe why lifetime choices can create pitfalls such as captive dependencies. Explain that resolving scoped services outside an HTTP request requires special attention.
- Custom service registration. Describe how to register the EmailSender hierarchy with WebApplicationBuilder.Services so that a request for IEmailSender is fulfilled by EmailSender and its required dependencies can also be created.
- Service implementation lifetime mapping. Explain the three pieces of information an Add* registration gives the DI container: the requested service type, the concrete implementation type it should create, and the service lifetime.
- Container construction limitations. Explain why the generic Add* registration methods cannot construct every service type. State that the built-in container requires a concrete type with one relevant constructor whose parameters are registered services or have default values, so a type requiring primitive constructor values such as string and int needs another registration approach.
- Instance and factory registrations. Describe the difference between registering a preconstructed object and registering a factory lambda. State that an object supplied to the container is always shared as a singleton, whereas a factory function is invoked when an instance is required and returns the object the container will use; factory functions must be synchronous.
- Open generic registration. Describe how to register an open generic service and implementation using the non-generic Add* overload that takes types. Explain that registering IRepository<T> with DbRepository<T> lets a constructor requiring a closed service such as IRepository<Customer> receive DbRepository<Customer>.
- Multiple interface implementations. Describe why a service interface such as IEmailSender can have several implementations, using EmailSender, SmsSender, and FacebookSender as the example.
- All versus single service use. Distinguish the two registration and injection scenarios introduced here: a consumer such as RegisterUser needs every implementation, or it needs only one implementation.
- Multiple implementation injection. Describe how to register multiple implementations of the same service interface and inject all of them into an endpoint handler as IEnumerable<IMessageSender>. Explain that the DI container supplies one instance for each registered implementation, in registration order.
- Last registration selection. Explain that when multiple implementations of a service type are registered and a consumer requests a single instance, the DI container resolves the last registered implementation. Apply this rule to the IMessageSender example, where FacebookSender is selected.
- Conditional default registrations. Explain how TryAddScoped conditionally registers a service only when no registration for that service type already exists. Describe why this lets a library supply a default implementation while allowing an application to provide its own alternative.
- Service lifetime purpose. Explain that a service lifetime is set during DI registration and determines how long an instance lives before the container creates a new one, including whether the container reuses an existing instance or creates another.
- Three built-in lifetimes. Describe the transient, scoped, and singleton lifetimes in ASP.NET Core: transient creates a new instance for every request, scoped reuses one instance within a scope but uses different instances for different scopes, and singleton always returns the same instance regardless of scope.
- Transient service lifetime. Explain that a service registered with the transient lifetime is created as a new instance every time the DI container needs it to fulfill a dependency, including multiple times within one request.
- Scoped service lifetime. Explain that a scoped service has one instance within a scope and a different instance in every other scope; in ASP.NET Core, a scope corresponds to a single HTTP request.
- Scoped registration and use cases. Describe registering a dependency with AddScoped and identify services suited to this lifetime: services shared within one request but changed between requests, such as database contexts and authentication services.
- Singleton instance reuse. Explain that a singleton service is created when first needed, or during registration when an instance is supplied, and the DI container injects that same instance every time the service is requested.
- Thread-safe singleton selection. Justify using singletons for expensive-to-create, shared, or immutable/stateless objects, and state that every singleton used in a web application must be thread-safe because concurrent requests can use it on multiple threads.
- Captive dependency problem. Explain how a captive dependency occurs when a longer-lived service receives a shorter-lived dependency, using the singleton Repository and scoped DataContext example.
- Lifetime dependency rule. State which dependency lifetimes are safe for singleton, scoped, and transient services, based on the rule that a service may use dependencies with lifetimes at least as long as its own.
- Root container resolution limits. Explain why resolving services directly from WebApplication.Services is appropriate only for singleton services. Describe how resolving scoped or transient services from this root provider can keep them alive until application shutdown and cause memory leaks.
- Manual service scope pattern. Describe how to resolve a scoped or transient service outside an HTTP request by creating an IServiceScope from IServiceProvider, resolving the service from the scope's ServiceProvider, and disposing the scope. Explain that HTTP requests receive a scope automatically, whereas Program.cs code outside a request must create one manually.
What it builds on
- Using dependency injection in ASP.NET Core (chapter 8) · The role of DI in ASP.NET Core and obtaining dependencies from the container.
- Using services from the DI container (chapter 8) · Using services that have been resolved by the DI container.
- Adding ASP.NET Core framework services to the container (chapter 8) · Framework services are added to the DI container through registrations.
- Creating loosely coupled code (chapter 8) · The benefit of depending on abstractions rather than tightly coupling code to implementations.
- Injecting services (chapter 7) · Services can be injected into minimal API endpoint handlers.
- Adding and configuring services (chapter 3) · Adding and configuring services in Program.cs.
- Program.cs file: Defining your application (chapter 3) · Program.cs as the place where an application builder is configured.
Section by section
Introduction
This chapter introduces how to register custom services in ASP.NET Core DI, manage multiple registrations, and choose safe service lifetimes.
- Custom service registration: Describe how registering an application's own classes with the DI container lets ASP.NET Core create them instead of requiring manual object creation. State that the chapter covers several registration patterns and limitations of the built-in container.
Why it matters: Service registration makes your application's dependencies available for framework-created components to receive automatically. - Multiple service registrations: Explain how the DI container handles multiple implementations or registrations of a service, including injecting all implementations, selecting a single registration, overriding a default, and conditional registration with TryAdd.
Why it matters: Applications and libraries often need to extend or replace services without accidentally discarding or duplicating registrations. - Service lifetime choices: Distinguish the transient, scoped, and singleton lifetimes by how long service instances are used, and describe why lifetime choices can create pitfalls such as captive dependencies. Explain that resolving scoped services outside an HTTP request requires special attention.
Why it matters: Choosing an inappropriate lifetime can cause incorrect sharing, stale state, or invalid dependencies in an application.
Key terms: dependency injection, DI container, service registration, built-in DI container, dependency graph, multiple implementations, default service registration, TryAdd, service lifetime, transient
9.1 Registering custom services with the DI container
This section shows how to register custom services and their dependencies so ASP.NET Core's DI container can create them when they are requested.
- Custom service registration: Describe how to register the EmailSender hierarchy with WebApplicationBuilder.Services so that a request for IEmailSender is fulfilled by EmailSender and its required dependencies can also be created.
Why it matters: Registration supplies the DI container with the recipe it needs to construct services for your endpoint handlers. - Service implementation lifetime mapping: Explain the three pieces of information an Add* registration gives the DI container: the requested service type, the concrete implementation type it should create, and the service lifetime.
Why it matters: These three choices determine what callers can request, what object is created, and how the container manages it. - Resolution failures and interface access: Explain that an unregistered IEmailSender is not recognized as a DI service in a minimal API handler, so the framework attempts to model-bind it from the request body and produces a runtime exception; also state that registering EmailSender only as IEmailSender means callers must request IEmailSender, not EmailSender.
Why it matters: Understanding the error and registration boundary helps you diagnose why an injected handler parameter cannot be resolved.
Key terms: DI container, IServiceCollection, WebApplicationBuilder.Services, IEmailSender, EmailSender, AddScoped, service type, implementation type, lifetime, TService
9.2 Registering services using objects and lambdas
This section shows how to register services that the built-in DI container cannot construct directly, using instances, factory lambdas, open-generic registrations, and registration extension methods.
- Container construction limitations: Explain why the generic Add* registration methods cannot construct every service type. State that the built-in container requires a concrete type with one relevant constructor whose parameters are registered services or have default values, so a type requiring primitive constructor values such as string and int needs another registration approach.
Why it matters: Knowing these construction rules tells you when a simple type-based DI registration will fail. - Instance and factory registrations: Describe the difference between registering a preconstructed object and registering a factory lambda. State that an object supplied to the container is always shared as a singleton, whereas a factory function is invoked when an instance is required and returns the object the container will use; factory functions must be synchronous.
Why it matters: These registration forms let you supply constructor values yourself and control how services are created. - Factory dependency access: Explain that a factory lambda receives an IServiceProvider, which can resolve services with GetService<T>() or GetRequiredService<T>() when needed to build the registered service. State that the section recommends avoiding this lookup where possible and favoring constructor injection because it is simpler to reason about and more performant.
Why it matters: Avoiding unnecessary service-location calls keeps dependency relationships clearer and registration code simpler. - Open generic registration: Describe how to register an open generic service and implementation using the non-generic Add* overload that takes types. Explain that registering IRepository<T> with DbRepository<T> lets a constructor requiring a closed service such as IRepository<Customer> receive DbRepository<Customer>.
Why it matters: Open-generic registration lets one reusable registration serve all closed generic versions of a service. - Grouped registration extensions: Describe organizing related service registrations in an extension method on the service collection. Explain that calling the custom extension method in Program.cs groups a logical set of registrations behind a simple API, similar in style to AddRazorPages(), making startup code easier to read.
Why it matters: Grouping registrations keeps Program.cs readable as an application gains features and dependencies.
Key terms: built-in DI container, concrete type, relevant constructor, Add* methods, primitive types, singleton, scoped, factory pattern, lambda function, IServiceProvider
9.3 Registering a service in the container multiple times
This section introduces registering multiple implementations of one interface and previews different injection behavior when a consumer needs all implementations versus only one.
- Multiple interface implementations: Describe why a service interface such as IEmailSender can have several implementations, using EmailSender, SmsSender, and FacebookSender as the example.
Why it matters: Multiple implementations let an application support different ways of performing the same kind of work. - All versus single service use: Distinguish the two registration and injection scenarios introduced here: a consumer such as RegisterUser needs every implementation, or it needs only one implementation.
Why it matters: Whether a consumer needs all services or one determines how the container's registrations should be used.
Key terms: interface, IEmailSender, EmailSender, SmsSender, FacebookSender, DI container, RegisterUser, consumer, implementation
9.3.1 Injecting multiple implementations of an interface
This section shows how to register several implementations of one interface and inject them all into an endpoint handler for collective use.
- Multiple implementation injection: Describe how to register multiple implementations of the same service interface and inject all of them into an endpoint handler as IEnumerable<IMessageSender>. Explain that the DI container supplies one instance for each registered implementation, in registration order.
Why it matters: This lets one action, such as user registration, send a message through every configured delivery channel. - Enumerable injection requirement: State that all registered implementations of service type T must be requested as IEnumerable<T>. Distinguish this required parameter type from T[] and ICollection<T>, which cannot be used as the handler or constructor argument even though the injected value is an array.
Why it matters: Using the exact supported parameter type prevents an InvalidOperationException when resolving multiple services.
Key terms: IMessageSender, IEnumerable<IMessageSender>, DI container, Add* extension methods, RegisterUser, IEnumerable<T>, T[], ICollection<T>, InvalidOperationException
9.3.2 Injecting a single implementation when multiple services are registered
This section explains that when several implementations of a service are registered, the DI container injects the last registration for a single service request and how this can override library defaults.
- Last registration selection: Explain that when multiple implementations of a service type are registered and a consumer requests a single instance, the DI container resolves the last registered implementation. Apply this rule to the IMessageSender example, where FacebookSender is selected.
Why it matters: This rule determines which implementation your application actually receives when it asks DI for one service. - Registration override tradeoff: Describe how registering a custom implementation after a library's Add* registration overrides the library implementation for single-service resolution. Explain that the earlier registrations remain available through IEnumerable<T>, so this approach does not leave only one implementation registered.
Why it matters: You can replace a library default without changing the library, but duplicate registrations can still affect code that requests all implementations.
Key terms: DI container, registered implementation, single instance, IMessageSender, FacebookSender, Add* extension method, custom implementation, IEnumerable<T>
9.3.3 Conditionally registering services using TryAdd
This section explains how to conditionally provide default DI services with TryAdd methods and how to replace an existing registration.
- Conditional default registrations: Explain how TryAddScoped conditionally registers a service only when no registration for that service type already exists. Describe why this lets a library supply a default implementation while allowing an application to provide its own alternative.
Why it matters: Conditional defaults make reusable libraries extensible without overriding an application's choices. - TryAdd registration outcome: Describe the outcome of the example in which EmailSender is already registered as IMessageSender and SmsSender is added with TryAddScoped. State that the SmsSender registration is ignored, so it is not available from the application container.
Why it matters: This outcome shows that TryAdd is a conditional fallback, not a way to accumulate multiple implementations. - Replacement lifetime requirement: State that Replace() can replace a previously registered implementation, and that the replacement must be registered with the same lifetime as the service it replaces.
Why it matters: Matching the original lifetime avoids changing when and how often the replaced dependency is created.
Key terms: TryAddScoped, Microsoft.Extensions.DependencyInjection.Extensions, IMessageSender, EmailSender, SmsSender, Replace(), lifetime
9.4 Understanding lifetimes: When are services created?
This section introduces DI service lifetimes, contrasts transient, scoped, and singleton reuse behavior, and sets up an example for observing their effects across dependency graphs and requests.
- Service lifetime purpose: Explain that a service lifetime is set during DI registration and determines how long an instance lives before the container creates a new one, including whether the container reuses an existing instance or creates another.
Why it matters: Choosing a lifetime controls object sharing and creation throughout your application. - Three built-in lifetimes: Describe the transient, scoped, and singleton lifetimes in ASP.NET Core: transient creates a new instance for every request, scoped reuses one instance within a scope but uses different instances for different scopes, and singleton always returns the same instance regardless of scope.
Why it matters: These three choices determine whether services are isolated, shared within a request, or shared application-wide. - DataContext lifetime example: Trace the example dependency graph in which the RowCounts handler needs both DataContext directly and Repository, while Repository itself depends on DataContext; explain that two requests require four DataContext resolutions before lifetime reuse rules are applied, and that random RowCount values identify different DataContext instances.
Why it matters: The example makes it possible to observe exactly how a selected lifetime changes sharing between direct and nested dependencies.
Key terms: lifetime, DI container, service registration, dependency, Transient, Scoped, Singleton, scope, web request, dependency graph
9.4.1 Transient: Everyone is unique
This section explains that transient DI services are newly created for every dependency resolution and are best limited to lightweight, mostly stateless services.
- Transient service lifetime: Explain that a service registered with the transient lifetime is created as a new instance every time the DI container needs it to fulfill a dependency, including multiple times within one request.
Why it matters: Choosing transient means consumers do not share a service instance, which affects state and object creation. - Transient lifetime suitability: Justify using the transient lifetime for lightweight services with little or no state, and describe its object-creation cost as being equivalent to calling new whenever an object is needed.
Why it matters: Transient services can create many objects, so the lifetime should fit the service's cost and state requirements.
Key terms: transient services, transient lifetime, dependency, dependency graph, DI container, AddTransient, lightweight services, state, scoped
9.4.2 Scoped: Let’s stick together
This section explains how scoped services share one instance throughout an ASP.NET Core request and receive a new instance for each separate request.
- Scoped service lifetime: Explain that a scoped service has one instance within a scope and a different instance in every other scope; in ASP.NET Core, a scope corresponds to a single HTTP request.
Why it matters: Scoped lifetime lets related services share request-specific state without carrying it into later requests. - Scoped registration and use cases: Describe registering a dependency with AddScoped and identify services suited to this lifetime: services shared within one request but changed between requests, such as database contexts and authentication services.
Why it matters: Choosing AddScoped for request-specific dependencies ensures collaborating services see the same request-level service instance. - Disposal at scope end: State that the DI container automatically disposes scoped and transient services that implement IDisposable when their scope ends.
Why it matters: Automatic disposal releases resources held by disposable services at the end of their DI scope.
Key terms: scoped lifetime, scope, request, DI container, AddScoped, scoped dependencies, DataContext, Repository, authentication services, IDisposable
9.4.3 Singleton: There can be only one
This section explains that singleton registration supplies one shared service instance for the application lifetime and requires that shared service to be thread-safe.
- Singleton instance reuse: Explain that a singleton service is created when first needed, or during registration when an instance is supplied, and the DI container injects that same instance every time the service is requested.
Why it matters: A singleton lifetime makes state and object identity shared across all requests and consumers. - Shared DataContext behavior: Describe how registering DataContext as a singleton makes the row-count value the same across requests, including when DataContext is injected directly into a handler or used transitively through Repository.
Why it matters: The row-count example makes the otherwise invisible reuse of a singleton instance observable. - Thread-safe singleton selection: Justify using singletons for expensive-to-create, shared, or immutable/stateless objects, and state that every singleton used in a web application must be thread-safe because concurrent requests can use it on multiple threads.
Why it matters: Choosing a shared lifetime for a non-thread-safe service can cause subtle failures when requests run concurrently.
Key terms: singleton, DI container, service registration, DataContext, RowCounts, Repository, thread-safe, concurrent requests, caching service
9.4.4 Keeping an eye out for captive dependencies
This section explains how mismatched DI service lifetimes create captive dependencies and how ASP.NET Core validation settings help detect lifetime and registration errors.
- Captive dependency problem: Explain how a captive dependency occurs when a longer-lived service receives a shorter-lived dependency, using the singleton Repository and scoped DataContext example.
Why it matters: A captive dependency can silently make an object live far longer than intended and cause difficult-to-find bugs. - Lifetime dependency rule: State which dependency lifetimes are safe for singleton, scoped, and transient services, based on the rule that a service may use dependencies with lifetimes at least as long as its own.
Why it matters: Applying the lifetime rule while registering services prevents captive dependencies before the application runs. - DI validation settings: Describe what ValidateScopes and ValidateOnBuild check, when they run, and why their development-only defaults relate to their performance cost.
Why it matters: These checks expose captive dependencies and missing registrations early, when they are much easier to fix.
Key terms: captive dependency, DataContext, Repository, scoped, singleton, transient, lifetime, ValidateScopes, ValidateOnBuild, WebApplicationBuilder
9.5 Resolving scoped services outside a request
This section explains how to safely resolve scoped or transient services outside an HTTP request by creating and disposing a DI scope manually.
- Root container resolution limits: Explain why resolving services directly from WebApplication.Services is appropriate only for singleton services. Describe how resolving scoped or transient services from this root provider can keep them alive until application shutdown and cause memory leaks.
Why it matters: Using the root container for the wrong lifetime can retain resources for the entire life of the application. - Manual service scope pattern: Describe how to resolve a scoped or transient service outside an HTTP request by creating an IServiceScope from IServiceProvider, resolving the service from the scope's ServiceProvider, and disposing the scope. Explain that HTTP requests receive a scope automatically, whereas Program.cs code outside a request must create one manually.
Why it matters: A manually disposed scope gives short-lived services the correct lifetime and releases the resources they hold. - Asynchronous scope preference: State that CreateAsyncScope() is generally preferred over CreateScope() when possible, because it addresses an edge case involving services that implement IAsyncDisposable. Describe that the asynchronous scope must also be disposed appropriately.
Why it matters: Preferring an asynchronous scope helps ensure services with asynchronous cleanup are handled correctly.
Key terms: WebApplication.Services, root DI container, singleton, scoped, transient, GetRequiredService<T>(), IServiceProvider, IServiceScope, CreateScope(), ServiceProvider
9.6 Summary
This summary consolidates how to register services, select implementations and lifetimes, handle multiple registrations, and resolve scoped services outside requests.
Where this is used later
- Registering a data context (chapter 12) · Registers an EF Core data context, whose lifetime must be chosen appropriately.
- Binding strongly typed settings without the IOptions interface (chapter 10) · Registering services with objects and lambdas.
- Configuring named clients at registration time (chapter 33) · Named HttpClient setup uses registration-time factory-style configuration.
- Using DI with OptionsBuilder and IConfigureOptions (chapter 31) · Advanced options configuration can use DI to obtain dependencies during configuration.
- Using scoped services in background tasks (chapter 34) · Background tasks need an explicit approach for obtaining scoped services.
- Using a third-party dependency injection container (chapter 31) · Choosing a third-party DI container requires understanding the kinds of registrations supported by the built-in container.
- Adding a configuration provider in Program.cs (chapter 10) · Application setup in Program.cs continues to add configuration providers and related startup code.
- Configuring the ASP.NET Core Identity services (chapter 23) · Adding Identity involves configuring a related set of services.
Concepts explained in this chapter
- Singleton patternA singleton pattern is a creational design pattern that ensures a service or object has one shared instance for the relevant application lifetime.
- bean scopeA scoped lifetime reuses one service instance within a dependency-injection scope and creates different instances for different scopes.
- service lifetimeA service lifetime is the dependency-injection rule that determines how long a registered service instance is retained and when it is disposed.
- transient lifetimeA transient lifetime is a dependency-injection lifetime policy that creates a new service instance each time the service is requested.
- dependency injection containerThe built-in dependency injection container is the default service container provided by ASP.NET Core for registering and resolving application services.
- dependency injectionDependency injection is a design pattern in which an object receives its dependencies from an external provider rather than creating them itself.
- service registrationService registration is the configuration that maps a requested service or interface to the implementation the dependency-injection container should provide.
- AddSingletonAddSingleton is an ASP.NET Core service-registration method that registers a service with singleton lifetime.
- scopeA scope is a named permission describing an operation or resource that a client or user has been granted or consented to access.
- implementation typeAn implementation type is the concrete class that a dependency injection container creates to fulfill a service dependency.
- service typeA service type is the class or interface that a dependency injection container uses as the requested dependency contract.
- AddScopedAddScoped is a Microsoft.Extensions.DependencyInjection method that registers a service with a scoped lifetime in the dependency-injection container.
- serviceA service is a class that provides a discrete piece of functionality to an application and can be registered with its dependency-injection container.
- IServiceScopeIServiceScope is the .NET abstraction representing a dependency-injection scope and providing access to the scoped service provider.
- genericsA generic method is a method defined with type parameters that can operate on arguments or return values of specified types.
- open genericAn open generic is a type or type definition that contains one or more unspecified generic type parameters.
- captive dependencyA captive dependency is a lifetime mismatch in which a longer-lived service holds a dependency whose lifetime is shorter than its own.
- factoryA factory function is a function that creates and returns a configured object.
- dependency graphA dependency graph is a directed graph representing objects or components and the dependencies required to construct or operate them.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
