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

Add a new UseKestel API to the WebApplicationFactory #60635

Merged
merged 17 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.TestHost" />
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
<Reference Include="Microsoft.Extensions.DependencyModel" />
<Reference Include="Microsoft.Extensions.Hosting" />
<Reference Include="Microsoft.Extensions.HostFactoryResolver.Sources" />
Expand Down
4 changes: 4 additions & 0 deletions src/Mvc/Mvc.Testing/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
*REMOVED*Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.Server.get -> Microsoft.AspNetCore.TestHost.TestServer!
Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.Server.get -> Microsoft.AspNetCore.TestHost.TestServer?
Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.UseKestrel() -> void
virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.CreateHandler() -> System.Net.Http.HttpMessageHandler!
5 changes: 4 additions & 1 deletion src/Mvc/Mvc.Testing/src/Resources.resx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Expand Down Expand Up @@ -126,4 +126,7 @@
<data name="MissingDepsFile" xml:space="preserve">
<value>Can't find '{0}'. This file is required for functional tests to run properly. There should be a copy of the file on your source project bin folder. If that is not the case, make sure that the property PreserveCompilationContext is set to true on your project file. E.g '&lt;PreserveCompilationContext&gt;true&lt;/PreserveCompilationContext&gt;'. For functional tests to work they need to either run from the build output folder or the {1} file from your application's output directory must be copied to the folder where the tests are running on. A common cause for this error is having shadow copying enabled when the tests run.</value>
</data>
<data name="ServerNotInitialized" xml:space="preserve">
<value>Server hasn't been initialized yet. Plase intialized the server first before trying to create a client.</value>
</data>
</root>
128 changes: 114 additions & 14 deletions src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -24,11 +24,14 @@
/// Typically the Startup or Program classes can be used.</typeparam>
public partial class WebApplicationFactory<TEntryPoint> : IDisposable, IAsyncDisposable where TEntryPoint : class
{
private bool _useKestrel;
private bool _disposed;
private bool _disposedAsync;
private TestServer? _server;
private IHost? _host;
private Action<IWebHostBuilder> _configuration;
private IWebHost? _webHost;
private Uri _webHostAddress;
private readonly List<HttpClient> _clients = new();
private readonly List<WebApplicationFactory<TEntryPoint>> _derivedFactories = new();

Expand All @@ -55,7 +58,7 @@
/// <typeparamref name="TEntryPoint" /> will be loaded as application assemblies.
/// </para>
/// </summary>
public WebApplicationFactory()

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux ARM)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl x64)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux ARM64)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS x64)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux x64)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl ARM64)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl ARM)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS arm64)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: Ubuntu x64)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: macOS)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: macOS)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check failure on line 61 in src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: Ubuntu x64)

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs#L61

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs(61,12): error CS8618: (NETCORE_ENGINEERING_TELEMETRY=Build) Non-nullable field '_webHostAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
{
_configuration = ConfigureWebHost;
}
Expand All @@ -71,7 +74,7 @@
/// <summary>
/// Gets the <see cref="TestServer"/> created by this <see cref="WebApplicationFactory{TEntryPoint}"/>.
/// </summary>
public TestServer Server
public TestServer? Server
{
get
{
Expand All @@ -88,7 +91,12 @@
get
{
EnsureServer();
return _host?.Services ?? _server.Host.Services;
if (_useKestrel)
{
return _webHost!.Services;
}

return _host?.Services ?? _server!.Host.Services;
}
}

Expand Down Expand Up @@ -136,10 +144,24 @@
return factory;
}

[MemberNotNull(nameof(_server))]
/// <summary>
/// Configures the factory to use Kestrel as the server.
/// </summary>
public void UseKestrel()
{
_useKestrel = true;
}

private static IWebHost CreateKestrelServer(IWebHostBuilder builder)
{
var host = builder.UseKestrel().Build();
host.Start();
return host;
}

private void EnsureServer()
{
if (_server != null)
if (_server != null || _webHost != null)
{
return;
}
Expand Down Expand Up @@ -197,21 +219,45 @@
{
SetContentRoot(builder);
_configuration(builder);
_server = CreateServer(builder);
if (_useKestrel)
{
_webHost = CreateKestrelServer(builder);

var serverAddressFeature = _webHost.ServerFeatures.Get<IServerAddressesFeature>();
if (serverAddressFeature?.Addresses.Count > 0)
{
// Store the web host address as it's going to be used every time a client is created to communicate to the server
_webHostAddress = new Uri(serverAddressFeature.Addresses.Last());
ClientOptions.BaseAddress = _webHostAddress;
}
}
else
{
_server = CreateServer(builder);
}
}
}

