diff --git a/AspNetCore.sln b/AspNetCore.sln index 52704941e80e..6d8858e42d93 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1612,6 +1612,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebView.Test", "src\Components\WebView\WebView\test\Microsoft.AspNetCore.Components.WebView.Test.csproj", "{4BD6F0DB-BE9C-4C54-B52A-D20B88855ED5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleWebSiteWithWebApplicationBuilder", "src\Mvc\test\WebSites\SimpleWebSiteWithWebApplicationBuilder\SimpleWebSiteWithWebApplicationBuilder.csproj", "{6CCCF618-2E70-4870-B39F-32C016FE08F0}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{B730328F-D9E9-4EAA-B28E-4631A14095F9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PhotinoPlatform", "PhotinoPlatform", "{44963D50-8B58-44E6-918D-788BCB406695}" @@ -7693,6 +7695,18 @@ Global {4BD6F0DB-BE9C-4C54-B52A-D20B88855ED5}.Release|x64.Build.0 = Release|Any CPU {4BD6F0DB-BE9C-4C54-B52A-D20B88855ED5}.Release|x86.ActiveCfg = Release|Any CPU {4BD6F0DB-BE9C-4C54-B52A-D20B88855ED5}.Release|x86.Build.0 = Release|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Debug|x64.ActiveCfg = Debug|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Debug|x64.Build.0 = Debug|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Debug|x86.ActiveCfg = Debug|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Debug|x86.Build.0 = Debug|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Release|Any CPU.Build.0 = Release|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Release|x64.ActiveCfg = Release|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Release|x64.Build.0 = Release|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Release|x86.ActiveCfg = Release|Any CPU + {6CCCF618-2E70-4870-B39F-32C016FE08F0}.Release|x86.Build.0 = Release|Any CPU {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Debug|Any CPU.Build.0 = Debug|Any CPU {558C46DE-DE16-41D5-8DB7-D6D748E32977}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -8515,6 +8529,7 @@ Global {A1D02CE6-1077-410A-81CB-D4BD500FD765} = {0508E463-0269-40C9-B5C2-3B600FB2A28B} {3044DFA5-DE4F-44D8-8DD8-EDF547BE513E} = {C445B129-0A4D-41F5-8347-6534B6B12303} {4BD6F0DB-BE9C-4C54-B52A-D20B88855ED5} = {C445B129-0A4D-41F5-8347-6534B6B12303} + {6CCCF618-2E70-4870-B39F-32C016FE08F0} = {088C37A5-30D2-40FB-B031-D163CFBED006} {B730328F-D9E9-4EAA-B28E-4631A14095F9} = {C445B129-0A4D-41F5-8347-6534B6B12303} {44963D50-8B58-44E6-918D-788BCB406695} = {B730328F-D9E9-4EAA-B28E-4631A14095F9} {3EC71A0E-6515-4A5A-B759-F0BCF1BCFC56} = {44963D50-8B58-44E6-918D-788BCB406695} diff --git a/src/Mvc/Mvc.Testing/src/DeferredHostBuilder.cs b/src/Mvc/Mvc.Testing/src/DeferredHostBuilder.cs new file mode 100644 index 000000000000..f84f8e792ca0 --- /dev/null +++ b/src/Mvc/Mvc.Testing/src/DeferredHostBuilder.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AspNetCore.Mvc.Testing +{ + // This host builder captures calls to the IHostBuilder then replays them in the call to ConfigureHostBuilder + internal class DeferredHostBuilder : IHostBuilder + { + public IDictionary Properties { get; } = new Dictionary(); + + private Action _configure; + private Func? _hostFactory; + + public DeferredHostBuilder() + { + _configure = b => + { + // Copy the properties from this builder into the builder + // that we're going to receive + foreach (var pair in Properties) + { + b.Properties[pair.Key] = pair.Value; + } + }; + } + + public IHost Build() + { + // This will never be null if the case where Build is being called + 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); + } + + public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) + { + _configure += b => b.ConfigureAppConfiguration(configureDelegate); + return this; + } + + public IHostBuilder ConfigureContainer(Action configureDelegate) + { + _configure += b => b.ConfigureContainer(configureDelegate); + return this; + } + + public IHostBuilder ConfigureHostConfiguration(Action configureDelegate) + { + _configure += b => b.ConfigureHostConfiguration(configureDelegate); + return this; + } + + public IHostBuilder ConfigureServices(Action configureDelegate) + { + _configure += b => b.ConfigureServices(configureDelegate); + return this; + } + + public IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory) where TContainerBuilder : notnull + { + _configure += b => b.UseServiceProviderFactory(factory); + return this; + } + + public IHostBuilder UseServiceProviderFactory(Func> factory) where TContainerBuilder : notnull + { + _configure += b => b.UseServiceProviderFactory(factory); + return this; + } + + public void ConfigureHostBuilder(object hostBuilder) + { + _configure(((IHostBuilder)hostBuilder)); + } + + public void SetHostFactory(Func hostFactory) + { + _hostFactory = hostFactory; + } + + private class DeferredHost : IHost, IAsyncDisposable + { + private readonly IHost _host; + + public DeferredHost(IHost host) + { + _host = host; + } + + public IServiceProvider Services => _host.Services; + + public void Dispose() => _host.Dispose(); + + public ValueTask DisposeAsync() + { + if (_host is IAsyncDisposable disposable) + { + return disposable.DisposeAsync(); + } + Dispose(); + return default; + } + + public 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); + + // 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); + + return tcs.Task; + } + + public Task StopAsync(CancellationToken cancellationToken = default) => _host.StopAsync(cancellationToken); + } + } +} diff --git a/src/Mvc/Mvc.Testing/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Testing/src/PublicAPI.Unshipped.txt index 93342e6e7b62..708e5ff66ace 100644 --- a/src/Mvc/Mvc.Testing/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Testing/src/PublicAPI.Unshipped.txt @@ -49,6 +49,6 @@ virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory.Conf virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory.CreateHost(Microsoft.Extensions.Hosting.IHostBuilder! builder) -> Microsoft.Extensions.Hosting.IHost! virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory.CreateHostBuilder() -> Microsoft.Extensions.Hosting.IHostBuilder? virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory.CreateServer(Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder) -> Microsoft.AspNetCore.TestHost.TestServer! -virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory.CreateWebHostBuilder() -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory.CreateWebHostBuilder() -> Microsoft.AspNetCore.Hosting.IWebHostBuilder? virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory.GetTestAssemblies() -> System.Collections.Generic.IEnumerable! virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory.Services.get -> System.IServiceProvider! diff --git a/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs b/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs index 3a8f7175dcb0..024f6d62431e 100644 --- a/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs +++ b/src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyModel; using Microsoft.Extensions.Hosting; @@ -149,23 +150,56 @@ private void EnsureServer() EnsureDepsFile(); var hostBuilder = CreateHostBuilder(); - if (hostBuilder != null) + if (hostBuilder is not null) { - hostBuilder.ConfigureWebHost(webHostBuilder => - { - SetContentRoot(webHostBuilder); - _configuration(webHostBuilder); - webHostBuilder.UseTestServer(); - }); - _host = CreateHost(hostBuilder); - _server = (TestServer)_host.Services.GetRequiredService(); + ConfigureHostBuilder(hostBuilder); return; } var builder = CreateWebHostBuilder(); - SetContentRoot(builder); - _configuration(builder); - _server = CreateServer(builder); + if (builder is null) + { + 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); + + if (factory is not null) + { + // If we have a valid factory it means the specified entry point's assembly can potentially resolve the IHost + // so we set the factory on the DeferredHostBuilder so we can invoke it on the call to IHostBuilder.Build. + deferredHostBuilder.SetHostFactory(factory); + + ConfigureHostBuilder(deferredHostBuilder); + return; + } + + throw new InvalidOperationException(Resources.FormatMissingBuilderMethod( + nameof(IHostBuilder), + nameof(IWebHostBuilder), + typeof(TEntryPoint).Assembly.EntryPoint!.DeclaringType!.FullName, + typeof(WebApplicationFactory).Name, + nameof(CreateHostBuilder), + nameof(CreateWebHostBuilder))); + } + else + { + SetContentRoot(builder); + _configuration(builder); + _server = CreateServer(builder); + } + } + + [MemberNotNull(nameof(_server))] + private void ConfigureHostBuilder(IHostBuilder hostBuilder) + { + hostBuilder.ConfigureWebHost(webHostBuilder => + { + SetContentRoot(webHostBuilder); + _configuration(webHostBuilder); + webHostBuilder.UseTestServer(); + }); + _host = CreateHost(hostBuilder); + _server = (TestServer)_host.Services.GetRequiredService(); } private void SetContentRoot(IWebHostBuilder builder) @@ -341,10 +375,8 @@ private void EnsureDepsFile() protected virtual IHostBuilder? CreateHostBuilder() { var hostBuilder = HostFactoryResolver.ResolveHostBuilderFactory(typeof(TEntryPoint).Assembly)?.Invoke(Array.Empty()); - if (hostBuilder != null) - { - hostBuilder.UseEnvironment(Environments.Development); - } + + hostBuilder?.UseEnvironment(Environments.Development); return hostBuilder; } @@ -357,23 +389,16 @@ private void EnsureDepsFile() /// array as arguments. /// /// A instance. - protected virtual IWebHostBuilder CreateWebHostBuilder() + protected virtual IWebHostBuilder? CreateWebHostBuilder() { var builder = WebHostBuilderFactory.CreateFromTypesAssemblyEntryPoint(Array.Empty()); - if (builder == null) - { - throw new InvalidOperationException(Resources.FormatMissingBuilderMethod( - nameof(IHostBuilder), - nameof(IWebHostBuilder), - typeof(TEntryPoint).Assembly.EntryPoint!.DeclaringType!.FullName, - typeof(WebApplicationFactory).Name, - nameof(CreateHostBuilder), - nameof(CreateWebHostBuilder))); - } - else + + if (builder is not null) { return builder.UseEnvironment(Environments.Development); } + + return null; } /// @@ -566,7 +591,7 @@ private class DelegatedWebApplicationFactory : WebApplicationFactory _createServer; private readonly Func _createHost; - private readonly Func _createWebHostBuilder; + private readonly Func _createWebHostBuilder; private readonly Func _createHostBuilder; private readonly Func> _getTestAssemblies; private readonly Action _configureClient; @@ -575,7 +600,7 @@ public DelegatedWebApplicationFactory( WebApplicationFactoryClientOptions options, Func createServer, Func createHost, - Func createWebHostBuilder, + Func createWebHostBuilder, Func createHostBuilder, Func> getTestAssemblies, Action configureClient, @@ -595,7 +620,7 @@ public DelegatedWebApplicationFactory( protected override IHost CreateHost(IHostBuilder builder) => _createHost(builder); - protected override IWebHostBuilder CreateWebHostBuilder() => _createWebHostBuilder(); + protected override IWebHostBuilder? CreateWebHostBuilder() => _createWebHostBuilder(); protected override IHostBuilder? CreateHostBuilder() => _createHostBuilder(); diff --git a/src/Mvc/Mvc.slnf b/src/Mvc/Mvc.slnf index f9eb1c5485cb..b6e6c9fbf539 100644 --- a/src/Mvc/Mvc.slnf +++ b/src/Mvc/Mvc.slnf @@ -22,10 +22,10 @@ "src\\Html.Abstractions\\src\\Microsoft.AspNetCore.Html.Abstractions.csproj", "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj", "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj", + "src\\Http\\Features\\src\\Microsoft.Extensions.Features.csproj", "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", - "src\\Http\\Features\\src\\Microsoft.Extensions.Features.csproj", "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", @@ -108,6 +108,7 @@ "src\\Mvc\\test\\WebSites\\RazorWebSite\\RazorWebSite.csproj", "src\\Mvc\\test\\WebSites\\RoutingWebSite\\Mvc.RoutingWebSite.csproj", "src\\Mvc\\test\\WebSites\\SecurityWebSite\\SecurityWebSite.csproj", + "src\\Mvc\\test\\WebSites\\SimpleWebSiteWithWebApplicationBuilder\\SimpleWebSiteWithWebApplicationBuilder.csproj", "src\\Mvc\\test\\WebSites\\SimpleWebSite\\SimpleWebSite.csproj", "src\\Mvc\\test\\WebSites\\TagHelpersWebSite\\TagHelpersWebSite.csproj", "src\\Mvc\\test\\WebSites\\VersioningWebSite\\VersioningWebSite.csproj", 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 9cb3d6b02146..d2f4321e19d1 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj +++ b/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Mvc/test/Mvc.FunctionalTests/SimpleWithWebApplicationBuilderTests.cs b/src/Mvc/test/Mvc.FunctionalTests/SimpleWithWebApplicationBuilderTests.cs new file mode 100644 index 000000000000..a677f34009d6 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/SimpleWithWebApplicationBuilderTests.cs @@ -0,0 +1,34 @@ +// 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 SimpleWithWebApplicationBuilderTests : IClassFixture> + { + public SimpleWithWebApplicationBuilderTests(MvcTestFixture fixture) + { + Client = fixture.CreateDefaultClient(); + } + + public HttpClient Client { get; } + + [Fact] + public async Task HelloWorld() + { + // Arrange + var expected = "Hello World"; + + // Act + var content = await Client.GetStringAsync("http://localhost/"); + + // Assert + Assert.Equal(expected, content); + } + } +} diff --git a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/FakeEntryPoint.cs b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/FakeEntryPoint.cs new file mode 100644 index 000000000000..13b9625cdc17 --- /dev/null +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/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 SimpleWebSiteWithWebApplicationBuilder +{ + public class FakeStartup + { + } +} diff --git a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Program.cs b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Program.cs new file mode 100644 index 000000000000..09c9e72998ab --- /dev/null +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/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")); + +app.Run(); diff --git a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Properties/launchSettings.json b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/Properties/launchSettings.json new file mode 100644 index 000000000000..3c71eb74877d --- /dev/null +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/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/SimpleWebSiteWithWebApplicationBuilder/SimpleWebSiteWithWebApplicationBuilder.csproj b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/SimpleWebSiteWithWebApplicationBuilder.csproj new file mode 100644 index 000000000000..58eb21f2db2d --- /dev/null +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/SimpleWebSiteWithWebApplicationBuilder.csproj @@ -0,0 +1,10 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + diff --git a/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/readme.md b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/readme.md new file mode 100644 index 000000000000..0537ca53ed18 --- /dev/null +++ b/src/Mvc/test/WebSites/SimpleWebSiteWithWebApplicationBuilder/readme.md @@ -0,0 +1,4 @@ +SimpleWebSiteWithWebApplicationBuilder +=== +This sample web project illustrates a minimal site using WebApplicationBuilder. +Please build from root (`.\build.cmd` on Windows; `./build.sh` elsewhere) before using this site. \ No newline at end of file