Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update transient services guidance #31705

Merged
merged 4 commits into from
Feb 6, 2024
Merged
Changes from 3 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
299 changes: 167 additions & 132 deletions aspnetcore/blazor/fundamentals/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,15 +317,19 @@ public IMyService MyService { get; set; }

## Utility base component classes to manage a DI scope

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. Server-side, the request scope lasts for the duration of the client connection, which can result in transient and scoped services living much longer than expected. Client-side, services registered with a scoped lifetime are treated as singletons, so they live longer than scoped services in typical ASP.NET Core apps.
In non-Blazor ASP.NET Core apps, scoped and transient services are typically scoped to the current request. After the request completes, scoped and transient services are disposed by the DI system.

In interactive server-side Blazor apps, the request scope lasts for the duration of the circuit (the SignalR connection between the client and server), which can result in scoped and transient services living much longer than expected. Therefore, direct use of scoped services should be avoided, and transient services shouldn't be registered or used at all. An alternative approach based on the <xref:Microsoft.AspNetCore.Components.OwningComponentBase> type is described later in this section.

Even in client-side Blazor apps that don't operate over a circuit, services registered with a scoped lifetime are treated as singletons, so they live longer than scoped services in typical ASP.NET Core apps. Client-side transient services can also live longer than expected because there's no request-response-based lifetime to trigger DI system disposal of transient services. Although long-lived transient services are of greater concern on the server, they should generally be avoided as client service registrations as well. Use of the <xref:Microsoft.AspNetCore.Components.OwningComponentBase> type is also recommended for client-side services to control service lifetime.