[MemberNotNull(nameof(_server))]
private void ConfigureHostBuilder(IHostBuilder hostBuilder)
{
hostBuilder.ConfigureWebHost(webHostBuilder =>
{
SetContentRoot(webHostBuilder);
_configuration(webHostBuilder);
webHostBuilder.UseTestServer();
if (!_useKestrel)
{
webHostBuilder.UseTestServer();
}
else
{
webHostBuilder.UseKestrel();
}
});
_host = CreateHost(hostBuilder);
_server = (TestServer)_host.Services.GetRequiredService<IServer>();
if (!_useKestrel)
{
_server = (TestServer)_host.Services.GetRequiredService<IServer>();
}
}

private void SetContentRoot(IWebHostBuilder builder)
Expand Down Expand Up @@ -455,8 +501,19 @@
/// redirects and handles cookies.
/// </summary>
/// <returns>The <see cref="HttpClient"/>.</returns>
public HttpClient CreateClient() =>
CreateClient(ClientOptions);
public HttpClient CreateClient()
{
var client = CreateClient(ClientOptions);

if (_useKestrel)
{
// Have to do this, as the ClientOptions.BaseAddress will be set to poitn to the kestrel server,
// and it may not match the original base address value.
client.BaseAddress = ClientOptions.BaseAddress;
}

return client;
}

/// <summary>
/// Creates an instance of <see cref="HttpClient"/> that automatically follows
Expand All @@ -481,7 +538,19 @@
HttpClient client;
if (handlers == null || handlers.Length == 0)
{
client = _server.CreateClient();
if (_useKestrel)
{
client = new HttpClient();
}
else
{
if (_server is null)
{
throw new InvalidOperationException(Resources.ServerNotInitialized);
}

client = _server.CreateClient();
}
}
else
{
Expand All @@ -490,7 +559,7 @@
handlers[i - 1].InnerHandler = handlers[i];
}

var serverHandler = _server.CreateHandler();
var serverHandler = CreateHandler();
handlers[^1].InnerHandler = serverHandler;

client = new HttpClient(handlers[0]);
Expand All @@ -503,6 +572,25 @@
return client;
}

/// <summary>
/// Creates a custom <see cref="HttpMessageHandler" /> for processing HTTP requests/responses with the test server.
/// </summary>

protected virtual HttpMessageHandler CreateHandler()
{
if (_useKestrel)
{
return new HttpClientHandler();
}

if (_server is null)
{
throw new InvalidOperationException(Resources.ServerNotInitialized);
}

return _server.CreateHandler();
}

/// <summary>
/// Configures <see cref="HttpClient"/> instances created by this <see cref="WebApplicationFactory{TEntryPoint}"/>.
/// </summary>
Expand All @@ -511,7 +599,19 @@
{
ArgumentNullException.ThrowIfNull(client);

client.BaseAddress = new Uri("http://localhost");
if (_useKestrel)
{
if (_webHost is null)
{
throw new InvalidOperationException(Resources.ServerNotInitialized);
}

client.BaseAddress = _webHostAddress;
}
else
{
client.BaseAddress = new Uri("http://localhost");
}
}

/// <summary>
Expand Down
14 changes: 14 additions & 0 deletions src/Mvc/test/Mvc.FunctionalTests/KestrelBasedWapFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Mvc.Testing;

namespace Microsoft.AspNetCore.Mvc.FunctionalTests;

public class KestrelBasedWapFactory : WebApplicationFactory<SimpleWebSite.Startup>
{
public KestrelBasedWapFactory() : base()
{
this.UseKestrel();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net;
using System.Net.Http.Headers;

namespace Microsoft.AspNetCore.Mvc.FunctionalTests;

public class RealServerBackedIntegrationTests : IClassFixture<KestrelBasedWapFactory>
{
public KestrelBasedWapFactory Factory { get; }

public RealServerBackedIntegrationTests(KestrelBasedWapFactory factory)
{
Factory = factory;
}

[Fact]
public async Task RetrievesDataFromRealServer()
{
// Arrange
var expectedMediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8");

// Act
var client = Factory.CreateClient();
var response = await client.GetAsync("/");
var responseContent = await response.Content.ReadAsStringAsync();

// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedMediaType, response.Content.Headers.ContentType);

Assert.Equal(5000, client.BaseAddress.Port);

Assert.Contains("first", responseContent);
Assert.Contains("second", responseContent);
Assert.Contains("wall", responseContent);
Assert.Contains("floor", responseContent);
}
}
Loading