Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions docs/core/extensions/access-by-line.txt
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
snippets/configuration/dependency-injection/Program.cs: ~/docs/core/extensions/dependency-injection.md
snippets/configuration/console-di-ienumerable/Program.cs: ~/docs/core/extensions/dependency-injection.md
snippets/configuration/app-lifetime/ExampleHostedService.cs: ~/docs/core/extensions/generic-host.md
snippets/configuration/app-lifetime/Program.cs: ~/docs/core/extensions/generic-host.md
snippets/configuration/console-custom-logging/ColorConsoleLogger.cs: ~/docs/core/extensions/custom-logging-provider.md
snippets/configuration/console-di-disposable/Program.cs: ~/docs/core/extensions/dependency-injection-guidelines.md
snippets/configuration/console-di-ienumerable/ExampleService.cs: ~/docs/core/extensions/dependency-injection.md
snippets/configuration/console-di-ienumerable/Program.cs: ~/docs/core/extensions/dependency-injection.md
snippets/configuration/console-di/Program.cs: ~/docs/core/extensions/dependency-injection-usage.md
snippets/configuration/console-host/Program.cs: ~/docs/core/extensions/generic-host.md
snippets/configuration/console-ini/Program.cs: ~/docs/core/extensions/configuration-providers.md
snippets/configuration/console-json/appsettings.json: ~/docs/core/extensions/options.md
snippets/configuration/console-json/Program.cs: ~/docs/core/extensions/configuration-providers.md
snippets/configuration/console-json/Program.cs: ~/docs/core/extensions/options.md
snippets/configuration/console-json/TransientFaultHandlingOptions.cs: ~/docs/core/extensions/options.md
snippets/configuration/console-xml/Program.cs: ~/docs/core/extensions/configuration-providers.md
snippets/configuration/console-ini/Program.cs: ~/docs/core/extensions/configuration-providers.md
snippets/configuration/console/Program.cs: ~/docs/core/extensions/configuration.md
snippets/logging/console-formatter-simple/Program.cs: ~/docs/core/extensions/custom-log-formatter.md
snippets/configuration/console/Program.cs: ~/docs/core/extensions/logging-providers.md
snippets/configuration/custom-provider/Program.cs: ~/docs/core/extensions/custom-configuration-provider.md
snippets/configuration/console-custom-logging/ColorConsoleLogger.cs: ~/docs/core/extensions/custom-logging-provider.md
snippets/configuration/console-di-disposable/Program.cs: ~/docs/core/extensions/dependency-injection-guidelines.md
snippets/configuration/console-di/Program.cs: ~/docs/core/extensions/dependency-injection-usage.md
snippets/configuration/console-host/Program.cs: ~/docs/core/extensions/generic-host.md
snippets/configuration/app-lifetime/Program.cs: ~/docs/core/extensions/generic-host.md
snippets/configuration/app-lifetime/ExampleHostedService.cs: ~/docs/core/extensions/generic-host.md
snippets/configuration/dependency-injection/Program.cs: ~/docs/core/extensions/dependency-injection.md
snippets/configuration/di-anti-patterns/Foo.cs: ~/docs/core/extensions/dependency-injection-guidelines.md
snippets/configuration/di-anti-patterns/Program.cs: ~/docs/core/extensions/dependency-injection-guidelines.md
snippets/configuration/worker-service-options/Worker.cs: ~/docs/core/extensions/high-performance-logging.md
snippets/configuration/console/Program.cs: ~/docs/core/extensions/logging-providers.md
snippets/configuration/worker-service/appsettings.IncludeScopes.json: ~/docs/core/extensions/logging.md
snippets/configuration/worker-service/Worker.cs: ~/docs/core/extensions/logging.md
snippets/configuration/console-json/appsettings.json: ~/docs/core/extensions/options.md
snippets/configuration/console-json/TransientFaultHandlingOptions.cs: ~/docs/core/extensions/options.md
snippets/configuration/console-json/Program.cs: ~/docs/core/extensions/options.md
snippets/logging/console-formatter-simple/Program.cs: ~/docs/core/extensions/custom-log-formatter.md
72 changes: 66 additions & 6 deletions docs/core/extensions/dependency-injection-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Dependency injection guidelines
description: Learn various dependency injection guidelines and best practices for .NET application development.
author: IEvangelist
ms.author: dapine
ms.date: 09/23/2020
ms.date: 10/29/2020
ms.topic: guide
---

Expand All @@ -29,11 +29,17 @@ In the following example, the services are created by the service container and

