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
24 changes: 24 additions & 0 deletions docs/core/extensions/access-by-line.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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/Program.cs: ~/docs/core/extensions/configuration.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/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/worker-service/appsettings.IncludeScopes.json: ~/docs/core/extensions/logging.md
snippets/configuration/worker-service/Worker.cs: ~/docs/core/extensions/logging.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)
35 changes: 30 additions & 5 deletions docs/core/extensions/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Dependency injection in .NET
description: Learn how .NET implements dependency injection and how to use it.
author: IEvangelist
ms.author: dapine
ms.date: 09/23/2020
ms.date: 10/28/2020
ms.topic: overview
---

Expand Down Expand Up @@ -205,16 +205,41 @@ The framework provides service registration extension methods that are useful in

For more information on type disposal, see the [Disposal of services](dependency-injection-guidelines.md#disposal-of-services) section.

Registering a service with only an implementation type is equivalent to registering that service with the same implementation and service type. This is why multiple implementations of a service cannot be registered using the methods that don't take an explicit service type. These methods can register multiple *instances* of a service, but they will all have the same *implementation* type.

Any of the above service registration methods can be used to register multiple service instances of the same service type. In the following example, `AddSingleton` is called twice with `IMessageWriter` as the service type. The second call to `AddSingleton` overrides the previous one when resolved as `IMessageWriter` and adds to the previous one when multiple services are resolved via `IEnumerable<IMessageWriter>`. Services appear in the order they were registered when resolved via `IEnumerable<{SERVICE}>`.

:::code language="csharp" source="snippets/configuration/console-di-ienumerable/Program.cs" highlight="19-24":::

The preceding sample source code registers two implementations of the `IMessageWriter`.

:::code language="csharp" source="snippets/configuration/console-di-ienumerable/ExampleService.cs" highlight="9-18":::

The `ExampleService` defines two constructor parameters; a single `IMessageWriter`, and an `IEnumerable<IMessageWriter>`. The single `IMessageWriter` is the last implemenation to have been registered, whereas the `IEnumerable<IMessageWriter>` represents all registered implementations.

The framework also provides `TryAdd{LIFETIME}` extension methods, which register the service only if there isn't already an implementation registered.

In the following example, the call to `AddSingleton` registers `MessageWriter` as an implementation for `IMessageWriter`. The call to `TryAddSingleton` has no effect because `IMessageWriter` already has a registered implementation:
In the following example, the call to `AddSingleton` registers `ConsoleMessageWriter` as an implementation for `IMessageWriter`. The call to `TryAddSingleton` has no effect because `IMessageWriter` already has a registered implementation:

```csharp
services.AddSingleton<IMessageWriter, MessageWriter>();
services.TryAddSingleton<IMessageWriter, DifferentMessageWriter>();
services.AddSingleton<IMessageWriter, ConsoleMessageWriter>();
services.TryAddSingleton<IMessageWriter, LoggingMessageWriter>();
```

The `TryAddSingleton` has no effect, as it was already added and the "try" will fail.
The `TryAddSingleton` has no effect, as it was already added and the "try" will fail. The `ExampleService` would assert the following:

```csharp
public class ExampleService
{
public ExampleService(
IMessageWriter messageWriter,
IEnumerable<IMessageWriter> messageWriters)
{
Trace.Assert(messageWriter is ConsoleMessageWriter);
Trace.Assert(messageWriters.Single() is ConsoleMessageWriter);
}
}
```

For more information, see:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

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

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
</PropertyGroup>

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

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

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

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

namespace ConsoleDI.IEnumerableExample
{
public class ConsoleMessageWriter : IMessageWriter
{
public void Write(string message) =>
Console.WriteLine(
$"ConsoleMessageWriter.Write(message: \"{message}\")");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace ConsoleDI.IEnumerableExample
{
public class ExampleService
{
public ExampleService(
IMessageWriter messageWriter,
IEnumerable<IMessageWriter> messageWriters)
{
Trace.Assert(messageWriter is LoggingMessageWriter);

var dependencyArray = messageWriters.ToArray();
Trace.Assert(dependencyArray[0] is ConsoleMessageWriter);
Trace.Assert(dependencyArray[1] is LoggingMessageWriter);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace ConsoleDI.IEnumerableExample
{
public interface IMessageWriter
{
void Write(string message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.Extensions.Logging;

namespace ConsoleDI.IEnumerableExample
{
public class LoggingMessageWriter : IMessageWriter
{
private readonly ILogger<LoggingMessageWriter> _logger;

public LoggingMessageWriter(ILogger<LoggingMessageWriter> logger) =>
_logger = logger;

public void Write(string message) =>
_logger.LogInformation(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Threading.Tasks;
using ConsoleDI.IEnumerableExample;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ConsoleDI.Example
{
class Program
{
static Task Main(string[] args)
{
IHost host = CreateHostBuilder(args).Build();

_ = host.Services.GetService<ExampleService>();

return host.RunAsync();
}

static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
services.AddSingleton<IMessageWriter, ConsoleMessageWriter>()
.AddSingleton<IMessageWriter, LoggingMessageWriter>()
.AddSingleton<ExampleService>());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>ConsoleDI.IEnumerableExample</RootNamespace>
</PropertyGroup>

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

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

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

</Project>
Loading