dotnet / dotnet/aspnetcore

UseKestrel(port) silently ignores port parameter on derived WebApplicationFactory from WithWebHostBuilder()

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

Description

## Summary

`WebApplicationFactory.UseKestrel(int port)` correctly applies the port when called on the base factory or a subclass, but silently ignores the port parameter when called on a factory derived via `WithWebHostBuilder()`. The server falls back to Kestrel's default port (5000) instead of using the specified port.

`WithWebHostBuilder` is the [documented approach](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-10.0#customize-the-client-with-withwebhostbuilder) for per-test service customization, including [injecting mock services](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-10.0#inject-mock-services).

## Repro

https://github.com/danroth27/waf-usekestrel-port-repro

**WebApp/Program.cs** — minimal web app:
```csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
```

**Test demonstrating the bug:**
```csharp
[Fact]
public async Task UseKestrel_WithSpecificPort_OnDerivedFactory_PortIsIgnored()
{
var expectedPort = FindFreePort(); // e.g., 54630
await using var factory = new WebApplicationFactory()
.WithWebHostBuilder(_ => { }); // creates DelegatedWebApplicationFactory
factory.UseKestrel(expectedPort);

var server = factory.Services.GetRequiredService();
var addresses = server.Features.Get()!.Addresses;
var actualPort = new Uri(addresses.First()).Port;

Assert.Equal(expectedPort, actualPort); // FAILS: Expected 54630, Actual 5000
}
```

The same code works correctly on the base factory (without `WithWebHostBuilder`):
```csharp
[Fact]
public async Task UseKestrel_WithSpecificPort_OnBaseFactory_Works()
{
var expectedPort = FindFreePort();
await using var factory = new WebApplicationFactory();
factory.UseKestrel(expectedPort);

var actualPort = GetServerPort(factory);

Assert.Equal(expectedPort, actualPort); // PASSES
}
```

## Steps

```bash
git clone https://github.com/danroth27/waf-usekestrel-port-repro
cd waf-usekestrel-port-repro
dotnet test
```

**Expected:** All 3 tests pass — `UseKestrel(port)` applies the port on both base and derived factories.

**Actual:** 2 of 3 tests pass. The test on the base factory passes. The two tests on derived factories (specific port + dynamic port 0) demonstrate that the port parameter is silently ignored.

## Root cause

`TryConfigureServerPort` — the method that applies `_kestrelPort` to `IServerAddressesFeature` — runs inside `CreateHost()`:

```csharp
// WebApplicationFactory.cs
protected virtual IHost CreateHost(IHostBuilder builder)
{
var host = builder.Build();
TryConfigureServerPort(() => GetServerAddressFeature(host)); // reads this._kestrelPort
host.Start();
return host;
}
```

When `WithWebHostBuilder()` creates a `DelegatedWebApplicationFactory`, it captures the **parent** factory's `CreateHost` as a delegate:

```csharp
// DelegatedWebApplicationFactory
protected override IHost CreateHost(IHostBuilder builder) => _createHost(builder);
// _createHost was captured from the parent: `CreateHost` (parent's method)
```

When the derived factory starts the server, `ConfigureHostBuilder` calls `CreateHost(hostBuilder)`, which delegates to the **parent's** `CreateHost`. Inside that method, `TryConfigureServerPort` reads `this._kestrelPort` — but `this` is the **parent** factory instance. Since `UseKestrel(port)` was called on the **derived** factory, the parent's `_kestrelPort` is null, and `TryConfigureServerPort` no-ops:

```csharp
private void TryConfigureServerPort(...)
{
if (_kestrelPort.HasValue) // parent._kestrelPort is null → skips
{ ... }
}
```

Meanwhile, the derived factory's `ConfigureHostBuilder` does check `_useKestrel` (which IS true on the derived factory), so Kestrel is correctly configured as the server — but the port is never applied.

## Workaround

Use the subclass approach (override `ConfigureWebHost`) instead of `WithWebHostBuilder`. On a subclass, `CreateHost` is not delegated, so `TryConfigureServerPort` correctly reads the subclass's `_kestrelPort`:

```csharp
public class CustomWebApplicationFactory : WebApplicationFactory
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Replace services as needed
});
}
}

// Usage:
var factory = new CustomWebApplicationFactory();
factory.UseKestrel(0); // ✅ works — dynamic port correctly assigned
var address = factory.Services.GetRequiredService()
.Features.Get()!.Addresses.First();
```

## Version info

- .NET SDK: 11.0.100-preview.2.26159.112
- `Microsoft.AspNetCore.Mvc.Testing`: 10.0.5 (also affects current `main` — same code path)
- OS: Windows 11

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.