Skip to content

Support the new top level statements with WebApplicationFactory #33462

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 6 commits into from
Jun 11, 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 @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
131 changes: 131 additions & 0 deletions src/Mvc/Mvc.Testing/src/DeferredHostBuilder.cs
Original file line number Diff line number Diff line change
@@ -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<object, object> Properties { get; } = new Dictionary<object, object>();

private Action<IHostBuilder> _configure;
private Func<string[], object>? _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)
Copy link
Member

Choose a reason for hiding this comment

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

Why not do:

b.Properties = new Dictionary<object, object>(Properties)

here?

Copy link
Member Author

Choose a reason for hiding this comment

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

b.Properties is read only.

{
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<string>());

// We can't return the host directly since we need to defer the call to StartAsync
return new DeferredHost(host);
}

public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
{
_configure += b => b.ConfigureAppConfiguration(configureDelegate);
return this;
}

public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate)
{
_configure += b => b.ConfigureContainer(configureDelegate);
return this;
}

public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
{
_configure += b => b.ConfigureHostConfiguration(configureDelegate);
return this;
}

public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
{
_configure += b => b.ConfigureServices(configureDelegate);
return this;
}

public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) where TContainerBuilder : notnull
{
_configure += b => b.UseServiceProviderFactory(factory);
return this;
}

public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory) where TContainerBuilder : notnull
{
_configure += b => b.UseServiceProviderFactory(factory);
return this;
}

public void ConfigureHostBuilder(object hostBuilder)
{
_configure(((IHostBuilder)hostBuilder));
}

public void SetHostFactory(Func<string[], object> 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<IHostApplicationLifetime>().ApplicationStarted.UnsafeRegister(_ => tcs.TrySetResult(), null);

return tcs.Task;
}

public Task StopAsync(CancellationToken cancellationToken = default) => _host.StopAsync(cancellationToken);
}
}
}
2 changes: 1 addition & 1 deletion src/Mvc/Mvc.Testing/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.Conf
virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.CreateHost(Microsoft.Extensions.Hosting.IHostBuilder! builder) -> Microsoft.Extensions.Hosting.IHost!
virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.CreateHostBuilder() -> Microsoft.Extensions.Hosting.IHostBuilder?
virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.CreateServer(Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder) -> Microsoft.AspNetCore.TestHost.TestServer!
virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.CreateWebHostBuilder() -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.CreateWebHostBuilder() -> Microsoft.AspNetCore.Hosting.IWebHostBuilder?
virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.GetTestAssemblies() -> System.Collections.Generic.IEnumerable<System.Reflection.Assembly!>!
virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.Services.get -> System.IServiceProvider!
87 changes: 56 additions & 31 deletions src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<IServer>();
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<TEntryPoint>).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<IServer>();
}

private void SetContentRoot(IWebHostBuilder builder)
Expand Down Expand Up @@ -341,10 +375,8 @@ private void EnsureDepsFile()
protected virtual IHostBuilder? CreateHostBuilder()
{
var hostBuilder = HostFactoryResolver.ResolveHostBuilderFactory<IHostBuilder>(typeof(TEntryPoint).Assembly)?.Invoke(Array.Empty<string>());
if (hostBuilder != null)
{
hostBuilder.UseEnvironment(Environments.Development);
}

hostBuilder?.UseEnvironment(Environments.Development);
return hostBuilder;
}

Expand All @@ -357,23 +389,16 @@ private void EnsureDepsFile()
/// array as arguments.
/// </remarks>
/// <returns>A <see cref="IWebHostBuilder"/> instance.</returns>
protected virtual IWebHostBuilder CreateWebHostBuilder()
protected virtual IWebHostBuilder? CreateWebHostBuilder()
{
var builder = WebHostBuilderFactory.CreateFromTypesAssemblyEntryPoint<TEntryPoint>(Array.Empty<string>());
if (builder == null)
{
throw new InvalidOperationException(Resources.FormatMissingBuilderMethod(
nameof(IHostBuilder),
nameof(IWebHostBuilder),
typeof(TEntryPoint).Assembly.EntryPoint!.DeclaringType!.FullName,
typeof(WebApplicationFactory<TEntryPoint>).Name,
nameof(CreateHostBuilder),
nameof(CreateWebHostBuilder)));
}
else

if (builder is not null)
{
return builder.UseEnvironment(Environments.Development);
}

return null;
}

/// <summary>
Expand Down Expand Up @@ -566,7 +591,7 @@ private class DelegatedWebApplicationFactory : WebApplicationFactory<TEntryPoint
{
private readonly Func<IWebHostBuilder, TestServer> _createServer;
private readonly Func<IHostBuilder, IHost> _createHost;
private readonly Func<IWebHostBuilder> _createWebHostBuilder;
private readonly Func<IWebHostBuilder?> _createWebHostBuilder;
private readonly Func<IHostBuilder?> _createHostBuilder;
private readonly Func<IEnumerable<Assembly>> _getTestAssemblies;
private readonly Action<HttpClient> _configureClient;
Expand All @@ -575,7 +600,7 @@ public DelegatedWebApplicationFactory(
WebApplicationFactoryClientOptions options,
Func<IWebHostBuilder, TestServer> createServer,
Func<IHostBuilder, IHost> createHost,
Func<IWebHostBuilder> createWebHostBuilder,
Func<IWebHostBuilder?> createWebHostBuilder,
Func<IHostBuilder?> createHostBuilder,
Func<IEnumerable<Assembly>> getTestAssemblies,
Action<HttpClient> configureClient,
Expand All @@ -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();

Expand Down
3 changes: 2 additions & 1 deletion src/Mvc/Mvc.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<ProjectReference Include="..\WebSites\RoutingWebSite\Mvc.RoutingWebSite.csproj" />
<ProjectReference Include="..\WebSites\SecurityWebSite\SecurityWebSite.csproj" />
<ProjectReference Include="..\WebSites\SimpleWebSite\SimpleWebSite.csproj" />
<ProjectReference Include="..\WebSites\SimpleWebSiteWithWebApplicationBuilder\SimpleWebSiteWithWebApplicationBuilder.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,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<MvcTestFixture<SimpleWebSiteWithWebApplicationBuilder.FakeStartup>>
{
public SimpleWithWebApplicationBuilderTests(MvcTestFixture<SimpleWebSiteWithWebApplicationBuilder.FakeStartup> 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);
}
}
}
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 SimpleWebSiteWithWebApplicationBuilder
{
public class FakeStartup
{
}
}
Loading