// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyModel;
namespace Microsoft.AspNetCore.Mvc.Testing
{
///
/// Factory for bootstrapping an application in memory for functional end to end tests.
///
/// A type in the entry point assembly of the application.
/// Typically the Startup or Program classes can be used.
public class WebApplicationFactory : IDisposable where TEntryPoint : class
{
private bool _disposed;
private TestServer _server;
private Action _configuration;
private IList _clients = new List();
private List> _derivedFactories =
new List>();
///
///
/// Creates an instance of . This factory can be used to
/// create a instance using the MVC application defined by
/// and one or more instances used to send to the .
/// The will find the entry point class of
/// assembly and initialize the application by calling IWebHostBuilder CreateWebHostBuilder(string [] args)
/// on .
///
///
/// This constructor will infer the application content root path by searching for a
/// on the assembly containing the functional tests with
/// a key equal to the assembly .
/// In case an attribute with the right key can't be found,
/// will fall back to searching for a solution file (*.sln) and then appending assembly name
/// to the solution directory. The application root directory will be used to discover views and content files.
///
///
/// The application assemblies will be loaded from the dependency context of the assembly containing
/// . This means that project dependencies of the assembly containing
/// will be loaded as application assemblies.
///
///
public WebApplicationFactory()
{
_configuration = ConfigureWebHost;
}
///
/// Finalizes an instance of the class.
///
~WebApplicationFactory()
{
Dispose(false);
}
///
/// Gets the created by this .
///
public TestServer Server => _server;
///
/// Gets the of factories created from this factory
/// by further customizing the when calling
/// .
///
public IReadOnlyList> Factories => _derivedFactories.AsReadOnly();
///
/// Gets the used by .
///
public WebApplicationFactoryClientOptions ClientOptions { get; private set; } = new WebApplicationFactoryClientOptions();
///
/// Creates a new with a
/// that is further customized by .
///
///
/// An to configure the .
///
/// A new .
public WebApplicationFactory WithWebHostBuilder(Action configuration) =>
WithWebHostBuilderCore(configuration);
internal virtual WebApplicationFactory WithWebHostBuilderCore(Action configuration)
{
var factory = new DelegatedWebApplicationFactory(
ClientOptions,
CreateServer,
CreateWebHostBuilder,
GetTestAssemblies,
ConfigureClient,
builder =>
{
_configuration(builder);
configuration(builder);
});
_derivedFactories.Add(factory);
return factory;
}
private void EnsureServer()
{
if (_server != null)
{
return;
}
EnsureDepsFile();
var builder = CreateWebHostBuilder();
SetContentRoot(builder);
_configuration(builder);
_server = CreateServer(builder);
}
private void SetContentRoot(IWebHostBuilder builder)
{
if (SetContentRootFromSetting(builder))
{
return;
}
var metadataAttributes = GetContentRootMetadataAttributes(
typeof(TEntryPoint).Assembly.FullName,
typeof(TEntryPoint).Assembly.GetName().Name);
string contentRoot = null;
for (var i = 0; i < metadataAttributes.Length; i++)
{
var contentRootAttribute = metadataAttributes[i];
var contentRootCandidate = Path.Combine(
AppContext.BaseDirectory,
contentRootAttribute.ContentRootPath);
var contentRootMarker = Path.Combine(
contentRootCandidate,
Path.GetFileName(contentRootAttribute.ContentRootTest));
if (File.Exists(contentRootMarker))
{
contentRoot = contentRootCandidate;
break;
}
}
if (contentRoot != null)
{
builder.UseContentRoot(contentRoot);
}
else
{
builder.UseSolutionRelativeContentRoot(typeof(TEntryPoint).Assembly.GetName().Name);
}
}
private static bool SetContentRootFromSetting(IWebHostBuilder builder)
{
// Attempt to look for TEST_CONTENTROOT_APPNAME in settings. This should result in looking for
// ASPNETCORE_TEST_CONTENTROOT_APPNAME environment variable.
var assemblyName = typeof(TEntryPoint).Assembly.GetName().Name;
var settingSuffix = assemblyName.ToUpperInvariant().Replace(".", "_");
var settingName = $"TEST_CONTENTROOT_{settingSuffix}";
var settingValue = builder.GetSetting(settingName);
if (settingValue == null)
{
return false;
}
builder.UseContentRoot(settingValue);
return true;
}
private WebApplicationFactoryContentRootAttribute[] GetContentRootMetadataAttributes(
string tEntryPointAssemblyFullName,
string tEntryPointAssemblyName)
{
var testAssembly = GetTestAssemblies();
var metadataAttributes = testAssembly
.SelectMany(a => a.GetCustomAttributes())
.Where(a => string.Equals(a.Key, tEntryPointAssemblyFullName, StringComparison.OrdinalIgnoreCase) ||
string.Equals(a.Key, tEntryPointAssemblyName, StringComparison.OrdinalIgnoreCase))
.OrderBy(a => a.Priority)
.ToArray();
return metadataAttributes;
}
///
/// Gets the assemblies containing the functional tests. The
/// applied to these
/// assemblies defines the content root to use for the given
/// .
///
/// The list of containing tests.
protected virtual IEnumerable GetTestAssemblies()
{
try
{
// The default dependency context will be populated in .net core applications.
var context = DependencyContext.Default;
if (context == null || context.CompileLibraries.Count == 0)
{
// The app domain friendly name will be populated in full framework.
return new[] { Assembly.Load(AppDomain.CurrentDomain.FriendlyName) };
}
var runtimeProjectLibraries = context.RuntimeLibraries
.ToDictionary(r => r.Name, r => r, StringComparer.Ordinal);
// Find the list of projects
var projects = context.CompileLibraries.Where(l => l.Type == "project");
var entryPointAssemblyName = typeof(TEntryPoint).Assembly.GetName().Name;
// Find the list of projects referencing TEntryPoint.
var candidates = context.CompileLibraries
.Where(library => library.Dependencies.Any(d => string.Equals(d.Name, entryPointAssemblyName, StringComparison.Ordinal)));
var testAssemblies = new List();
foreach (var candidate in candidates)
{
if (runtimeProjectLibraries.TryGetValue(candidate.Name, out var runtimeLibrary))
{
var runtimeAssemblies = runtimeLibrary.GetDefaultAssemblyNames(context);
testAssemblies.AddRange(runtimeAssemblies.Select(Assembly.Load));
}
}
return testAssemblies;
}
catch (Exception)
{
}
return Array.Empty();
}
private void EnsureDepsFile()
{
if (typeof(TEntryPoint).Assembly.EntryPoint == null)
{
throw new InvalidOperationException(Resources.FormatInvalidAssemblyEntryPoint(typeof(TEntryPoint).Name));
}
var depsFileName = $"{typeof(TEntryPoint).Assembly.GetName().Name}.deps.json";
var depsFile = new FileInfo(Path.Combine(AppContext.BaseDirectory, depsFileName));
if (!depsFile.Exists)
{
throw new InvalidOperationException(Resources.FormatMissingDepsFile(
depsFile.FullName,
Path.GetFileName(depsFile.FullName)));
}
}
///
/// Creates a used to set up .
///
///
/// The default implementation of this method looks for a public static IWebHostBuilder CreateDefaultBuilder(string[] args)
/// method defined on the entry point of the assembly of and invokes it passing an empty string
/// array as arguments.
///
/// A instance.
protected virtual IWebHostBuilder CreateWebHostBuilder()
{
var builder = WebHostBuilderFactory.CreateFromTypesAssemblyEntryPoint(Array.Empty());
if (builder == null)
{
throw new InvalidOperationException(Resources.FormatMissingCreateWebHostBuilderMethod(
nameof(IWebHostBuilder),
typeof(TEntryPoint).Assembly.EntryPoint.DeclaringType.FullName,
typeof(WebApplicationFactory).Name,
nameof(CreateWebHostBuilder)));
}
else
{
return builder.UseEnvironment("Development");
}
}
///
/// Creates the with the bootstrapped application in .
///
/// The used to
/// create the server.
/// The with the bootstrapped application.
protected virtual TestServer CreateServer(IWebHostBuilder builder) => new TestServer(builder);
///
/// Gives a fixture an opportunity to configure the application before it gets built.
///
/// The for the application.
protected virtual void ConfigureWebHost(IWebHostBuilder builder)
{
}
///
/// Creates an instance of that automatically follows
/// redirects and handles cookies.
///
/// The .
public HttpClient CreateClient() =>
CreateClient(ClientOptions);
///
/// Creates an instance of that automatically follows
/// redirects and handles cookies.
///
/// The .
public HttpClient CreateClient(WebApplicationFactoryClientOptions options) =>
CreateDefaultClient(options.BaseAddress, options.CreateHandlers());
///
/// Creates a new instance of an that can be used to
/// send to the server. The base address of the
/// instance will be set to http://localhost.
///
/// A list of instances to set up on the
/// .
/// The .
public HttpClient CreateDefaultClient(params DelegatingHandler[] handlers)
{
EnsureServer();
HttpClient client;
if (handlers == null || handlers.Length == 0)
{
client = _server.CreateClient();
}
else
{
for (var i = handlers.Length - 1; i > 0; i--)
{
handlers[i - 1].InnerHandler = handlers[i];
}
var serverHandler = _server.CreateHandler();
handlers[handlers.Length - 1].InnerHandler = serverHandler;
client = new HttpClient(handlers[0]);
}
_clients.Add(client);
ConfigureClient(client);
return client;
}
///
/// Configures instances created by this .
///
/// The instance getting configured.
protected virtual void ConfigureClient(HttpClient client)
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}
client.BaseAddress = new Uri("http://localhost");
}
///
/// Creates a new instance of an that can be used to
/// send to the server.
///
/// The base address of the instance.
/// A list of instances to set up on the
/// .
/// The .
public HttpClient CreateDefaultClient(Uri baseAddress, params DelegatingHandler[] handlers)
{
var client = CreateDefaultClient(handlers);
client.BaseAddress = baseAddress;
return client;
}
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
///
/// to release both managed and unmanaged resources;
/// to release only unmanaged resources.
///
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
foreach (var client in _clients)
{
client.Dispose();
}
foreach (var factory in _derivedFactories)
{
factory.Dispose();
}
_server?.Dispose();
}
_disposed = true;
}
private class DelegatedWebApplicationFactory : WebApplicationFactory
{
private readonly Func _createServer;
private readonly Func _createWebHostBuilder;
private readonly Func> _getTestAssemblies;
private readonly Action _configureClient;
public DelegatedWebApplicationFactory(
WebApplicationFactoryClientOptions options,
Func createServer,
Func createWebHostBuilder,
Func> getTestAssemblies,
Action configureClient,
Action configureWebHost)
{
ClientOptions = new WebApplicationFactoryClientOptions(options);
_createServer = createServer;
_createWebHostBuilder = createWebHostBuilder;
_getTestAssemblies = getTestAssemblies;
_configureClient = configureClient;
_configuration = configureWebHost;
}
protected override TestServer CreateServer(IWebHostBuilder builder) => _createServer(builder);
protected override IWebHostBuilder CreateWebHostBuilder() => _createWebHostBuilder();
protected override IEnumerable GetTestAssemblies() => _getTestAssemblies();
protected override void ConfigureWebHost(IWebHostBuilder builder) => _configuration(builder);
protected override void ConfigureClient(HttpClient client) => _configureClient(client);
internal override WebApplicationFactory WithWebHostBuilderCore(Action configuration)
{
return new DelegatedWebApplicationFactory(
ClientOptions,
_createServer,
_createWebHostBuilder,
_getTestAssemblies,
_configureClient,
builder =>
{
_configuration(builder);
configuration(builder);
});
}
}
}
}