From 44c47fd2849355dda8de885566f21a9f7f9dba36 Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 29 Oct 2020 08:23:54 -0500 Subject: [PATCH 1/5] wip bits...mid-change, context switch --- .../dependency-injection-guidelines.md | 18 ++++ .../configuration/di-anti-patterns/Program.cs | 101 ++++++++++++++++++ .../di-anti-patterns/di-anti-patterns.csproj | 11 ++ 3 files changed, 130 insertions(+) create mode 100644 docs/core/extensions/snippets/configuration/di-anti-patterns/Program.cs create mode 100644 docs/core/extensions/snippets/configuration/di-anti-patterns/di-anti-patterns.csproj diff --git a/docs/core/extensions/dependency-injection-guidelines.md b/docs/core/extensions/dependency-injection-guidelines.md index 170b348c1338b..0b0e40e84e5d6 100644 --- a/docs/core/extensions/dependency-injection-guidelines.md +++ b/docs/core/extensions/dependency-injection-guidelines.md @@ -156,6 +156,24 @@ Like all sets of recommendations, you may encounter situations where ignoring a 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. + +### Disposable transient services captured by container + + + +### Async DI factories can cause deadlocks + +The term "DI factories" refers to the overload methods that exist when calling `Add{LIFETIME}`. There are often overloads accepting a `Func` where `T` is the service being registered, and they parameters in the overload is named `implementationFactory`. + +### Captive dependency + + +### Scoped service as singleton + + ## See also - [Dependency injection in .NET](dependency-injection.md) diff --git a/docs/core/extensions/snippets/configuration/di-anti-patterns/Program.cs b/docs/core/extensions/snippets/configuration/di-anti-patterns/Program.cs new file mode 100644 index 0000000000000..7ac5d78c1b128 --- /dev/null +++ b/docs/core/extensions/snippets/configuration/di-anti-patterns/Program.cs @@ -0,0 +1,101 @@ + +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(); + using ServiceProvider serviceProvider = + services.BuildServiceProvider(); + + for (int i = 0; i < 1000; ++ i) + { + _ = serviceProvider.GetRequiredService(); + } + + serviceProvider.Dispose(); + } + + static void DeadLockWithFactories() + { + var services = new ServiceCollection(); + services.AddSingleton(implementationFactory: provider => + { + Bar bar = GetBarAsync(provider).Result; + return new Foo(bar); + }); + + services.AddSingleton(); + + using ServiceProvider serviceProvider = + services.BuildServiceProvider(); + _ = serviceProvider.GetRequiredService(); + } + + static async Task GetBarAsync(IServiceProvider serviceProvider) + { + await Task.Delay(1000); + + return serviceProvider.GetRequiredService(); + } + + static void CaptiveDependency() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddScoped(); + + // using ServiceProvider serviceProvider = services.BuildServiceProvider(); + using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true); + + _ = serviceProvider.GetRequiredService(); + } + + static void ScopedServiceBecomesSingleton() + { + var services = new ServiceCollection(); + services.AddScoped(); + + using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true); + using (IServiceScope scope = serviceProvider.CreateScope()) + { + // Correctly scoped resolution + Bar correct = scope.ServiceProvider.GetRequiredService(); + } + + // This makes the scoped service a singleton, as it is not within a scope + Bar avoid = serviceProvider.GetRequiredService(); + } + } + + public class Foo + { + public Foo(Bar bar) + { + } + } + + public class Bar + { + } + + public class SomethingDisposable : IDisposable + { + void IDisposable.Dispose() => Console.WriteLine("Disposed"); + } +} diff --git a/docs/core/extensions/snippets/configuration/di-anti-patterns/di-anti-patterns.csproj b/docs/core/extensions/snippets/configuration/di-anti-patterns/di-anti-patterns.csproj new file mode 100644 index 0000000000000..0aafc3f97ef6e --- /dev/null +++ b/docs/core/extensions/snippets/configuration/di-anti-patterns/di-anti-patterns.csproj @@ -0,0 +1,11 @@ + + + + net5.0 + DependencyInjection.AntiPatterns + + + + + + From 1fe7bdb4bd464a6dc7c38d7374378e8e584544ae Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 29 Oct 2020 10:58:38 -0500 Subject: [PATCH 2/5] Updates to additional/contextual samples of anti-patterns --- .../dependency-injection-guidelines.md | 47 ++++++++++++++++--- .../configuration/di-anti-patterns/Bar.cs | 6 +++ .../di-anti-patterns/ExampleDisposable.cs | 10 ++++ .../configuration/di-anti-patterns/Foo.cs | 9 ++++ .../configuration/di-anti-patterns/Program.cs | 41 +++++----------- 5 files changed, 78 insertions(+), 35 deletions(-) create mode 100644 docs/core/extensions/snippets/configuration/di-anti-patterns/Bar.cs create mode 100644 docs/core/extensions/snippets/configuration/di-anti-patterns/ExampleDisposable.cs create mode 100644 docs/core/extensions/snippets/configuration/di-anti-patterns/Foo.cs diff --git a/docs/core/extensions/dependency-injection-guidelines.md b/docs/core/extensions/dependency-injection-guidelines.md index 0b0e40e84e5d6..5ae4ea1ac5b8a 100644 --- a/docs/core/extensions/dependency-injection-guidelines.md +++ b/docs/core/extensions/dependency-injection-guidelines.md @@ -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 --- @@ -111,7 +111,7 @@ Register the instance with a scoped lifetime. Use 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) +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 @@ -145,11 +145,11 @@ 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 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 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 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. @@ -160,19 +160,54 @@ DI is an *alternative* to static/global object access patterns. You may not be a 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 , by default the DI container will hold onto these references, and not 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 often overloads accepting a `Func` where `T` is the service being registered, and they parameters in the overload is named `implementationFactory`. +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. + +:::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 on a `Task` returning method. This ***causes a deadlock***. The `GetBarAsync` method simply emulates an asynchronous work operation with , and then calls . + +:::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 . When you validate the scopes, you'd 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. + +:::code language="csharp" source="snippets/configuration/di-anti-patterns/Program.cs" range="68-82"::: + +In the preceding code, `Bar` is retrieved within an , which is correct. The anti-pattern is the retrieval of `Bar` outside of the scope. ## See also diff --git a/docs/core/extensions/snippets/configuration/di-anti-patterns/Bar.cs b/docs/core/extensions/snippets/configuration/di-anti-patterns/Bar.cs new file mode 100644 index 0000000000000..6179e7540b3dc --- /dev/null +++ b/docs/core/extensions/snippets/configuration/di-anti-patterns/Bar.cs @@ -0,0 +1,6 @@ +namespace DependencyInjection.AntiPatterns +{ + public class Bar + { + } +} diff --git a/docs/core/extensions/snippets/configuration/di-anti-patterns/ExampleDisposable.cs b/docs/core/extensions/snippets/configuration/di-anti-patterns/ExampleDisposable.cs new file mode 100644 index 0000000000000..4dd9c209b2ba8 --- /dev/null +++ b/docs/core/extensions/snippets/configuration/di-anti-patterns/ExampleDisposable.cs @@ -0,0 +1,10 @@ +using System; + +namespace DependencyInjection.AntiPatterns +{ + public class ExampleDisposable : IDisposable + { + public void Dispose() => + Console.WriteLine($"Disposed: {GetHashCode(),12}"); + } +} diff --git a/docs/core/extensions/snippets/configuration/di-anti-patterns/Foo.cs b/docs/core/extensions/snippets/configuration/di-anti-patterns/Foo.cs new file mode 100644 index 0000000000000..db18f6bf1ca28 --- /dev/null +++ b/docs/core/extensions/snippets/configuration/di-anti-patterns/Foo.cs @@ -0,0 +1,9 @@ +namespace DependencyInjection.AntiPatterns +{ + public class Foo + { + public Foo(Bar bar) + { + } + } +} diff --git a/docs/core/extensions/snippets/configuration/di-anti-patterns/Program.cs b/docs/core/extensions/snippets/configuration/di-anti-patterns/Program.cs index 7ac5d78c1b128..e2d0b3a44609a 100644 --- a/docs/core/extensions/snippets/configuration/di-anti-patterns/Program.cs +++ b/docs/core/extensions/snippets/configuration/di-anti-patterns/Program.cs @@ -1,5 +1,4 @@ - -using System; +using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -19,19 +18,18 @@ static void Main() static void TransientDisposablesWithoutDispose() { var services = new ServiceCollection(); - services.AddTransient(); - using ServiceProvider serviceProvider = - services.BuildServiceProvider(); + services.AddTransient(); + ServiceProvider serviceProvider = services.BuildServiceProvider(); for (int i = 0; i < 1000; ++ i) { - _ = serviceProvider.GetRequiredService(); + _ = serviceProvider.GetRequiredService(); } - serviceProvider.Dispose(); + // serviceProvider.Dispose(); } - static void DeadLockWithFactories() + static void DeadLockWithAsyncFactory() { var services = new ServiceCollection(); services.AddSingleton(implementationFactory: provider => @@ -42,13 +40,13 @@ static void DeadLockWithFactories() services.AddSingleton(); - using ServiceProvider serviceProvider = - services.BuildServiceProvider(); + using ServiceProvider serviceProvider = services.BuildServiceProvider(); _ = serviceProvider.GetRequiredService(); } static async Task GetBarAsync(IServiceProvider serviceProvider) { + // Emulate asynchronous work operation await Task.Delay(1000); return serviceProvider.GetRequiredService(); @@ -60,8 +58,9 @@ static void CaptiveDependency() services.AddSingleton(); services.AddScoped(); - // using ServiceProvider serviceProvider = services.BuildServiceProvider(); - using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true); + using ServiceProvider serviceProvider = services.BuildServiceProvider(); + // Enable scope validation + // using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true); _ = serviceProvider.GetRequiredService(); } @@ -78,24 +77,8 @@ static void ScopedServiceBecomesSingleton() Bar correct = scope.ServiceProvider.GetRequiredService(); } - // This makes the scoped service a singleton, as it is not within a scope + // Not within a scope, becomes a singleton Bar avoid = serviceProvider.GetRequiredService(); } } - - public class Foo - { - public Foo(Bar bar) - { - } - } - - public class Bar - { - } - - public class SomethingDisposable : IDisposable - { - void IDisposable.Dispose() => Console.WriteLine("Disposed"); - } } From 362533bcda8271dcb497a7fb013fb6720ab239bc Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 29 Oct 2020 11:01:54 -0500 Subject: [PATCH 3/5] Added access-by-line entries, and sorted 'em --- docs/core/extensions/access-by-line.txt | 30 +++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/core/extensions/access-by-line.txt b/docs/core/extensions/access-by-line.txt index ad3a08e3a2b3b..1e0885d8ec6c4 100644 --- a/docs/core/extensions/access-by-line.txt +++ b/docs/core/extensions/access-by-line.txt @@ -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 From 7fdb251440e8c595d0de39005b4a5217f5a47c31 Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 29 Oct 2020 11:38:06 -0500 Subject: [PATCH 4/5] Minor updates --- docs/core/extensions/dependency-injection-guidelines.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/core/extensions/dependency-injection-guidelines.md b/docs/core/extensions/dependency-injection-guidelines.md index 5ae4ea1ac5b8a..8d7123fc5ea2f 100644 --- a/docs/core/extensions/dependency-injection-guidelines.md +++ b/docs/core/extensions/dependency-injection-guidelines.md @@ -205,10 +205,11 @@ For more information, see [Scope validation](dependency-injection.md#scope-valid 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"::: +:::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 , which is correct. The anti-pattern is the retrieval of `Bar` outside of the scope. +In the preceding code, `Bar` is retrieved within an , 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) From b41eacb584e205d80b58bec64debea0d37df1347 Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 29 Oct 2020 11:59:57 -0500 Subject: [PATCH 5/5] Added a sentence describing each disposable instance's intended lifetime --- docs/core/extensions/dependency-injection-guidelines.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/core/extensions/dependency-injection-guidelines.md b/docs/core/extensions/dependency-injection-guidelines.md index 8d7123fc5ea2f..7188b94cba4de 100644 --- a/docs/core/extensions/dependency-injection-guidelines.md +++ b/docs/core/extensions/dependency-injection-guidelines.md @@ -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: