Skip to content

Commit ee415b3

Browse files
committed
Handle more cases with the new entry point pattern
- Handle an exception being thrown from main before start is called and make sure it propagates to the WebApplicationFactory. - Don't hang if the application doesn't call Start before it completes.
1 parent d2ab01b commit ee415b3

10 files changed

+140
-9
lines changed

AspNetCore.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1624,6 +1624,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhotinoTestApp", "src\Compo
16241624
EndProject
16251625
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}"
16261626
EndProject
1627+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleWebSiteWithWebApplicationBuilderException", "src\Mvc\test\WebSites\SimpleWebSiteWithWebApplicationBuilderException\SimpleWebSiteWithWebApplicationBuilderException.csproj", "{5C641396-7E92-4F5C-A5A1-B4CDF480539B}"
1628+
EndProject
16271629
Global
16281630
GlobalSection(SolutionConfigurationPlatforms) = preSolution
16291631
Debug|Any CPU = Debug|Any CPU
@@ -7731,6 +7733,18 @@ Global
77317733
{B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|x64.Build.0 = Release|Any CPU
77327734
{B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|x86.ActiveCfg = Release|Any CPU
77337735
{B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD}.Release|x86.Build.0 = Release|Any CPU
7736+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
7737+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|Any CPU.Build.0 = Debug|Any CPU
7738+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|x64.ActiveCfg = Debug|Any CPU
7739+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|x64.Build.0 = Debug|Any CPU
7740+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|x86.ActiveCfg = Debug|Any CPU
7741+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Debug|x86.Build.0 = Debug|Any CPU
7742+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|Any CPU.ActiveCfg = Release|Any CPU
7743+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|Any CPU.Build.0 = Release|Any CPU
7744+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|x64.ActiveCfg = Release|Any CPU
7745+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|x64.Build.0 = Release|Any CPU
7746+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|x86.ActiveCfg = Release|Any CPU
7747+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B}.Release|x86.Build.0 = Release|Any CPU
77347748
EndGlobalSection
77357749
GlobalSection(SolutionProperties) = preSolution
77367750
HideSolutionNode = FALSE
@@ -8535,6 +8549,7 @@ Global
85358549
{3EC71A0E-6515-4A5A-B759-F0BCF1BCFC56} = {44963D50-8B58-44E6-918D-788BCB406695}
85368550
{558C46DE-DE16-41D5-8DB7-D6D748E32977} = {3EC71A0E-6515-4A5A-B759-F0BCF1BCFC56}
85378551
{B1AA24A4-5E02-4DC1-B57F-6EB03F91E4DD} = {44963D50-8B58-44E6-918D-788BCB406695}
8552+
{5C641396-7E92-4F5C-A5A1-B4CDF480539B} = {088C37A5-30D2-40FB-B031-D163CFBED006}
85388553
EndGlobalSection
85398554
GlobalSection(ExtensibilityGlobals) = postSolution
85408555
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

src/Mvc/Mvc.Testing/src/DeferredHostBuilder.cs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ internal class DeferredHostBuilder : IHostBuilder
1818
private Action<IHostBuilder> _configure;
1919
private Func<string[], object>? _hostFactory;
2020

21+
// This task represents a call to IHost.Start, we create it here preemptively in case the application
22+
// exits due to an exception or because it didn't wait for the shutdown signal
23+
private readonly TaskCompletionSource _hostStartTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
24+
2125
public DeferredHostBuilder()
2226
{
2327
_configure = b =>
@@ -37,7 +41,7 @@ public IHost Build()
3741
var host = (IHost)_hostFactory!(Array.Empty<string>());
3842

3943
// We can't return the host directly since we need to defer the call to StartAsync
40-
return new DeferredHost(host);
44+
return new DeferredHost(host, _hostStartTcs);
4145
}
4246

4347
public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
@@ -81,6 +85,19 @@ public void ConfigureHostBuilder(object hostBuilder)
8185
_configure(((IHostBuilder)hostBuilder));
8286
}
8387

