dotnet / dotnet/aspnetcore

WebApplicationFactory disposes host before StopAsync on background service is called

Open
#57,738 1 comment 0 reactions 0 assignees View on GitHub
area-mvc feature-mvc-testing
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

Please see the description from https://github.com/dotnet/aspnetcore/issues/50622

TL;DR; when deriving a `BackgroundService` and overriding `StopAsync` this fails with `ObjectDisposedException` for `serviceProvider` during integration testing:

```csharp
public override async Task StopAsync(CancellationToken token)
{
await base.StopAsync(token);

using var scope = serviceProvider.CreateScope();
}
```

Where `serviceProvider` was injected via constructor.

### Expected Behavior

I should be able to resolve services from DI until my stopping is done.

### Steps To Reproduce

See full repro here: https://github.com/lostmsu/ServiceProviderDisposedRepro (upgraded to .NET 8)

### Exceptions (if any)

```
Class cleanup failed in ServiceProviderDisposed.Tests.Tests

System.ObjectDisposedException
Cannot access a disposed object.
Object name: 'IServiceProvider'.
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ThrowHelper.ThrowObjectDisposedException()
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.CreateScope(IServiceProvider provider)
at ServiceProviderDisposed.DummyBackgroundService.StopAsync(CancellationToken cancellationToken) in C:\Users\brome\Projects\Play\Bugs\ServiceProviderDisposedRepro\ServiceProviderDisposed\DummyBackgroundService.cs:line 24
at Microsoft.Extensions.Hosting.Internal.Host.ForeachService[T](IEnumerable`1 services, CancellationToken token, Boolean concurrent, Boolean abortOnFirstException, List`1 exceptions, Func`3 operation)
at Microsoft.Extensions.Hosting.Internal.Host.StopAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.DisposeAsync()
at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.Dispose(Boolean disposing)
at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.Dispose()
at Xunit.Sdk.ExceptionAggregator.Run(Action code) in /_/src/xunit.core/Sdk/ExceptionAggregator.cs:line 73
```

### .NET Version

8.0.401

### Anything else?

The original bug report suggested to implement `IHostedLifecycleService`, but moving

```csharp
using var scope = serviceProvider.CreateScope();
```

to `StoppedAsync` does not prevent the issue.

The scenario I have in mind is to do something after my main service procedure ends (e.g. finalize database recording).

### Workaround

Just found a workaround: you can move the necessary code at the end of

```csharp
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(50);
}
DoYourThingHere();
}
```

The problem with that though is that you lose access to the `CancellationToken` from `StopAsync`: the one in `ExecuteAsync` has completely different purpose.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.