From 50032f13eacbaa08f137c5443d0ee69668874d00 Mon Sep 17 00:00:00 2001 From: martincostello Date: Tue, 3 Jun 2025 19:20:11 +0100 Subject: [PATCH] [MVC.Testing] Fix NullReferenceException Fix `NullReferenceException` accessing `Services` in a web application using top-level programs. Fixes #62224. --- .../Mvc.Testing/src/WebApplicationFactory.cs | 6 +- ...relBasedWebApplicationFactoryForMinimal.cs | 15 +++++ .../RealServerBackedIntegrationTests.cs | 16 ++++- ...erverUsingMinimalBackedIntegrationTests.cs | 62 +++++++++++++++++++ ...alServerWithDynamicPortIntegrationTests.cs | 1 - 5 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 src/Mvc/test/Mvc.FunctionalTests/KestrelBasedWebApplicationFactoryForMinimal.cs create mode 100644 src/Mvc/test/Mvc.FunctionalTests/RealServerUsingMinimalBackedIntegrationTests.cs diff --git a/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs b/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs index 2a9b1a082f30..5e028ac82230 100644 --- a/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs +++ b/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs @@ -103,7 +103,7 @@ public virtual IServiceProvider Services StartServer(); if (_useKestrel) { - return _webHost!.Services; + return _webHost?.Services ?? _host!.Services; } return _host?.Services ?? _server!.Host.Services; @@ -263,8 +263,8 @@ public void StartServer() { var deferredHostBuilder = new DeferredHostBuilder(); deferredHostBuilder.UseEnvironment(Environments.Development); - // There's no helper for UseApplicationName, but we need to - // set the application name to the target entry point + // There's no helper for UseApplicationName, but we need to + // set the application name to the target entry point // assembly name. deferredHostBuilder.ConfigureHostConfiguration(config => { diff --git a/src/Mvc/test/Mvc.FunctionalTests/KestrelBasedWebApplicationFactoryForMinimal.cs b/src/Mvc/test/Mvc.FunctionalTests/KestrelBasedWebApplicationFactoryForMinimal.cs new file mode 100644 index 000000000000..c72f6ac6410f --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/KestrelBasedWebApplicationFactoryForMinimal.cs @@ -0,0 +1,15 @@ +// 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 KestrelBasedWebApplicationFactoryForMinimal : WebApplicationFactory +{ + public KestrelBasedWebApplicationFactoryForMinimal() : base() + { + // Use dynamically assigned port to avoid test conflicts in CI. + this.UseKestrel(0); + } +} diff --git a/src/Mvc/test/Mvc.FunctionalTests/RealServerBackedIntegrationTests.cs b/src/Mvc/test/Mvc.FunctionalTests/RealServerBackedIntegrationTests.cs index 1186e196c84f..28b92a3484b5 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/RealServerBackedIntegrationTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/RealServerBackedIntegrationTests.cs @@ -4,6 +4,8 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc.FunctionalTests; @@ -40,9 +42,6 @@ public async Task RetrievesDataFromRealServer() [Fact] public async Task ServerReachableViaGenericHttpClient() { - // Arrange - var baseAddress = new Uri("http://localhost:5000"); - // Act using var factoryClient = Factory.CreateClient(); using var client = new HttpClient() { BaseAddress = factoryClient.BaseAddress }; @@ -52,4 +51,15 @@ public async Task ServerReachableViaGenericHttpClient() // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + + [Fact] + public void CanResolveServices() + { + // Act + var server = Factory.Services.GetRequiredService(); + + // Assert + Assert.NotNull(server); + Assert.Contains("Kestrel", server.GetType().FullName); + } } diff --git a/src/Mvc/test/Mvc.FunctionalTests/RealServerUsingMinimalBackedIntegrationTests.cs b/src/Mvc/test/Mvc.FunctionalTests/RealServerUsingMinimalBackedIntegrationTests.cs new file mode 100644 index 000000000000..4fba8f30a8c9 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/RealServerUsingMinimalBackedIntegrationTests.cs @@ -0,0 +1,62 @@ +// 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; +using System.Net.Http.Headers; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests; + +public class RealServerUsingMinimalBackedIntegrationTests : IClassFixture +{ + public KestrelBasedWebApplicationFactoryForMinimal Factory { get; } + + public RealServerUsingMinimalBackedIntegrationTests(KestrelBasedWebApplicationFactoryForMinimal factory) + { + Factory = factory; + } + + [Fact] + public async Task RetrievesDataFromRealServer() + { + // Arrange + var expectedMediaType = MediaTypeHeaderValue.Parse("text/plain; 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.Contains("Hello World", responseContent); + } + + [Fact] + public async Task ServerReachableViaGenericHttpClient() + { + // Act + using var factoryClient = Factory.CreateClient(); + using var client = new HttpClient() { BaseAddress = factoryClient.BaseAddress }; + + using var response = await client.GetAsync("/"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public void CanResolveServices() + { + // Act + var server = Factory.Services.GetRequiredService(); + + // Assert + Assert.NotNull(server); + Assert.Contains("Kestrel", server.GetType().FullName); + } +} diff --git a/src/Mvc/test/Mvc.FunctionalTests/RealServerWithDynamicPortIntegrationTests.cs b/src/Mvc/test/Mvc.FunctionalTests/RealServerWithDynamicPortIntegrationTests.cs index b8279ec4846f..b10f81963c82 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/RealServerWithDynamicPortIntegrationTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/RealServerWithDynamicPortIntegrationTests.cs @@ -3,7 +3,6 @@ using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using Microsoft.AspNetCore.Mvc.Testing; namespace Microsoft.AspNetCore.Mvc.FunctionalTests;