88+
public void EntryPointCompleted(Exception? exception)
89+
{
90+
// If the entry point completed we'll set the tcs just in case the application doesn't call IHost.Start/StartAsync.
91+
if (exception is not null)
92+
{
93+
_hostStartTcs.TrySetException(exception);
94+
}
95+
else
96+
{
97+
_hostStartTcs.TrySetResult();
98+
}
99+
}
100+
84101
public void SetHostFactory(Func<string[], object> hostFactory)
85102
{
86103
_hostFactory = hostFactory;
@@ -89,10 +106,12 @@ public void SetHostFactory(Func<string[], object> hostFactory)
89106
private class DeferredHost : IHost, IAsyncDisposable
90107
{
91108
private readonly IHost _host;
109+
private readonly TaskCompletionSource _hostStartedTcs;
92110

93-
public DeferredHost(IHost host)
111+
public DeferredHost(IHost host, TaskCompletionSource hostStartedTcs)
94112
{
95113
_host = host;
114+
_hostStartedTcs = hostStartedTcs;
96115
}
97116

98117
public IServiceProvider Services => _host.Services;
@@ -109,20 +128,18 @@ public ValueTask DisposeAsync()
109128
return default;
110129
}
111130

112-
public Task StartAsync(CancellationToken cancellationToken = default)
131+
public async Task StartAsync(CancellationToken cancellationToken = default)
113132
{
114-
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
115-
116133
// Wait on the existing host to start running and have this call wait on that. This avoids starting the actual host too early and
117134
// leaves the application in charge of calling start.
118135

119-
using var reg = cancellationToken.UnsafeRegister(_ => tcs.TrySetCanceled(), null);
136+
using var reg = cancellationToken.UnsafeRegister(_ => _hostStartedTcs.TrySetCanceled(), null);
120137

121138
// REVIEW: This will deadlock if the application creates the host but never calls start. This is mitigated by the cancellationToken
122139
// but it's rarely a valid token for Start
123-
_host.Services.GetRequiredService<IHostApplicationLifetime>().ApplicationStarted.UnsafeRegister(_ => tcs.TrySetResult(), null);
140+
using var reg2 = _host.Services.GetRequiredService<IHostApplicationLifetime>().ApplicationStarted.UnsafeRegister(_ => _hostStartedTcs.TrySetResult(), null);
124141

125-
return tcs.Task;
142+
await _hostStartedTcs.Task;
126143
}
127144

128145
public Task StopAsync(CancellationToken cancellationToken = default) => _host.StopAsync(cancellationToken);

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,11 @@ private void EnsureServer()
161161
{
162162
var deferredHostBuilder = new DeferredHostBuilder();
163163
// This helper call does the hard work to determine if we can fallback to diagnostic source events to get the host instance
164-
var factory = HostFactoryResolver.ResolveHostFactory(typeof(TEntryPoint).Assembly, stopApplication: false, configureHostBuilder: deferredHostBuilder.ConfigureHostBuilder);
164+
var factory = HostFactoryResolver.ResolveHostFactory(
165+
typeof(TEntryPoint).Assembly,
166+
stopApplication: false,
167+
configureHostBuilder: deferredHostBuilder.ConfigureHostBuilder,
168+
entrypointCompleted: deferredHostBuilder.EntryPointCompleted);
165169

166170
if (factory is not null)
167171
{

src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<ProjectReference Include="..\WebSites\SecurityWebSite\SecurityWebSite.csproj" />
3939
<ProjectReference Include="..\WebSites\SimpleWebSite\SimpleWebSite.csproj" />
4040
<ProjectReference Include="..\WebSites\SimpleWebSiteWithWebApplicationBuilder\SimpleWebSiteWithWebApplicationBuilder.csproj" />
41+
<ProjectReference Include="..\WebSites\SimpleWebSiteWithWebApplicationBuilderException\SimpleWebSiteWithWebApplicationBuilderException.csproj" />
4142
<ProjectReference Include="..\WebSites\TagHelpersWebSite\TagHelpersWebSite.csproj" />
4243
<ProjectReference Include="..\WebSites\VersioningWebSite\VersioningWebSite.csproj" />
4344
<ProjectReference Include="..\WebSites\XmlFormattersWebSite\XmlFormattersWebSite.csproj" />
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Threading.Tasks;
8+
using Xunit;
9+
10+
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
11+
{
12+
public class SimpleWithWebApplicationBuilderExceptionTests : IClassFixture<MvcTestFixture<SimpleWebSiteWithWebApplicationBuilderException.FakeStartup>>
13+
{
14+
private MvcTestFixture<SimpleWebSiteWithWebApplicationBuilderException.FakeStartup> _fixture;
15+
16+
public SimpleWithWebApplicationBuilderExceptionTests(MvcTestFixture<SimpleWebSiteWithWebApplicationBuilderException.FakeStartup> fixture)
17+
{
18+
_fixture = fixture;
19+
}
20+
21+
[Fact]
22+
public void ExceptionThrownFromApplicationCanBeObserved()
23+
{
24+
var ex = Assert.Throws<InvalidOperationException>(() => _fixture.CreateClient());
25+
Assert.Equal("This application failed to start", ex.Message);
26+
}
27+
}
28+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
6+
/// <summary>
7+
/// This is a class we use to reference this assembly statically from tests
8+
/// </summary>
9+
namespace SimpleWebSiteWithWebApplicationBuilderException
10+
{
11+
public class FakeStartup
12+
{
13+
}
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Builder;
6+
7+
var app = WebApplication.Create(args);
8+
9+
app.MapGet("/", (Func<string>)(() => "Hello World"));
10+
11+
throw new InvalidOperationException("This application failed to start");
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "http://localhost:51807/",
7+
"sslPort": 44365
8+
}
9+
},
10+
"profiles": {
11+
"SimpleWebSite": {
12+
"commandName": "Project",
13+
"launchBrowser": true,
14+
"environmentVariables": {
15+
"ASPNETCORE_ENVIRONMENT": "Development"
16+
},
17+
"applicationUrl": "https://localhost:5001;http://localhost:5000"
18+
},
19+
"IIS Express": {
20+
"commandName": "IISExpress",
21+
"launchBrowser": true,
22+
"environmentVariables": {
23+
"ASPNETCORE_ENVIRONMENT": "Development"
24+
}
25+
}
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<Reference Include="Microsoft.AspNetCore" />
9+
</ItemGroup>
10+
</Project>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
SimpleWebSiteWithWebApplicationBuilderException
2+
===
3+
This sample web project illustrates a minimal site using WebApplicationBuilder that throws in main.
4+
Please build from root (`.\build.cmd` on Windows; `./build.sh` elsewhere) before using this site.

0 commit comments

Comments
 (0)