From 508bb71492a1fd02b5cd91843ee3ab5a96982ded Mon Sep 17 00:00:00 2001 From: abhiramch018 Date: Tue, 25 Nov 2025 17:47:47 +0530 Subject: [PATCH 1/5] docs: clarify thread safety of service resolution in .NET dependency injection ### Summary Clarified that resolving services from the built-in .NET dependency injection (DI) container is thread-safe. ### Changes - Updated the **Thread safety** section in `dependency-injection-guidelines.md`. - Added explicit statement that: - Service resolution via `IServiceProvider` and `IServiceScope` is thread-safe. - Service implementations themselves must still handle internal concurrency. - Added a note block for clarity. ### Motivation Resolves issue #47086 by explicitly documenting DI service resolution thread safety, which was previously implied but not stated. --- .../extensions/dependency-injection-guidelines.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/core/extensions/dependency-injection-guidelines.md b/docs/core/extensions/dependency-injection-guidelines.md index b813f82b612ff..03505ef178f7d 100644 --- a/docs/core/extensions/dependency-injection-guidelines.md +++ b/docs/core/extensions/dependency-injection-guidelines.md @@ -139,9 +139,16 @@ The following third-party containers can be used with ASP.NET Core apps: ## Thread safety -Create thread-safe singleton services. If a singleton service has a dependency on a transient service, the transient service may also require thread safety depending on how it's used by the singleton. +Create thread-safe singleton services. If a singleton service has a dependency on a transient service, the transient service may also require thread safety depending on how it's used by the singleton. The factory method of a singleton service, such as the second argument to [AddSingleton\(IServiceCollection, Func\)](xref:Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton%2A), doesn't need to be thread-safe. Like a type (`static`) constructor, it's guaranteed to be called only once by a single thread. + +Additionally, the process of **resolving services** from the built-in .NET dependency injection (DI) container is **thread-safe**. +Once an `IServiceProvider` or `IServiceScope` has been built, it's safe to resolve services concurrently from multiple threads. + +> [!NOTE] +> Thread safety of the DI container itself only guarantees that constructing and resolving services is safe. +> It doesn't make the resolved service instances themselves thread-safe. +> Any service (especially singletons) that holds shared mutable state must implement its own synchronization logic if accessed concurrently. -The factory method of a singleton service, such as the second argument to [AddSingleton\(IServiceCollection, Func\)](xref:Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton%2A), doesn't need to be thread-safe. Like a type (`static`) constructor, it's guaranteed to be called only once by a single thread. ## Recommendations From 15cce2b4b62a751208cb1eb106e0714f4bd17cad Mon Sep 17 00:00:00 2001 From: "Meaghan Osagie (Lewis)" Date: Wed, 26 Nov 2025 11:24:22 -0800 Subject: [PATCH 2/5] Remove extra line Clarify thread safety and recommendations for DI usage. --- docs/core/extensions/dependency-injection-guidelines.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/core/extensions/dependency-injection-guidelines.md b/docs/core/extensions/dependency-injection-guidelines.md index 03505ef178f7d..166c9afcbc109 100644 --- a/docs/core/extensions/dependency-injection-guidelines.md +++ b/docs/core/extensions/dependency-injection-guidelines.md @@ -149,7 +149,6 @@ Once an `IServiceProvider` or `IServiceScope` has been built, it's safe to resol > It doesn't make the resolved service instances themselves thread-safe. > Any service (especially singletons) that holds shared mutable state must implement its own synchronization logic if accessed concurrently. - ## Recommendations - `async/await` and `Task` based service resolution isn't supported. Because C# doesn't support asynchronous constructors, use asynchronous methods after synchronously resolving the service. From f657c47d1c54c7f1c1314560e7b254c98f24d349 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:57:23 -0800 Subject: [PATCH 3/5] Revise dependency injection guidelines for clarity Updated the dependency injection guidelines to improve clarity and correctness in the documentation. --- .../dependency-injection-guidelines.md | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/docs/core/extensions/dependency-injection-guidelines.md b/docs/core/extensions/dependency-injection-guidelines.md index 166c9afcbc109..497de606742f0 100644 --- a/docs/core/extensions/dependency-injection-guidelines.md +++ b/docs/core/extensions/dependency-injection-guidelines.md @@ -10,7 +10,7 @@ ai-usage: ai-assisted # Dependency injection guidelines -This article provides general guidelines and best practices for implementing dependency injection in .NET applications. +This article provides general guidelines and best practices for implementing dependency injection (DI) in .NET applications. ## Design services for dependency injection @@ -20,7 +20,7 @@ When designing services for dependency injection: - Avoid direct instantiation of dependent classes within services. Direct instantiation couples the code to a particular implementation. - Make services small, well-factored, and easily tested. -If a class has many injected dependencies, it might be a sign that the class has too many responsibilities and violates the [Single Responsibility Principle (SRP)](/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#single-responsibility). Attempt to refactor the class by moving some of its responsibilities into new classes. +If a class has many injected dependencies, it might be a sign that the class has too many responsibilities and violates the [Single Responsibility Principle (SRP)](../../standard/modern-web-apps-azure-architecture/architectural-principles.md#single-responsibility). Attempt to refactor the class by moving some of its responsibilities into new classes. ### Disposal of services @@ -69,7 +69,7 @@ SingletonDisposable.Dispose() Consider the following code: ```csharp -// Register example service in IServiceCollection +// Register example service in IServiceCollection. builder.Services.AddSingleton(new ExampleService()); ``` @@ -109,19 +109,19 @@ Register the instance with a scoped lifetime. Use instances with a transient lifetime. Use the factory pattern instead so the solved service can be manually disposed after it is done being used. -- Don't resolve instances with a transient or scoped lifetime in the root scope. The only exception to this is if the app creates/recreates and disposes , but this isn't an ideal pattern. +- Don't register instances with a transient lifetime. Use the factory pattern instead so the solved service can be manually disposed when it's no longer in use. +- Don't resolve instances with a transient or scoped lifetime in the root scope. The only exception to this is if the app creates or recreates and disposes , but this isn't an ideal pattern. - Receiving an dependency via DI doesn't require that the receiver implement itself. The receiver of the dependency shouldn't call on that dependency. - Use scopes to control the lifetimes of services. Scopes aren't hierarchical, and there's no special connection among scopes. -For more information on resource cleanup, see [Implement a `Dispose` method](../../standard/garbage-collection/implementing-dispose.md), or [Implement a `DisposeAsync` method](../../standard/garbage-collection/implementing-disposeasync.md). Additionally, consider the [Disposable transient services captured by container](#disposable-transient-services-captured-by-container) scenario as it relates to resource cleanup. +For more information on resource cleanup, see [Implement a `Dispose` method](../../standard/garbage-collection/implementing-dispose.md) or [Implement a `DisposeAsync` method](../../standard/garbage-collection/implementing-disposeasync.md). Additionally, consider the [Disposable transient services captured by container](#disposable-transient-services-captured-by-container) scenario as it relates to resource cleanup. ## Default service container replacement The built-in service container is designed to serve the needs of the framework and most consumer apps. We recommend using the built-in container unless you need a specific feature that it doesn't support, such as: - Property injection -- Injection based on name (.NET 7 and earlier versions only. For more information, see [Keyed services](dependency-injection.md#keyed-services).) +- Injection based on name (.NET 7 and earlier versions only–for more information, see [Keyed services](dependency-injection.md#keyed-services)) - Child containers - Custom lifetime management - `Func` support for lazy initialization @@ -139,14 +139,13 @@ The following third-party containers can be used with ASP.NET Core apps: ## Thread safety -Create thread-safe singleton services. If a singleton service has a dependency on a transient service, the transient service may also require thread safety depending on how it's used by the singleton. The factory method of a singleton service, such as the second argument to [AddSingleton\(IServiceCollection, Func\)](xref:Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton%2A), doesn't need to be thread-safe. Like a type (`static`) constructor, it's guaranteed to be called only once by a single thread. +Create thread-safe singleton services. If a singleton service has a dependency on a transient service, the transient service might also require thread safety depending on how it's used by the singleton. The factory method of a singleton service, such as the second argument to [AddSingleton\(IServiceCollection, Func\)](xref:Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton%2A), doesn't need to be thread-safe. Like a type (`static`) constructor, it's guaranteed to be called only once by a single thread. -Additionally, the process of **resolving services** from the built-in .NET dependency injection (DI) container is **thread-safe**. +Additionally, the process of resolving services from the built-in .NET dependency injection container is thread-safe. Once an `IServiceProvider` or `IServiceScope` has been built, it's safe to resolve services concurrently from multiple threads. > [!NOTE] -> Thread safety of the DI container itself only guarantees that constructing and resolving services is safe. -> It doesn't make the resolved service instances themselves thread-safe. +> Thread safety of the DI container itself only guarantees that constructing and resolving services is safe. It doesn't make the resolved service instances themselves thread-safe. > Any service (especially singletons) that holds shared mutable state must implement its own synchronization logic if accessed concurrently. ## Recommendations @@ -160,7 +159,7 @@ Once an `IServiceProvider` or `IServiceScope` has been built, it's safe to resol - Avoid calls to when configuring services. Calling `BuildServiceProvider` typically happens when the developer wants to resolve a service when registering another service. Instead, use an overload that includes the `IServiceProvider` for this reason. - [Disposable transient services are captured](#disposable-transient-services-captured-by-container) by the container for disposal. This can turn into a memory leak if resolved from the top-level container. - Enable scope validation to make sure the app doesn't have singletons that capture scoped services. For more information, see [Scope validation](dependency-injection.md#scope-validation). -- Only use singleton lifetime for services with their own state that is expensive to create or globally shared. Avoid using singleton lifetime for services which themselves have no state. Most .NET IoC containers use "Transient" as the default scope. Considerations and drawbacks of singletons: +- Only use singleton lifetime for services with their own state that is expensive to create or globally shared. Avoid using singleton lifetime for services that have no state themself. Most .NET IoC containers use "Transient" as the default scope. Considerations and drawbacks of singletons: - **Thread safety**: A singleton must be implemented in a thread-safe way. - **Coupling**: It can couple otherwise unrelated requests. - **Testing challenges**: Shared state and coupling can make unit testing more difficult. @@ -170,20 +169,20 @@ Once an `IServiceProvider` or `IServiceScope` has been built, it's safe to resol - **Scope leakage**: A singleton can inadvertently capture scoped or transient dependencies, effectively promoting them to singletons and causing unintended side effects. - **Initialization overhead**: When resolving a service, the IoC container needs to look up the singleton instance. If it doesn't already exist, it needs to create it in a thread-safe manner. In contrast, a stateless transient service is very cheap to create and destroy. -Like all sets of recommendations, you may encounter situations where ignoring a recommendation is required. Exceptions are rare, mostly special cases within the framework itself. +Like all sets of recommendations, you might encounter situations where ignoring a recommendation is required. Exceptions are rare, and are mostly special cases within the framework itself. -DI is an *alternative* to static/global object access patterns. You may not be able to realize the benefits of DI if you mix it with static object access. +DI is an *alternative* to static/global object access patterns. You might not realize the benefits of DI if you mix it with static object access. ## Example anti-patterns -In addition to the guidelines in this article, there are several anti-patterns *you **should** avoid*. Some of these anti-patterns are learnings from developing the runtimes themselves. +In addition to the guidelines in this article, there are several anti-patterns you **should avoid**. Some of these anti-patterns are learnings from developing the runtimes themselves. > [!WARNING] -> These are example anti-patterns, *do not* copy the code, *do not* use these patterns, and avoid these patterns at all costs. +> These are example anti-patterns. *Do not* copy the code, *do not* use these patterns, and avoid these patterns at all costs. ### Disposable transient services captured by container -When you register *Transient* services that implement , by default the DI container will hold onto these references, and not of them until the container is disposed when application stops if they were resolved from the container, or until the scope is disposed if they were resolved from a scope. This can turn into a memory leak if resolved from container level. +When you register *transient* services that implement , by default the DI container holds onto these references. It doesn't dispose of them until the container is disposed when application stops if they were resolved from the container, or until the scope is disposed if they were resolved from a scope. A memory leak can result if resolved from container level. :::image type="content" source="media/transient-disposables-without-dispose.png" lightbox="media/transient-disposables-without-dispose.png" alt-text="Anti-pattern: Transient disposables without dispose. Do not copy!"::: @@ -193,7 +192,7 @@ For more information on debugging memory leaks, see [Debug a memory leak in .NET ### Async DI factories can cause deadlocks -The term "DI factories" refers to the overload methods that exist when calling `Add{LIFETIME}`. There are overloads accepting a `Func` where `T` is the service being registered, and the parameter is named `implementationFactory`. The `implementationFactory` can be provided as a lambda expression, local function, or method. If the factory is asynchronous, and you use , this will cause a deadlock. +The term "DI factories" refers to the overload methods that exist when calling `Add{LIFETIME}`. There are overloads that accept a `Func` where `T` is the service being registered, and the parameter is named `implementationFactory`. The `implementationFactory` can be provided as a lambda expression, local function, or method. If the factory is asynchronous, and you use , it will cause a deadlock. :::image type="content" source="media/deadlock-with-async-factory.png" lightbox="media/deadlock-with-async-factory.png" alt-text="Anti-pattern: Deadlock with async factory. Do not copy!"::: @@ -207,7 +206,7 @@ When you're running this anti-pattern and the deadlock occurs, you can view the ### Captive dependency -The term ["captive dependency"](https://blog.ploeh.dk/2014/06/02/captive-dependency) was coined by [Mark Seemann](https://blog.ploeh.dk/about), and refers to the misconfiguration of service lifetimes, where a longer-lived service holds a shorter-lived service captive. +The term ["captive dependency"](https://blog.ploeh.dk/2014/06/02/captive-dependency), coined by [Mark Seemann](https://blog.ploeh.dk/about), refers to the misconfiguration of service lifetimes, where a longer-lived service holds a shorter-lived service captive. :::image type="content" source="media/captive-dependency.png" lightbox="media/captive-dependency.png" alt-text="Anti-pattern: Captive dependency. Do not copy!"::: @@ -215,13 +214,13 @@ In the preceding code, `Foo` is registered as a singleton and `Bar` is scoped - :::code language="csharp" source="snippets/configuration/di-anti-patterns/Foo.cs"::: -The `Foo` object requires a `Bar` object, and since `Foo` is a singleton, and `Bar` is scoped - this is a misconfiguration. As is, `Foo` is only instantiated once, and it holds onto `Bar` for its lifetime, which is longer than the intended scoped lifetime of `Bar`. Consider validating scopes by passing `validateScopes: true` to the . When you validate the scopes, you get an with a message similar to "Cannot consume scoped service 'Bar' from singleton 'Foo'.". +The `Foo` object requires a `Bar` object, and since `Foo` is a singleton, and `Bar` is scoped, this is a misconfiguration. As is, `Foo` is only instantiated once, and it holds onto `Bar` for its lifetime, which is longer than the intended scoped lifetime of `Bar`. Consider validating scopes by passing `validateScopes: true` to the . When you validate the scopes, you get an with a message similar to "Cannot consume scoped service 'Bar' from singleton 'Foo'.". For more information, see [Scope validation](dependency-injection.md#scope-validation). ### Scoped service as singleton -When using scoped services, if you're not creating a scope or within an existing scope - the service becomes a singleton. +When using scoped services, if you're not creating a scope or within an existing scope, the service becomes a singleton. :::image type="content" source="media/scoped-services-becomes-singleton.png" lightbox="media/scoped-services-becomes-singleton.png" alt-text="Anti-pattern: Scoped service becomes singleton. Do not copy!"::: From b844cd6e557d9e2d1f9480fdcf834e321cef4c78 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Mon, 22 Dec 2025 14:02:21 -0800 Subject: [PATCH 4/5] Update docs/core/extensions/dependency-injection-guidelines.md --- docs/core/extensions/dependency-injection-guidelines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/extensions/dependency-injection-guidelines.md b/docs/core/extensions/dependency-injection-guidelines.md index 497de606742f0..13f83c2ed72c6 100644 --- a/docs/core/extensions/dependency-injection-guidelines.md +++ b/docs/core/extensions/dependency-injection-guidelines.md @@ -20,7 +20,7 @@ When designing services for dependency injection: - Avoid direct instantiation of dependent classes within services. Direct instantiation couples the code to a particular implementation. - Make services small, well-factored, and easily tested. -If a class has many injected dependencies, it might be a sign that the class has too many responsibilities and violates the [Single Responsibility Principle (SRP)](../../standard/modern-web-apps-azure-architecture/architectural-principles.md#single-responsibility). Attempt to refactor the class by moving some of its responsibilities into new classes. +If a class has many injected dependencies, it might be a sign that the class has too many responsibilities and violates the [Single Responsibility Principle (SRP)](../../architecture/modern-web-apps-azure/architectural-principles.md#single-responsibility). Attempt to refactor the class by moving some of its responsibilities into new classes. ### Disposal of services From 66ed65bebba8496b122fb7c2f5be2d9f86577488 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Tue, 23 Dec 2025 08:49:53 -0800 Subject: [PATCH 5/5] Update docs/core/extensions/dependency-injection-guidelines.md --- docs/core/extensions/dependency-injection-guidelines.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/core/extensions/dependency-injection-guidelines.md b/docs/core/extensions/dependency-injection-guidelines.md index 13f83c2ed72c6..fd62eec162973 100644 --- a/docs/core/extensions/dependency-injection-guidelines.md +++ b/docs/core/extensions/dependency-injection-guidelines.md @@ -121,7 +121,6 @@ For more information on resource cleanup, see [Implement a `Dispose` method](../ The built-in service container is designed to serve the needs of the framework and most consumer apps. We recommend using the built-in container unless you need a specific feature that it doesn't support, such as: - Property injection -- Injection based on name (.NET 7 and earlier versions only–for more information, see [Keyed services](dependency-injection.md#keyed-services)) - Child containers - Custom lifetime management - `Func` support for lazy initialization