dotnet / dotnet/aspnetcore

WebApplicationFactory.WithWebHostBuilder isn't thread-safe — concurrent calls corrupt _derivedFactories and crash on dispose

Open
#67,223 0 comments 0 reactions 0 assignees View on GitHub
area-mvc
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

### Describe the bug

`WithWebHostBuilder` appends the new derived factory to the parent's private `_derivedFactories` `List`, and `Dispose`/`DisposeAsync` later enumerate that same list. Neither access is synchronized.

Calling `WithWebHostBuilder` concurrently on one factory races the `List.Add` — the backing array can tear during a resize, leaving a `null` slot (or dropping entries). It usually goes unnoticed until the parent is disposed and `DisposeAsyncCore` walks the list:

```csharp
foreach (var factory in _derivedFactories)
{
await ((IAsyncDisposable)factory).DisposeAsync(); // factory is null -> NRE
}
```

```
System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.DisposeAsync()
```

### Why we hit this

We maintain a test framework (TUnit) with a parallel ASP.NET Core integration-testing helper. The isolation pattern is: one shared root `WebApplicationFactory` per session, and each test derives its own per-test factory via `WithWebHostBuilder(...)` to layer per-test service overrides. With tests running in parallel, many `WithWebHostBuilder` calls hit the same root at once — exactly this race. Being a race, it surfaces as an intermittent NRE at teardown rather than a clear failure.

We've worked around it by serializing the call with a lock, but deriving per-test factories from a shared root is a natural way to parallelize, so it seems worth fixing (or documenting) upstream.

### Repro

```csharp
var root = new WebApplicationFactory();

await Parallel.ForEachAsync(Enumerable.Range(0, 64), (_, _) =>
{
root.WithWebHostBuilder(_ => { });
return ValueTask.CompletedTask;
});

await root.DisposeAsync(); // intermittently throws NullReferenceException
```

### Expected behavior

Either `WithWebHostBuilder` (and the dispose enumeration) is safe to call concurrently, or the docs state a single factory must not be derived from concurrently.

### Version

`Microsoft.AspNetCore.Mvc.Testing` 10.0.9; same unsynchronized `List` on `main` and back through net8.

Related: #32329 — also touches `_derivedFactories`, but that's nested factories not disposing; separate issue.

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.