Преглед изворни кода

Handle more cases with the new entry point pattern (#33500)

* 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.
David Fowler пре 4 година
родитељ
комит
746b9f82fb

+ 15 - 0
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}

+ 28 - 11
src/Mvc/Mvc.Testing/src/DeferredHostBuilder.cs

@@ -18,6 +18,10 @@ namespace Microsoft.AspNetCore.Mvc.Testing
         private Action<IHostBuilder> _configure;
         private Func<string[], object>? _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 @@ namespace Microsoft.AspNetCore.Mvc.Testing
             var host = (IHost)_hostFactory!(Array.Empty<string>());
 
             // 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<HostBuilderContext, IConfigurationBuilder> configureDelegate)
@@ -81,6 +85,19 @@ namespace Microsoft.AspNetCore.Mvc.Testing
             _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<string[], object> hostFactory)
         {
             _hostFactory = hostFactory;
@@ -89,40 +106,40 @@ namespace Microsoft.AspNetCore.Mvc.Testing
         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<IHostApplicationLifetime>().ApplicationStarted.UnsafeRegister(_ => tcs.TrySetResult(), null);
+                using var reg2 = _host.Services.GetRequiredService<IHostApplicationLifetime>().ApplicationStarted.UnsafeRegister(_ => _hostStartedTcs.TrySetResult(), null);
 
-                return tcs.Task;
+                await _hostStartedTcs.Task.ConfigureAwait(false);
             }
 
             public Task StopAsync(CancellationToken cancellationToken = default) => _host.StopAsync(cancellationToken);

+ 5 - 1
src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

@@ -161,7 +161,11 @@ namespace Microsoft.AspNetCore.Mvc.Testing
             {
                 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)
                 {

+ 1 - 0
src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj

@@ -38,6 +38,7 @@
     <ProjectReference Include="..\WebSites\SecurityWebSite\SecurityWebSite.csproj" />
     <ProjectReference Include="..\WebSites\SimpleWebSite\SimpleWebSite.csproj" />
     <ProjectReference Include="..\WebSites\SimpleWebSiteWithWebApplicationBuilder\SimpleWebSiteWithWebApplicationBuilder.csproj" />
+    <ProjectReference Include="..\WebSites\SimpleWebSiteWithWebApplicationBuilderException\SimpleWebSiteWithWebApplicationBuilderException.csproj" />
     <ProjectReference Include="..\WebSites\TagHelpersWebSite\TagHelpersWebSite.csproj" />
     <ProjectReference Include="..\WebSites\VersioningWebSite\VersioningWebSite.csproj" />
     <ProjectReference Include="..\WebSites\XmlFormattersWebSite\XmlFormattersWebSite.csproj" />

+ 28 - 0
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<MvcTestFixture<SimpleWebSiteWithWebApplicationBuilderException.FakeStartup>>
+    {
+        private MvcTestFixture<SimpleWebSiteWithWebApplicationBuilderException.FakeStartup> _fixture;
+
+        public SimpleWithWebApplicationBuilderExceptionTests(MvcTestFixture<SimpleWebSiteWithWebApplicationBuilderException.FakeStartup> fixture)
+        {
+            _fixture = fixture;
+        }
+
+        [Fact]
+        public void ExceptionThrownFromApplicationCanBeObserved()
+        {
+            var ex = Assert.Throws<InvalidOperationException>(() => _fixture.CreateClient());
+            Assert.Equal("This application failed to start", ex.Message);
+        }
+    }
+}

+ 14 - 0
src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/FakeEntryPoint.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+/// <summary>
+/// This is a class we use to reference this assembly statically from tests
+/// </summary>
+namespace SimpleWebSiteWithWebApplicationBuilderException
+{
+    public class FakeStartup
+    {
+    }
+}

+ 11 - 0
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<string>)(() => "Hello World"));
+
+throw new InvalidOperationException("This application failed to start");

+ 27 - 0
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"
+      }
+    }
+  }
+}

+ 10 - 0
src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilderException/SimpleWebSiteWithWebApplicationBuilderException.csproj

@@ -0,0 +1,10 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore" />
+  </ItemGroup>
+</Project>

+ 4 - 0
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.