> [!NOTE]
> To detect disposable transient services in an app, see the following sections:
> To detect disposable transient services in an app, see the following sections later in this article:
>
> [Detect client-side transient disposables](#detect-client-side-transient-disposables)
> [Detect server-side transient disposables](#detect-server-side-transient-disposables)

An approach that limits a service lifetime 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:
An approach that limits a service lifetime 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:

* Should be reused within a component, as the transient lifetime is inappropriate.
* Shouldn't be shared across components, as the singleton lifetime is inappropriate.
Expand Down Expand Up @@ -470,11 +474,9 @@ For more information, see <xref:blazor/blazor-ef-core>.

## Detect client-side transient disposables

The following Blazor WebAssembly example shows how to detect client-side 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.

`DetectIncorrectUsagesOfTransientDisposables.cs` for client-side development:
The following Blazor WebAssembly example shows how to detect client-side disposable transient services in an app that should use <xref:Microsoft.AspNetCore.Components.OwningComponentBase>. This approach is useful if you have any concern that developers working on a Blazor app in the future register and consume one or more transient disposable services in the app. For more information, see the [Utility base component classes to manage a DI scope](#utility-base-component-classes-to-manage-a-di-scope) section.

<!-- UPDATE 8.0 Do we need to see if the code works in the client of a BWA? -->
`DetectIncorrectUsagesOfTransientDisposables.cs`:

:::moniker range=">= aspnetcore-8.0"

Expand Down Expand Up @@ -506,108 +508,126 @@ The following Blazor WebAssembly example shows how to detect client-side disposa

:::moniker-end

`TransientDisposable.cs`:
The following service doesn't require implementing any service features merely to demonstrate how transient services are detected with the approach in this section.

```csharp
public class TransientDisposable : IDisposable
{
public void Dispose() => throw new NotImplementedException();
}
```
`Services/TransientDisposableService.cs`:

The `TransientDisposable` in the following example is detected.
:::moniker range=">= aspnetcore-8.0"

In the `Program` file of a Blazor WebAssembly app:
:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_WebAssembly/Services/TransientDisposableService.cs":::

:::moniker range=">= aspnetcore-6.0"
:::moniker-end

```csharp
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorWebAssemblyTransientDisposable;
:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.DetectIncorrectUsageOfTransients();
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Services/TransientDisposableService.cs":::

builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped(sp =>
new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
:::moniker-end

var host = builder.Build();
host.EnableTransientDisposableDetection();
await host.RunAsync();
```
:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0"

:::code language="csharp" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Services/TransientDisposableService.cs":::

:::moniker-end

:::moniker range="< aspnetcore-6.0"
:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0"

```csharp
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.DetectIncorrectUsageOfTransients();
builder.RootComponents.Add<App>("#app");
:::code language="csharp" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Services/TransientDisposableService.cs":::

builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped(sp =>
new HttpClient
{
BaseAddress = new(builder.HostEnvironment.BaseAddress)
});
:::moniker-end

var host = builder.Build();
host.EnableTransientDisposableDetection();
await host.RunAsync();
}
}
:::moniker range="< aspnetcore-5.0"

public class TransientDisposable : IDisposable
{
public void Dispose() => throw new NotImplementedException();
}
```
:::code language="csharp" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Services/TransientDisposableService.cs":::

:::moniker-end

The preceding example sets the base address with `builder.HostEnvironment.BaseAddress` (<xref:Microsoft.AspNetCore.Components.WebAssembly.Hosting.IWebAssemblyHostEnvironment.BaseAddress%2A?displayProperty=nameWithType>), which gets the base address for the app and is typically derived from the `<base>` tag's `href` value in the host page.
The `TransientDisposableService` in the following example is detected.

In the `Program` file of the Blazor WebAssembly app:

* Add or confirm the presence of the app's `Services` namespace:

```csharp
using BlazorSample.Services;
```

* Immediately after the `builder` is assigned from <xref:Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder.CreateDefault%2A?displayProperty=nameWithType>:

```csharp
builder.DetectIncorrectUsageOfTransients();
```

* Where services are registered:

```csharp
builder.Services.AddTransient<TransientDisposableService>();
```

* Remove the line that builds and runs the host:

```diff
- await builder.Build().RunAsync();
```

Replace the line with the following code, which calls `EnableTransientDisposableDetection` in the processing pipeline of the app:

```csharp
var host = builder.Build();
host.EnableTransientDisposableDetection();
await host.RunAsync();
```

The app can register transient disposables without throwing an exception. However, attempting to resolve a transient disposable results in an <xref:System.InvalidOperationException>, as the following example shows.

`TransientExample.razor`:
`Pages/TransientService.razor`:

```razor
@page "/transient-example"
@inject TransientDisposable TransientDisposable
:::moniker range=">= aspnetcore-8.0"

<h1>Transient Disposable Detection</h1>
```
:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_WebAssembly/Pages/TransientService.razor":::

Navigate to the `TransientExample` component at `/transient-example` and an <xref:System.InvalidOperationException> is thrown when the framework attempts to construct an instance of `TransientDisposable`:
:::moniker-end

:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"

> System.InvalidOperationException: Trying to resolve transient disposable service TransientDisposable in the wrong scope. Use an 'OwningComponentBase\<T>' component base class for the service 'T' you are trying to resolve.
:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_WebAssembly/Pages/dependency-injection/TransientService.razor":::

:::moniker-end

:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0"

:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_WebAssembly/Pages/dependency-injection/TransientService.razor":::

:::moniker-end

:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0"

:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_WebAssembly/Pages/dependency-injection/TransientService.razor":::

:::moniker-end

:::moniker range="< aspnetcore-5.0"

:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_WebAssembly/Pages/dependency-injection/TransientService.razor":::

:::moniker-end

Navigate to the `TransientService` component at `/transient-service` and an <xref:System.InvalidOperationException> is thrown when the framework attempts to construct an instance of `TransientDisposableService`:

> System.InvalidOperationException: Trying to resolve transient disposable service TransientDisposableService in the wrong scope. Use an 'OwningComponentBase\<T>' component base class for the service 'T' you are trying to resolve.

> [!NOTE]
> Transient service registrations for <xref:System.Net.Http.IHttpClientFactory> handlers are recommended. The `TransientExample` component in this section indicates the following transient disposables client-side that use authentication, which is expected:
> Transient service registrations for <xref:System.Net.Http.IHttpClientFactory> handlers are recommended. If the app contains <xref:System.Net.Http.IHttpClientFactory> handlers and uses the <xref:Microsoft.Extensions.DependencyInjection.IRemoteAuthenticationBuilder%602> to add support for authentication, the following transient disposables for client-side authentication are also discovered, which is expected and can be ignored:
>
> * <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.BaseAddressAuthorizationMessageHandler>
> * <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.AuthorizationMessageHandler>

## Detect server-side transient disposables

The following example shows how to detect server-side 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.
The following example shows how to detect server-side disposable transient services in an app that should use <xref:Microsoft.AspNetCore.Components.OwningComponentBase>. This approach is useful if you have any concern that developers working on a Blazor app in the future register and consume one or more transient disposable services in the app. For more information, see the [Utility base component classes to manage a DI scope](#utility-base-component-classes-to-manage-a-di-scope) section.

`DetectIncorrectUsagesOfTransientDisposables.cs`:

<!-- UPDATE 8.0 Confirm that it works in a BWA -->

:::moniker range=">= aspnetcore-8.0"

:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/DetectIncorrectUsagesOfTransientDisposables.cs":::
Expand Down Expand Up @@ -638,32 +658,45 @@ The following example shows how to detect server-side disposable transient servi

:::moniker-end

:::moniker range=">= aspnetcore-8.0"

`Services/TransitiveTransientDisposableDependency.cs`:

:::code language="csharp" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Services/TransitiveTransientDisposableDependency.cs":::

:::moniker-end

:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"

`TransitiveTransientDisposableDependency.cs`:

```csharp
public class TransitiveTransientDisposableDependency
: ITransitiveTransientDisposableDependency, IDisposable
{
public void Dispose() { }
}
:::code language="csharp" source="~/../blazor-samples/7.0/BlazorSample_Server/dependency-injection/TransitiveTransientDisposableDependency.cs":::

public interface ITransitiveTransientDisposableDependency
{
}
:::moniker-end

public class TransientDependency
{
private readonly ITransitiveTransientDisposableDependency
transitiveTransientDisposableDependency;
:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0"

public TransientDependency(ITransitiveTransientDisposableDependency
transitiveTransientDisposableDependency)
{
this.transitiveTransientDisposableDependency =
transitiveTransientDisposableDependency;
}
}
```
`TransitiveTransientDisposableDependency.cs`:

:::code language="csharp" source="~/../blazor-samples/6.0/BlazorSample_Server/dependency-injection/TransitiveTransientDisposableDependency.cs":::

:::moniker-end

:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0"

`TransitiveTransientDisposableDependency.cs`:

:::code language="csharp" source="~/../blazor-samples/5.0/BlazorSample_Server/dependency-injection/TransitiveTransientDisposableDependency.cs":::

:::moniker-end

:::moniker range="< aspnetcore-5.0"

`TransitiveTransientDisposableDependency.cs`:

:::code language="csharp" source="~/../blazor-samples/3.1/BlazorSample_Server/dependency-injection/TransitiveTransientDisposableDependency.cs":::

:::moniker-end

The `TransientDependency` in the following example is detected.

Expand All @@ -682,57 +715,59 @@ builder.Services.AddTransient<ITransitiveTransientDisposableDependency,

:::moniker range="< aspnetcore-6.0"

In `Startup.cs`:
In `Startup.cs` where services are registered in `ConfigureServices`:

```csharp
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.AddTransient<TransientDependency>();
services.AddTransient<ITransitiveTransientDisposableDependency,
TransitiveTransientDisposableDependency>();
}
services.AddTransient<TransientDependency>();
services.AddTransient<ITransitiveTransientDisposableDependency,
TransitiveTransientDisposableDependency>();
```

public class TransitiveTransientDisposableDependency
: ITransitiveTransientDisposableDependency, IDisposable
{
public void Dispose() { }
}
:::moniker-end

public interface ITransitiveTransientDisposableDependency
{
}
The app can register transient disposables without throwing an exception. However, attempting to resolve a transient disposable results in an <xref:System.InvalidOperationException>, as the following example shows.

public class TransientDependency
{
private readonly ITransitiveTransientDisposableDependency
_transitiveTransientDisposableDependency;
:::moniker range=">= aspnetcore-8.0"

public TransientDependency(ITransitiveTransientDisposableDependency
transitiveTransientDisposableDependency)
{
_transitiveTransientDisposableDependency =
transitiveTransientDisposableDependency;
}
}
```
`Components/Pages/TransientService.razor`:

:::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/TransientService.razor":::

:::moniker-end

The app can register transient disposables without throwing an exception. However, attempting to resolve a transient disposable results in an <xref:System.InvalidOperationException>, as the following example shows.
:::moniker range=">= aspnetcore-7.0 < aspnetcore-8.0"

`TransientExample.razor`:
`Pages/TransientService.razor`:

```razor
@page "/transient-example"
@inject TransientDependency TransientDependency
:::code language="razor" source="~/../blazor-samples/7.0/BlazorSample_Server/Pages/dependency-injection/TransientService.razor":::

<h1>Transient Disposable Detection</h1>
```
:::moniker-end

:::moniker range=">= aspnetcore-6.0 < aspnetcore-7.0"

`Pages/TransientService.razor`:

:::code language="razor" source="~/../blazor-samples/6.0/BlazorSample_Server/Pages/dependency-injection/TransientService.razor":::

:::moniker-end

:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0"

`Pages/TransientService.razor`:

:::code language="razor" source="~/../blazor-samples/5.0/BlazorSample_Server/Pages/dependency-injection/TransientService.razor":::

:::moniker-end

:::moniker range="< aspnetcore-5.0"

`Pages/TransientService.razor`:

:::code language="razor" source="~/../blazor-samples/3.1/BlazorSample_Server/Pages/dependency-injection/TransientService.razor":::

:::moniker-end

Navigate to the `TransientExample` component at `/transient-example` and an <xref:System.InvalidOperationException> is thrown when the framework attempts to construct an instance of `TransientDependency`:
Navigate to the `TransientService` component at `/transient-service` and an <xref:System.InvalidOperationException> is thrown when the framework attempts to construct an instance of `TransientDependency`:

> System.InvalidOperationException: Trying to resolve transient disposable service TransientDependency in the wrong scope. Use an 'OwningComponentBase\<T>' component base class for the service 'T' you are trying to resolve.

Expand Down