Skip to content

Commit ed4a123

Browse files
authored
Blazor transient disposables (#18956)
1 parent 48b93bd commit ed4a123

File tree

6 files changed

+355
-0
lines changed

6 files changed

+355
-0
lines changed

aspnetcore/blazor/fundamentals/dependency-injection.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ Prerequisites for constructor injection:
189189

190190
In ASP.NET Core apps, scoped services are typically scoped to the current request. After the request completes, any scoped or transient services are disposed by the DI system. In Blazor Server apps, the request scope lasts for the duration of the client connection, which can result in transient and scoped services living much longer than expected. In Blazor WebAssembly apps, services registered with a scoped lifetime are treated as singletons, so they live longer than scoped services in typical ASP.NET Core apps.
191191

192+
> [!NOTE]
193+
> To detect disposable transient services in an app, see the [Detect transient disposables](#detect-transient-disposables) section.
194+
192195
An approach that limits a service lifetime in Blazor apps is use of the <xref:Microsoft.AspNetCore.Components.OwningComponentBase> type. <xref:Microsoft.AspNetCore.Components.OwningComponentBase> is an abstract type derived from <xref:Microsoft.AspNetCore.Components.ComponentBase> that creates a DI scope corresponding to the lifetime of the component. Using this scope, it's possible to use DI services with a scoped lifetime and have them live as long as the component. When the component is destroyed, services from the component's scoped service provider are disposed as well. This can be useful for services that:
193196

194197
* Should be reused within a component, as the transient lifetime is inappropriate.
@@ -332,6 +335,34 @@ If a single component might use a <xref:Microsoft.EntityFrameworkCore.DbContext>
332335
}
333336
```
334337
338+
## Detect transient disposables
339+
340+
The following examples show how to detect disposable transient services in an app that should use <xref:Microsoft.AspNetCore.Components.OwningComponentBase>. For more information, see the [Utility base component classes to manage a DI scope](#utility-base-component-classes-to-manage-a-di-scope) section.
341+
342+
### Blazor WebAssembly
343+
344+
`DetectIncorrectUsagesOfTransientDisposables.cs`:
345+
346+
[!code-csharp[](dependency-injection/samples_snapshot/3.x/transient-disposables/DetectIncorrectUsagesOfTransientDisposables-wasm.cs)]
347+
348+
The `TransientDisposable` in the following example is detected (`Program.cs`):
349+
350+
[!code-csharp[](dependency-injection/samples_snapshot/3.x/transient-disposables/wasm-program.cs?highlight=6,9,17,22-25)]
351+
352+
### Blazor Server
353+
354+
`DetectIncorrectUsagesOfTransientDisposables.cs`:
355+
356+
[!code-csharp[](dependency-injection/samples_snapshot/3.x/transient-disposables/DetectIncorrectUsagesOfTransientDisposables-server.cs)]
357+
358+
`Program`:
359+
360+
[!code-csharp[](dependency-injection/samples_snapshot/3.x/transient-disposables/server-program.cs?highlight=3)]
361+
362+
The `TransientDependency` in the following example is detected (`Startup.cs`):
363+
364+
[!code-csharp[](dependency-injection/samples_snapshot/3.x/transient-disposables/server-startup.cs?highlight=6-8,11-32)]
365+
335366
## Additional resources
336367
337368
* <xref:fundamentals/dependency-injection>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using System;
2+
using Microsoft.AspNetCore.Components.Server.Circuits;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.DependencyInjection.Extensions;
5+
using Microsoft.Extensions.Hosting;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Microsoft.Extensions.DependencyInjection
9+
{
10+
using BlazorServerTransientDisposable;
11+
12+
public static class WebHostBuilderTransientDisposableExtensions
13+
{
14+
public static IHostBuilder DetectIncorrectUsageOfTransients(
15+
this IHostBuilder builder)
16+
{
17+
builder
18+
.UseServiceProviderFactory(
19+
new DetectIncorrectUsageOfTransientDisposablesServiceFactory())
20+
.ConfigureServices(
21+
s => s.TryAddEnumerable(ServiceDescriptor.Scoped<CircuitHandler,
22+
ThrowOnTransientDisposableHandler>()));
23+
24+
return builder;
25+
}
26+
}
27+
}
28+
29+
namespace BlazorServerTransientDisposable
30+
{
31+
internal class ThrowOnTransientDisposableHandler : CircuitHandler
32+
{
33+
public ThrowOnTransientDisposableHandler(
34+
ThrowOnTransientDisposable throwOnTransientDisposable)
35+
{
36+
throwOnTransientDisposable.ShouldThrow = true;
37+
}
38+
}
39+
40+
public class DetectIncorrectUsageOfTransientDisposablesServiceFactory
41+
: IServiceProviderFactory<IServiceCollection>
42+
{
43+
public IServiceCollection CreateBuilder(IServiceCollection services) =>
44+
services;
45+
46+
public IServiceProvider CreateServiceProvider(
47+
IServiceCollection containerBuilder)
48+
{
49+
var collection = new ServiceCollection();
50+
foreach (var descriptor in containerBuilder)
51+
{
52+
if (descriptor.Lifetime == ServiceLifetime.Transient &&
53+
descriptor.ImplementationType != null &&
54+
typeof(IDisposable).IsAssignableFrom(
55+
descriptor.ImplementationType))
56+
{
57+
collection.Add(CreatePatchedDescriptor(descriptor));
58+
}
59+
else if (descriptor.Lifetime == ServiceLifetime.Transient &&
60+
descriptor.ImplementationFactory != null)
61+
{
62+
collection.Add(CreatePatchedFactoryDescriptor(descriptor));
63+
}
64+
else
65+
{
66+
collection.Add(descriptor);
67+
}
68+
}
69+
70+
collection.AddScoped<ThrowOnTransientDisposable>();
71+
72+
return collection.BuildServiceProvider();
73+
}
74+
75+
private ServiceDescriptor CreatePatchedFactoryDescriptor(
76+
ServiceDescriptor original)
77+
{
78+
var newDescriptor = new ServiceDescriptor(
79+
original.ServiceType,
80+
(sp) =>
81+
{
82+
var originalFactory = original.ImplementationFactory;
83+
var originalResult = originalFactory(sp);
84+
85+
var throwOnTransientDisposable =
86+
sp.GetRequiredService<ThrowOnTransientDisposable>();
87+
if (throwOnTransientDisposable.ShouldThrow &&
88+
originalResult is IDisposable d)
89+
{
90+
throw new InvalidOperationException("Trying to resolve " +
91+
$"transient disposable service {d.GetType().Name} in " +
92+
"the wrong scope. Use an 'OwningComponentBase<T>' " +
93+
"component base class for the service 'T' you are " +
94+
"trying to resolve.");
95+
}
96+
97+
return originalResult;
98+
},
99+
original.Lifetime);
100+
101+
return newDescriptor;
102+
}
103+
104+
private ServiceDescriptor CreatePatchedDescriptor(
105+
ServiceDescriptor original)
106+
{
107+
var newDescriptor = new ServiceDescriptor(
108+
original.ServiceType,
109+
(sp) => {
110+
var throwOnTransientDisposable =
111+
sp.GetRequiredService<ThrowOnTransientDisposable>();
112+
if (throwOnTransientDisposable.ShouldThrow)
113+
{
114+
throw new InvalidOperationException("Trying to resolve " +
115+
"transient disposable service " +
116+
$"{original.ImplementationType.Name} in the wrong " +
117+
"scope. Use an 'OwningComponentBase<T>' component " +
118+
"base class for the service 'T' you are trying to " +
119+
"resolve.");
120+
}
121+
122+
return ActivatorUtilities.CreateInstance(sp,
123+
original.ImplementationType);
124+
},
125+
ServiceLifetime.Transient);
126+
return newDescriptor;
127+
}
128+
}
129+
130+
internal class ThrowOnTransientDisposable
131+
{
132+
public bool ShouldThrow { get; set; }
133+
}
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using System;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.DependencyInjection.Extensions;
4+
5+
namespace Microsoft.Extensions.DependencyInjection
6+
{
7+
using BlazorServerTransientDisposable;
8+
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
9+
10+
public static class WebHostBuilderTransientDisposableExtensions
11+
{
12+
public static WebAssemblyHostBuilder DetectIncorrectUsageOfTransients(
13+
this WebAssemblyHostBuilder builder)
14+
{
15+
builder
16+
.ConfigureContainer(
17+
new DetectIncorrectUsageOfTransientDisposablesServiceFactory());
18+
19+
return builder;
20+
}
21+
22+
public static WebAssemblyHost EnableTransientDisposableDetection(
23+
this WebAssemblyHost webAssemblyHost)
24+
{
25+
webAssemblyHost.Services
26+
.GetRequiredService<ThrowOnTransientDisposable>().ShouldThrow = true;
27+
return webAssemblyHost;
28+
}
29+
}
30+
}
31+
32+
namespace BlazorServerTransientDisposable
33+
{
34+
public class DetectIncorrectUsageOfTransientDisposablesServiceFactory
35+
: IServiceProviderFactory<IServiceCollection>
36+
{
37+
public IServiceCollection CreateBuilder(IServiceCollection services) =>
38+
services;
39+
40+
public IServiceProvider CreateServiceProvider(
41+
IServiceCollection containerBuilder)
42+
{
43+
var collection = new ServiceCollection();
44+
foreach (var descriptor in containerBuilder)
45+
{
46+
if (descriptor.Lifetime == ServiceLifetime.Transient &&
47+
descriptor.ImplementationType != null &&
48+
typeof(IDisposable).IsAssignableFrom(
49+
descriptor.ImplementationType))
50+
{
51+
collection.Add(CreatePatchedDescriptor(descriptor));
52+
}
53+
else if (descriptor.Lifetime == ServiceLifetime.Transient &&
54+
descriptor.ImplementationFactory != null)
55+
{
56+
collection.Add(CreatePatchedFactoryDescriptor(descriptor));
57+
}
58+
else
59+
{
60+
collection.Add(descriptor);
61+
}
62+
}
63+
64+
collection.AddScoped<ThrowOnTransientDisposable>();
65+
66+
return collection.BuildServiceProvider();
67+
}
68+
69+
private ServiceDescriptor CreatePatchedFactoryDescriptor(
70+
ServiceDescriptor original)
71+
{
72+
var newDescriptor = new ServiceDescriptor(
73+
original.ServiceType,
74+
(sp) =>
75+
{
76+
var originalFactory = original.ImplementationFactory;
77+
var originalResult = originalFactory(sp);
78+
79+
var throwOnTransientDisposable =
80+
sp.GetRequiredService<ThrowOnTransientDisposable>();
81+
if (throwOnTransientDisposable.ShouldThrow &&
82+
originalResult is IDisposable d)
83+
{
84+
throw new InvalidOperationException("Trying to resolve " +
85+
$"transient disposable service {d.GetType().Name} in " +
86+
"the wrong scope. Use an 'OwningComponentBase<T>' " +
87+
"component base class for the service 'T' you are " +
88+
"trying to resolve.");
89+
}
90+
91+
return originalResult;
92+
},
93+
original.Lifetime);
94+
95+
return newDescriptor;
96+
}
97+
98+
private ServiceDescriptor CreatePatchedDescriptor(ServiceDescriptor original)
99+
{
100+
var newDescriptor = new ServiceDescriptor(
101+
original.ServiceType,
102+
(sp) => {
103+
var throwOnTransientDisposable =
104+
sp.GetRequiredService<ThrowOnTransientDisposable>();
105+
if (throwOnTransientDisposable.ShouldThrow)
106+
{
107+
throw new InvalidOperationException("Trying to resolve " +
108+
"transient disposable service " +
109+
$"{original.ImplementationType.Name} in the wrong " +
110+
"scope. Use an 'OwningComponentBase<T>' component base " +
111+
"class for the service 'T' you are trying to resolve.");
112+
}
113+
114+
return ActivatorUtilities.CreateInstance(sp,
115+
original.ImplementationType);
116+
},
117+
ServiceLifetime.Transient);
118+
return newDescriptor;
119+
}
120+
}
121+
122+
internal class ThrowOnTransientDisposable
123+
{
124+
public bool ShouldThrow { get; set; }
125+
}
126+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
public static IHostBuilder CreateHostBuilder(string[] args) =>
2+
Host.CreateDefaultBuilder(args)
3+
.DetectIncorrectUsageOfTransients()
4+
.ConfigureWebHostDefaults(webBuilder =>
5+
{
6+
webBuilder.UseStartup<Startup>();
7+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
public void ConfigureServices(IServiceCollection services)
2+
{
3+
services.AddRazorPages();
4+
services.AddServerSideBlazor();
5+
services.AddSingleton<WeatherForecastService>();
6+
services.AddTransient<TransientDependency>();
7+
services.AddTransient<ITransitiveTransientDisposableDependency,
8+
TransitiveTransientDisposableDependency>();
9+
}
10+
11+
public class TransitiveTransientDisposableDependency
12+
: ITransitiveTransientDisposableDependency, IDisposable
13+
{
14+
public void Dispose() { }
15+
}
16+
17+
public interface ITransitiveTransientDisposableDependency
18+
{
19+
}
20+
21+
public class TransientDependency
22+
{
23+
private readonly ITransitiveTransientDisposableDependency
24+
_transitiveTransientDisposableDependency;
25+
26+
public TransientDependency(ITransitiveTransientDisposableDependency
27+
transitiveTransientDisposableDependency)
28+
{
29+
_transitiveTransientDisposableDependency =
30+
transitiveTransientDisposableDependency;
31+
}
32+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
public class Program
2+
{
3+
public static async Task Main(string[] args)
4+
{
5+
var builder = WebAssemblyHostBuilder.CreateDefault(args);
6+
builder.DetectIncorrectUsageOfTransients();
7+
builder.RootComponents.Add<App>("app");
8+
9+
builder.Services.AddTransient<TransientDisposable>();
10+
builder.Services.AddScoped(sp =>
11+
new HttpClient
12+
{
13+
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
14+
});
15+
16+
var host = builder.Build();
17+
host.EnableTransientDisposableDetection();
18+
await host.RunAsync();
19+
}
20+
}
21+
22+
public class TransientDisposable : IDisposable
23+
{
24+
public void Dispose() => throw new NotImplementedException();
25+
}

0 commit comments

Comments
 (0)