:::code language="csharp" source="snippets/configuration/console-di-disposable/TransientDisposable.cs":::

The preceding disposable is intended to have a transient lifetime.

:::code language="csharp" source="snippets/configuration/console-di-disposable/ScopedDisposable.cs":::

The preceding disposable is intended to have a scoped lifetime.

:::code language="csharp" source="snippets/configuration/console-di-disposable/SingletonDisposable.cs":::

:::code language="csharp" source="snippets/configuration/console-di-disposable/Program.cs" range="1-21,41-60":::
The preceding disposable is intended to have a singleton lifetime.

:::code language="csharp" source="snippets/configuration/console-di-disposable/Program.cs" range="1-21,41-60" highlight="":::

The debug console shows the following sample output after running:

Expand Down Expand Up @@ -111,7 +117,7 @@ Register the instance with a scoped lifetime. Use <xref:Microsoft.Extensions.Dep
- Receiving an <xref:System.IDisposable> dependency via DI doesn't require that the receiver implement <xref:System.IDisposable> itself. The receiver of the <xref:System.IDisposable> dependency shouldn't call <xref:System.IDisposable.Dispose%2A> 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)
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

Expand Down Expand Up @@ -145,17 +151,71 @@ The factory method of single service, such as the second argument to [AddSinglet
- `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.
- Avoid storing data and configuration directly in the service container. For example, a user's shopping cart shouldn't typically be added to the service container. Configuration should use the options pattern. Similarly, avoid "data holder" objects that only exist to allow access to another object. It's better to request the actual item via DI.
- Avoid static access to services. For example, avoid capturing [IApplicationBuilder.ApplicationServices](xref:Microsoft.AspNetCore.Builder.IApplicationBuilder.ApplicationServices) as a static field or property for use elsewhere.
- Keep DI factories fast and synchronous.
- Avoid using the *service locator pattern*. For example, don't invoke <xref:System.IServiceProvider.GetService%2A> to obtain a service instance when you can use DI instead.
- Keep [DI factories](#async-di-factories-can-cause-deadlocks) fast and synchronous.
- Avoid using the [*service locator pattern*](#scoped-service-as-singleton). For example, don't invoke <xref:System.IServiceProvider.GetService%2A> to obtain a service instance when you can use DI instead.
- Another service locator variation to avoid is injecting a factory that resolves dependencies at runtime. Both of these practices mix [Inversion of Control](/dotnet/standard/modern-web-apps-azure-architecture/architectural-principles#dependency-inversion) strategies.
- Avoid calls to <xref:Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider%2A> in `ConfigureServices`. Calling `BuildServiceProvider` typically happens when the developer wants to resolve a service in `ConfigureServices`.
- Disposable transient services are captured by the container for disposal. This can turn into a memory leak if resolved from the top-level container.
- [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).

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.

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.

## 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.

> [!WARNING]
> 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 <xref:System.IDisposable>, by default the DI container will hold onto these references, and not <xref:System.IDisposable.Dispose> of them until the application stops. This can turn into a memory leak if resolved from the level container.

:::code language="csharp" source="snippets/configuration/di-anti-patterns/Program.cs" range="18-30":::

In the preceding anti-pattern, 1,000 `ExampleDisposable` objects are instantiated and rooted. They will not be disposed of until the `serviceProvider` instance is disposed.

For more information on debugging memory leaks, see [Debug a memory leak in .NET](../diagnostics/debug-memory-leak.md).

### 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<IServiceProvider, T>` 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 <xref:System.Threading.Tasks.Task%601.Result?displayProperty=nameWithType>, this will cause a deadlock.

:::code language="csharp" source="snippets/configuration/di-anti-patterns/Program.cs" range="32-45" highlight="4-8":::

In the preceding code, the `implementationFactory` is given a lambda expression where the body calls <xref:System.Threading.Tasks.Task%601.Result?displayProperty=nameWithType> on a `Task<Bar>` returning method. This ***causes a deadlock***. The `GetBarAsync` method simply emulates an asynchronous work operation with <xref:System.Threading.Tasks.Task.Delay%2A?displayProperty=nameWithType>, and then calls <xref:Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService%60%601(System.IServiceProvider)>.

:::code language="csharp" source="snippets/configuration/di-anti-patterns/Program.cs" range="47-53":::

For more information on asynchronous guidance, see [Asynchronous programming: Important info and advice](../../csharp/async.md#important-info-and-advice). For more information debugging deadlocks, see [Debug a deadlock in .NET](../diagnostics/debug-deadlock.md).

When you're running this anti-pattern and the deadlock occurs, you can view the two threads waiting from Visual Studio's Parallel Stacks window. For more information, see [View threads and tasks in the Parallel Stacks window](/visualstudio/debugger/using-the-parallel-stacks-window).

### Captive dependency

The term ["captive dependency"](https://blog.ploeh.dk/2014/06/02/captive-dependency) was coined by [Mark Seeman](https://blog.ploeh.dk/about), and refers to the misconfiguration of service lifetimes, where a longer-lived service holds a shorter-lived service captive.

:::code language="csharp" source="snippets/configuration/di-anti-patterns/Program.cs" range="55-65":::

In the preceding code, `Foo` is registered as a singleton and `Bar` is scoped - which on the surface seems valid. However, consider the implementation of `Foo`.

:::code language="csharp" source="snippets/configuration/di-anti-patterns/Foo.cs" highlight="5":::

The `Foo` object requires a `Bar` object, and since `Foo` is a singleton, and `Bar` is scoped - this is a misconfiguration. As is, `Foo` would only be instantiated once, and it would hold onto `Bar` for its lifetime, which is longer than the intended scoped lifetime of `Bar`. You should consider validating scopes, by passing `validateScopes: true` to the <xref:Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Boolean)>. When you validate the scopes, you'd get an <xref:System.InvalidOperationException> 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.

:::code language="csharp" source="snippets/configuration/di-anti-patterns/Program.cs" range="68-82" highlight="13-14":::

In the preceding code, `Bar` is retrieved within an <xref:Microsoft.Extensions.DependencyInjection.IServiceScope>, which is correct. The anti-pattern is the retrieval of `Bar` outside of the scope, and the variable is named `avoid` to show which example retrieval is incorrect.

## See also

- [Dependency injection in .NET](dependency-injection.md)
- [Tutorial: Use dependency injection in .NET](dependency-injection-usage.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace DependencyInjection.AntiPatterns
{
public class Bar
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace DependencyInjection.AntiPatterns
{
public class ExampleDisposable : IDisposable
{
public void Dispose() =>
Console.WriteLine($"Disposed: {GetHashCode(),12}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace DependencyInjection.AntiPatterns
{
public class Foo
{
public Foo(Bar bar)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace DependencyInjection.AntiPatterns
{
class Program
{
static void Main()
{
// Uncomment individual methods below to see anti-patterns in action:
// TransientDisposablesWithoutDispose();
// DeadLockWithFactories();
// CaptiveDependency();
// ScopedServiceBecomesSingleton();
}

static void TransientDisposablesWithoutDispose()
{
var services = new ServiceCollection();
services.AddTransient<ExampleDisposable>();
ServiceProvider serviceProvider = services.BuildServiceProvider();

for (int i = 0; i < 1000; ++ i)
{
_ = serviceProvider.GetRequiredService<ExampleDisposable>();
}

// serviceProvider.Dispose();
}

static void DeadLockWithAsyncFactory()
{
var services = new ServiceCollection();
services.AddSingleton<Foo>(implementationFactory: provider =>
{
Bar bar = GetBarAsync(provider).Result;
return new Foo(bar);
});

services.AddSingleton<Bar>();

using ServiceProvider serviceProvider = services.BuildServiceProvider();
_ = serviceProvider.GetRequiredService<Foo>();
}

static async Task<Bar> GetBarAsync(IServiceProvider serviceProvider)
{
// Emulate asynchronous work operation
await Task.Delay(1000);

return serviceProvider.GetRequiredService<Bar>();
}

static void CaptiveDependency()
{
var services = new ServiceCollection();
services.AddSingleton<Foo>();
services.AddScoped<Bar>();

using ServiceProvider serviceProvider = services.BuildServiceProvider();
// Enable scope validation
// using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);

_ = serviceProvider.GetRequiredService<Foo>();
}

static void ScopedServiceBecomesSingleton()
{
var services = new ServiceCollection();
services.AddScoped<Bar>();

using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);
using (IServiceScope scope = serviceProvider.CreateScope())
{
// Correctly scoped resolution
Bar correct = scope.ServiceProvider.GetRequiredService<Bar>();
}

// Not within a scope, becomes a singleton
Bar avoid = serviceProvider.GetRequiredService<Bar>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>DependencyInjection.AntiPatterns</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0-rc.2.20475.5" />
</ItemGroup>
</Project>