diff --git a/AspNetCore.sln b/AspNetCore.sln index 6d8858e42d93..3caea6eb3817 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1624,6 +1624,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhotinoTestApp", "src\Compo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.WebView.Photino", "src\Components\WebView\Samples\PhotinoPlatform\src\Microsoft.AspNetCore.Components.WebView.Photino.csproj", "{B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleWebSiteWithWebApplicationBuilderException", "src\Mvc\test\WebSites\SimpleWebSiteWithWebApplicationBuilderException\SimpleWebSiteWithWebApplicationBuilderException.csproj", "{5C641396-7E92-4F5C-A5A1-B4CDF480539B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -7731,6 +7733,18 @@ Global {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|x64.Build.0 = Release|Any CPU {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|x86.ActiveCfg = Release|Any CPU {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|x86.Build.0 = Release|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|x64.ActiveCfg = Debug|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|x64.Build.0 = Debug|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|x86.ActiveCfg = Debug|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|x86.Build.0 = Debug|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|Any CPU.Build.0 = Release|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|x64.ActiveCfg = Release|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|x64.Build.0 = Release|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|x86.ActiveCfg = Release|Any CPU + {5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -8535,6 +8549,7 @@ Global {3EC71A0E-6515-4A5A-B759-F0BCF1BCFC56} = {44963D50-8B58-44E6-918D-788BCB406695} {558C46DE-DE16-41D5-8DB7-D6D748E32977} = {3EC71A0E-6515-4A5A-B759-F0BCF1BCFC56} {B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD} = {44963D50-8B58-44E6-918D-788BCB406695} + {5C641396-7E92-4F5C-A5A1-B4CDF480539B} = {088C37A5-30D2-40FB-B031-D163CFBED006} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/src/Mvc/Mvc.Testing/src/DeferredHostBuilder.cs b/src/Mvc/Mvc.Testing/src/DeferredHostBuilder.cs index f84f8e792ca0..1a9fea4d99eb 100644 --- a/src/Mvc/Mvc.Testing/src/DeferredHostBuilder.cs +++ b/src/Mvc/Mvc.Testing/src/DeferredHostBuilder.cs @@ -18,6 +18,10 @@ internal class DeferredHostBuilder : IHostBuilder private Action _configure; private Func? _hostFactory; + // This task represents a call to IHost.Start, we create it here preemptively in case the application + // exits due to an exception or because it didn't wait for the shutdown signal + private readonly TaskCompletionSource _hostStartTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + public DeferredHostBuilder() { _configure = b => @@ -37,7 +41,7 @@ public IHost Build() var host = (IHost)_hostFactory!(Array.Empty()); // We can't return the host directly since we need to defer the call to StartAsync - return new DeferredHost(host); + return new DeferredHost(host, _hostStartTcs); } public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) @@ -81,6 +85,19 @@ public void ConfigureHostBuilder(object hostBuilder) _configure(((IHostBuilder)hostBuilder)); } + public void EntryPointCompleted(Exception? exception) + { + // If the entry point completed we'll set the tcs just in case the application doesn't call IHost.Start/StartAsync. + if (exception is not null) + { + _hostStartTcs.TrySetException(exception); + } + else + { + _hostStartTcs.TrySetResult(); + } + } + public void SetHostFactory(Func hostFactory) { _hostFactory = hostFactory; @@ -89,40 +106,40 @@ public void SetHostFactory(Func hostFactory) private class DeferredHost : IHost, IAsyncDisposable { private readonly IHost _host; + private readonly TaskCompletionSource _hostStartedTcs; - public DeferredHost(IHost host) + public DeferredHost(IHost host, TaskCompletionSource hostStartedTcs) { _host = host; + _hostStartedTcs = hostStartedTcs; } public IServiceProvider Services => _host.Services; public void Dispose() => _host.Dispose(); - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { if (_host is IAsyncDisposable disposable) { - return disposable.DisposeAsync(); + await disposable.DisposeAsync().ConfigureAwait(false); + return; } Dispose(); - return default; } - public Task StartAsync(CancellationToken cancellationToken = default) + public async Task StartAsync(CancellationToken cancellationToken = default) { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - // Wait on the existing host to start running and have this call wait on that. This avoids starting the actual host too early and // leaves the application in charge of calling start. - using var reg = cancellationToken.UnsafeRegister(_ => tcs.TrySetCanceled(), null); + using var reg = cancellationToken.UnsafeRegister(_ => _hostStartedTcs.TrySetCanceled(), null); // REVIEW: This will deadlock if the application creates the host but never calls start. This is mitigated by the cancellationToken // but it's rarely a valid token for Start - _host.Services.GetRequiredService().ApplicationStarted.UnsafeRegister(_ => tcs.TrySetResult(), null); + using var reg2 = _host.Services.GetRequiredService().ApplicationStarted.UnsafeRegister(_ => _hostStartedTcs.TrySetResult(), null); - return tcs.Task; + await _hostStartedTcs.Task.ConfigureAwait(false); } public Task StopAsync(CancellationToken cancellationToken = default) => _host.StopAsync(cancellationToken); diff --git a/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs b/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs index 024f6d62431e..3e3e4df1e056 100644 --- a/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs +++ b/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs @@ -161,7 +161,11 @@ private void EnsureServer() { var deferredHostBuilder = new DeferredHostBuilder(); // This helper call does the hard work to determine if we can fallback to diagnostic source events to get the host instance - var factory = HostFactoryResolver.ResolveHostFactory(typeof(TEntryPoint).Assembly, stopApplication: false, configureHostBuilder: deferredHostBuilder.ConfigureHostBuilder); + var factory = HostFactoryResolver.ResolveHostFactory( + typeof(TEntryPoint).Assembly, + stopApplication: false, + configureHostBuilder: deferredHostBuilder.ConfigureHostBuilder, + entrypointCompleted: deferredHostBuilder.EntryPointCompleted); if (factory is not null) { diff --git a/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj b/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj index 0d7f871ef545..b70a060cae94 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj +++ b/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Mvc/test/Mvc.FunctionalTests/SimpleWithWebApplicationBuilderExceptionTests.cs b/src/Mvc/test/Mvc.FunctionalTests/SimpleWithWebApplicationBuilderExceptionTests.cs new file mode 100644 index 000000000000..baca3e522ad9 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/SimpleWithWebApplicationBuilderExceptionTests.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class SimpleWithWebApplicationBuilderExceptionTests : IClassFixture> + { + private MvcTestFixture _fixture; + + public SimpleWithWebApplicationBuilderExceptionTests(MvcTestFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public void ExceptionThrownFromApplicationCanBeObserved() + { + var ex = Assert.Throws(() => _fixture.CreateClient()); + Assert.Equal("This application failed to start", ex.Message); + } + } +} diff --git a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/FakeEntryPoint.cs b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/FakeEntryPoint.cs new file mode 100644 index 000000000000..94550b75cd98 --- /dev/null +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/FakeEntryPoint.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +/// +/// This is a class we use to reference this assembly statically from tests +/// +namespace SimpleWebSiteWithWebApplicationBuilderException +{ + public class FakeStartup + { + } +} diff --git a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/Program.cs b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/Program.cs new file mode 100644 index 000000000000..a77fa70765ef --- /dev/null +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/Program.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; + +var app = WebApplication.Create(args); + +app.MapGet("/", (Func)(() => "Hello World")); + +throw new InvalidOperationException("This application failed to start"); diff --git a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/Properties/launchSettings.json b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/Properties/launchSettings.json new file mode 100644 index 000000000000..3c71eb74877d --- /dev/null +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51807/", + "sslPort": 44365 + } + }, + "profiles": { + "SimpleWebSite": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/SimpleWebSiteWithWebApplicationBuilderException.csproj b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/SimpleWebSiteWithWebApplicationBuilderException.csproj new file mode 100644 index 000000000000..58eb21f2db2d --- /dev/null +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/SimpleWebSiteWithWebApplicationBuilderException.csproj @@ -0,0 +1,10 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + diff --git a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/readme.md b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/readme.md new file mode 100644 index 000000000000..b1877b5ff61f --- /dev/null +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/readme.md @@ -0,0 +1,4 @@ +SimpleWebSiteWithWebApplicationBuilderException +=== +This sample web project illustrates a minimal site using WebApplicationBuilder that throws in main. +Please build from root (`.\build.cmd` on Windows; `./build.sh` elsewhere) before using this site.