Skip to content

Handle more cases with the new entry point pattern #33500

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

Merged
merged 4 commits into from
Jun 18, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to create a new test project for each of these scenarios or is there a way for us to have one test project with multiple apps for each scenario (app runs fine, app throws exception, etc)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could have a single app that changed behavior based on arguments.

EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
39 changes: 28 additions & 11 deletions src/Mvc/Mvc.Testing/src/DeferredHostBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ internal class DeferredHostBuilder : IHostBuilder
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 =>
Expand All @@ -37,7 +41,7 @@ public IHost Build()
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)
Expand Down Expand Up @@ -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<string[], object> hostFactory)
{
_hostFactory = hostFactory;
Expand All @@ -89,40 +106,40 @@ public void SetHostFactory(Func<string[], object> 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<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);
Expand Down
6 changes: 5 additions & 1 deletion src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
}
}
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -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.