diff --git a/Mvc.sln b/Mvc.sln
index 4aac68236a..0f2d1a146b 100644
--- a/Mvc.sln
+++ b/Mvc.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
-VisualStudioVersion = 15.0.26228.9
+VisualStudioVersion = 15.0.26615.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@@ -122,6 +122,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorPagesWebSite", "test\W
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Performance", "test\Microsoft.AspNetCore.Mvc.Performance\Microsoft.AspNetCore.Mvc.Performance.csproj", "{F16CEE0D-A28E-43BD-802F-99BAFE4BA7CE}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Testing", "src\Microsoft.AspNetCore.Mvc.Testing\Microsoft.AspNetCore.Mvc.Testing.csproj", "{7500B228-1769-4CFB-A571-3DFAC6678A06}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Mvc.Testing.Xunit", "src\Microsoft.AspNetCore.Mvc.Testing.Xunit\Microsoft.AspNetCore.Mvc.Testing.Xunit.csproj", "{5248D809-E5E5-49FE-B3E8-428D454C63B3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -756,6 +760,30 @@ Global
{F16CEE0D-A28E-43BD-802F-99BAFE4BA7CE}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F16CEE0D-A28E-43BD-802F-99BAFE4BA7CE}.Release|x86.ActiveCfg = Release|Any CPU
{F16CEE0D-A28E-43BD-802F-99BAFE4BA7CE}.Release|x86.Build.0 = Release|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Debug|x86.Build.0 = Debug|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|x86.ActiveCfg = Release|Any CPU
+ {7500B228-1769-4CFB-A571-3DFAC6678A06}.Release|x86.Build.0 = Release|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Debug|x86.Build.0 = Debug|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|x86.ActiveCfg = Release|Any CPU
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -818,5 +846,10 @@ Global
{0AB46520-F441-4E01-B444-08F4D23F8B1B} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{4BA6EC9A-B6D9-41F2-BFDA-D82B22D80352} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{F16CEE0D-A28E-43BD-802F-99BAFE4BA7CE} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
+ {7500B228-1769-4CFB-A571-3DFAC6678A06} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
+ {5248D809-E5E5-49FE-B3E8-428D454C63B3} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}
EndGlobalSection
EndGlobal
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/CultureReplacer.cs b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/CultureReplacer.cs
new file mode 100644
index 0000000000..75d60d3ace
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/CultureReplacer.cs
@@ -0,0 +1,82 @@
+// 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.Globalization;
+using System.Threading;
+
+namespace Microsoft.AspNetCore.Mvc.Testing.Xunit.Internal
+{
+ internal class CultureReplacer : IDisposable
+ {
+ private const string _defaultCultureName = "en-GB";
+ private const string _defaultUICultureName = "en-US";
+ private static readonly CultureInfo _defaultCulture = new CultureInfo(_defaultCultureName);
+ private readonly CultureInfo _originalCulture;
+ private readonly CultureInfo _originalUICulture;
+ private readonly long _threadId;
+
+ // Culture => Formatting of dates/times/money/etc, defaults to en-GB because en-US is the same as InvariantCulture
+ // We want to be able to find issues where the InvariantCulture is used, but a specific culture should be.
+ //
+ // UICulture => Language
+ public CultureReplacer(string culture = _defaultCultureName, string uiCulture = _defaultUICultureName)
+ : this(new CultureInfo(culture), new CultureInfo(uiCulture))
+ {
+ }
+
+ public CultureReplacer(CultureInfo culture, CultureInfo uiCulture)
+ {
+ _originalCulture = CultureInfo.CurrentCulture;
+ _originalUICulture = CultureInfo.CurrentUICulture;
+ _threadId = Thread.CurrentThread.ManagedThreadId;
+ CultureInfo.CurrentCulture = culture;
+ CultureInfo.CurrentUICulture = uiCulture;
+ }
+
+ ///
+ /// The name of the culture that is used as the default value for CultureInfo.DefaultThreadCurrentCulture when CultureReplacer is used.
+ ///
+ public static string DefaultCultureName
+ {
+ get { return _defaultCultureName; }
+ }
+
+ ///
+ /// The name of the culture that is used as the default value for [Thread.CurrentThread(NET45)/CultureInfo(K10)].CurrentUICulture when CultureReplacer is used.
+ ///
+ public static string DefaultUICultureName
+ {
+ get { return _defaultUICultureName; }
+ }
+
+ ///
+ /// The culture that is used as the default value for [Thread.CurrentThread(NET45)/CultureInfo(K10)].CurrentCulture when CultureReplacer is used.
+ ///
+ public static CultureInfo DefaultCulture
+ {
+ get { return _defaultCulture; }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ if(Thread.CurrentThread.ManagedThreadId != _threadId)
+ {
+ throw new InvalidOperationException("The current thread is not the same as the thread " +
+ "invoking the constructor. This should never happen.");
+ }
+
+ CultureInfo.CurrentCulture = _originalCulture;
+ CultureInfo.CurrentUICulture = _originalUICulture;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/CultureReplacerMiddleware.cs b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/CultureReplacerMiddleware.cs
new file mode 100644
index 0000000000..64408bb787
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/CultureReplacerMiddleware.cs
@@ -0,0 +1,42 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Mvc.Testing.Xunit.Internal
+{
+ ///
+ /// A middleware that ensures web sites run in a consistent culture. Currently useful for tests that format dates,
+ /// times, or numbers. Will be more useful when we have localized resources.
+ ///
+ public class CultureReplacerMiddleware
+ {
+ // Have no current need to use cultures other than the ReplaceCultureAttribute defaults (en-GB, en-US).
+ private readonly ReplaceCultureAttribute _replaceCulture = new ReplaceCultureAttribute();
+
+ private readonly RequestDelegate _next;
+
+ public CultureReplacerMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ // Use ReplaceCultureAttribute to avoid thread consistency checks in CultureReplacer. await doesn't
+ // necessarily end on the original thread. For this case, problems arise when next middleware throws. Can
+ // remove the thread consistency checks once culture is (at least for .NET 4.6) handled using
+ // AsyncLocal.
+ try
+ {
+ _replaceCulture.Before(methodUnderTest: null);
+ await _next(context);
+ }
+ finally
+ {
+ _replaceCulture.After(methodUnderTest: null);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/CultureReplacerStartupFilter.cs b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/CultureReplacerStartupFilter.cs
new file mode 100644
index 0000000000..caa71a337f
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/CultureReplacerStartupFilter.cs
@@ -0,0 +1,27 @@
+// 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;
+using Microsoft.AspNetCore.Hosting;
+
+namespace Microsoft.AspNetCore.Mvc.Testing.Xunit.Internal
+{
+ ///
+ /// Inserts the at the beginning of the pipeline.
+ ///
+ public class CultureReplacerStartupFilter : IStartupFilter
+ {
+ ///
+ public Action Configure(Action next)
+ {
+ return AddCulture;
+
+ void AddCulture(IApplicationBuilder builder)
+ {
+ builder.UseMiddleware();
+ next(builder);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/ReplaceCultureAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/ReplaceCultureAttribute.cs
new file mode 100644
index 0000000000..c7914127e7
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Internal/ReplaceCultureAttribute.cs
@@ -0,0 +1,69 @@
+// 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.Globalization;
+using System.Reflection;
+using Xunit.Sdk;
+
+namespace Microsoft.AspNetCore.Mvc.Testing.Xunit.Internal
+{
+ ///
+ /// Replaces the current culture and UI culture for the test.
+ ///
+ [AttributeUsage(AttributeTargets.Method)]
+ internal class ReplaceCultureAttribute : BeforeAfterTestAttribute
+ {
+ private const string _defaultCultureName = "en-GB";
+ private const string _defaultUICultureName = "en-US";
+ private CultureInfo _originalCulture;
+ private CultureInfo _originalUICulture;
+
+ ///
+ /// Replaces the current culture and UI culture to en-GB and en-US respectively.
+ ///
+ public ReplaceCultureAttribute() :
+ this(_defaultCultureName, _defaultUICultureName)
+ {
+ }
+
+ ///
+ /// Replaces the current culture and UI culture based on specified values.
+ ///
+ public ReplaceCultureAttribute(string currentCulture, string currentUICulture)
+ {
+ Culture = new CultureInfo(currentCulture);
+ UICulture = new CultureInfo(currentUICulture);
+ }
+
+ ///
+ /// The for the test. Defaults to en-GB.
+ ///
+ ///
+ /// en-GB is used here as the default because en-US is equivalent to the InvariantCulture. We
+ /// want to be able to find bugs where we're accidentally relying on the Invariant instead of the
+ /// user's culture.
+ ///
+ public CultureInfo Culture { get; }
+
+ ///
+ /// The for the test. Defaults to en-US.
+ ///
+ public CultureInfo UICulture { get; }
+
+ public override void Before(MethodInfo methodUnderTest)
+ {
+ _originalCulture = CultureInfo.CurrentCulture;
+ _originalUICulture = CultureInfo.CurrentUICulture;
+
+ CultureInfo.CurrentCulture = Culture;
+ CultureInfo.CurrentUICulture = UICulture;
+ }
+
+ public override void After(MethodInfo methodUnderTest)
+ {
+ CultureInfo.CurrentCulture = _originalCulture;
+ CultureInfo.CurrentUICulture = _originalUICulture;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Microsoft.AspNetCore.Mvc.Testing.Xunit.csproj b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Microsoft.AspNetCore.Mvc.Testing.Xunit.csproj
new file mode 100644
index 0000000000..bf45538e3f
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/Microsoft.AspNetCore.Mvc.Testing.Xunit.csproj
@@ -0,0 +1,25 @@
+
+
+
+
+
+ XUnit convenience fixture for creating functional tests for MVC applications.
+ netstandard2.0
+ $(NoWarn);CS1591
+ aspnetcore;aspnetcoremvc;aspnetcoremvctesting
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/WebApplicationTestFixture.cs b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/WebApplicationTestFixture.cs
new file mode 100644
index 0000000000..6ebc54b642
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/WebApplicationTestFixture.cs
@@ -0,0 +1,126 @@
+// 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.IO;
+using System.Net.Http;
+using System.Reflection;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.Mvc.Testing.Xunit.Internal;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.AspNetCore.Mvc.FunctionalTests
+{
+ ///
+ /// XUnit fixture for bootstrapping an application in memory for functional end to end tests.
+ ///
+ /// The applications startup class.
+ public class WebApplicationTestFixture : IDisposable where TStartup : class
+ {
+ private readonly TestServer _server;
+
+ public WebApplicationTestFixture()
+ : this("src")
+ {
+ }
+
+ protected WebApplicationTestFixture(string solutionRelativePath)
+ : this("*.sln", solutionRelativePath)
+ {
+ }
+
+ protected WebApplicationTestFixture(string solutionSearchPattern, string solutionRelativePath)
+ {
+ var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
+
+ // This step assumes project name = assembly name.
+ var projectName = startupAssembly.GetName().Name;
+ var projectPath = Path.Combine(solutionRelativePath, projectName);
+ var builder = new MvcWebApplicationBuilder()
+ .UseSolutionRelativeContentRoot(projectPath)
+ .UseApplicationAssemblies();
+
+ ConfigureApplication(builder);
+
+ var xunitRunnerJson = new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), "xunit.runner.json"));
+ if (!xunitRunnerJson.Exists)
+ {
+ Console.WriteLine("Can't find xunit.runner.json. " +
+ "Functional tests require '\"shadowCopy\": false' to work properly. " +
+ "Make sure your XUnit configuration has that setup.");
+ }
+
+ var content = JsonConvert.DeserializeObject(File.ReadAllText(xunitRunnerJson.FullName));
+ if (!content.TryGetValue("shadowCopy", out var token) || !(bool)token)
+ {
+ Console.WriteLine("'shadowCopy' is not set to true on xunit.runner.json. " +
+ "Functional tests require '\"shadowCopy\": false' to work properly. " +
+ "Make sure your XUnit configuration has that setup.");
+ }
+
+ using (new CultureReplacer())
+ {
+ _server = builder.Build();
+ }
+
+ Client = _server.CreateClient();
+ Client.BaseAddress = new Uri("http://localhost");
+ }
+
+ ///
+ /// Gives a fixture an opportunity to configure the application before it gets built.
+ ///
+ /// The for the application.
+ protected virtual void ConfigureApplication(MvcWebApplicationBuilder builder)
+ {
+ builder.ConfigureAfterStartup(s => s.TryAddEnumerable(ServiceDescriptor.Transient()));
+ }
+
+ public HttpClient Client { get; }
+
+ public HttpClient CreateClient()
+ {
+ var client = _server.CreateClient();
+ client.BaseAddress = new Uri("http://localhost");
+
+ return client;
+ }
+
+ public HttpClient CreateClient(Uri baseAddress, params DelegatingHandler[] handlers)
+ {
+ if (handlers.Length == 0)
+ {
+ var client = _server.CreateClient();
+ client.BaseAddress = baseAddress;
+
+ return client;
+ }
+ else
+ {
+
+ for (var i = handlers.Length - 1; i > 1; i++)
+ {
+ handlers[i - 1].InnerHandler = handlers[i];
+ }
+
+ var serverHandler = _server.CreateHandler();
+ handlers[handlers.Length - 1].InnerHandler = serverHandler;
+ var client = new HttpClient(handlers[0]);
+ client.BaseAddress = baseAddress;
+
+ return client;
+ }
+ }
+
+ public void Dispose()
+ {
+ Client.Dispose();
+ _server.Dispose();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/build/Microsoft.AspNetCore.Mvc.Testing.Xunit.targets b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/build/Microsoft.AspNetCore.Mvc.Testing.Xunit.targets
new file mode 100644
index 0000000000..2f8351755d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/build/Microsoft.AspNetCore.Mvc.Testing.Xunit.targets
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/xunit.runner.json b/src/Microsoft.AspNetCore.Mvc.Testing.Xunit/build/xunit.runner.json
similarity index 100%
rename from test/Microsoft.AspNetCore.Mvc.FunctionalTests/xunit.runner.json
rename to src/Microsoft.AspNetCore.Mvc.Testing.Xunit/build/xunit.runner.json
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing/Internal/CookieContainerHandler.cs b/src/Microsoft.AspNetCore.Mvc.Testing/Internal/CookieContainerHandler.cs
new file mode 100644
index 0000000000..edcaa857c1
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing/Internal/CookieContainerHandler.cs
@@ -0,0 +1,41 @@
+// 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.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Mvc.Testing.Internal
+{
+ ///
+ /// Delegating handler for managing cookies on functional tests.
+ ///
+ public class CookieContainerHandler : DelegatingHandler
+ {
+ public CookieContainerHandler(HttpMessageHandler innerHandler)
+ : base(innerHandler)
+ {
+ }
+
+ public CookieContainer Container { get; } = new CookieContainer();
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var cookieHeader = Container.GetCookieHeader(request.RequestUri);
+ request.Headers.Add("Cookie", cookieHeader);
+
+ var response = await base.SendAsync(request, cancellationToken);
+
+ if (response.Headers.TryGetValues("Set-Cookie", out var setCookieHeaders))
+ {
+ foreach (var header in setCookieHeaders)
+ {
+ Container.SetCookies(response.RequestMessage.RequestUri, header);
+ }
+ }
+
+ return response;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing/Internal/TestServiceRegistrations.cs b/src/Microsoft.AspNetCore.Mvc.Testing/Internal/TestServiceRegistrations.cs
new file mode 100644
index 0000000000..018c55bba4
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing/Internal/TestServiceRegistrations.cs
@@ -0,0 +1,33 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Mvc.Testing.Internal
+{
+ ///
+ /// Helper class to orchestrate service registrations in .
+ ///
+ public class TestServiceRegistrations
+ {
+ public IList> Before { get; set; } = new List>();
+ public IList> After { get; set; } = new List>();
+
+ public void ConfigureServices(IServiceCollection services, Action startupConfigureServices)
+ {
+ foreach (var config in Before)
+ {
+ config(services);
+ }
+
+ startupConfigureServices();
+
+ foreach (var config in After)
+ {
+ config(services);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing/Internal/TestStartup.cs b/src/Microsoft.AspNetCore.Mvc.Testing/Internal/TestStartup.cs
new file mode 100644
index 0000000000..2e9665d4e7
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing/Internal/TestStartup.cs
@@ -0,0 +1,55 @@
+// 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.Linq;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Mvc.Testing.Internal
+{
+ ///
+ /// Fake startup class used in functional tests to decorate the registration of
+ /// ConfigureServices.
+ ///
+ /// The startup class of your application.
+ public class TestStartup where TStartup : class
+ {
+ private readonly IServiceProvider _serviceProvider;
+ private readonly TestServiceRegistrations _registrations;
+ private readonly TStartup _instance;
+
+ public TestStartup(IServiceProvider serviceProvider, TestServiceRegistrations registrations)
+ {
+ _serviceProvider = serviceProvider;
+ _registrations = registrations;
+ _instance = (TStartup)ActivatorUtilities.CreateInstance(serviceProvider, typeof(TStartup));
+ }
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ var configureServices = _instance.GetType().GetMethod(nameof(ConfigureServices));
+ var parameters = Enumerable.Repeat(services, 1)
+ .Concat(configureServices
+ .GetParameters()
+ .Skip(1)
+ .Select(p => ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, p.ParameterType)))
+ .ToArray();
+
+ _registrations.ConfigureServices(services, () => configureServices.Invoke(_instance, parameters));
+ }
+
+ public void Configure(IApplicationBuilder applicationBuilder)
+ {
+ var configure = _instance.GetType().GetMethod(nameof(Configure));
+ var parameters = Enumerable.Repeat(applicationBuilder, 1)
+ .Concat(configure
+ .GetParameters()
+ .Skip(1)
+ .Select(p => ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, p.ParameterType)))
+ .ToArray();
+
+ configure.Invoke(_instance, parameters);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing/Microsoft.AspNetCore.Mvc.Testing.csproj b/src/Microsoft.AspNetCore.Mvc.Testing/Microsoft.AspNetCore.Mvc.Testing.csproj
new file mode 100644
index 0000000000..6c0201d8c9
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing/Microsoft.AspNetCore.Mvc.Testing.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Support for writing functional tests for MVC applications.
+ netstandard2.0
+ $(NoWarn);CS1591
+ aspnetcore;aspnetcoremvc;aspnetcoremvctesting
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing/MvcWebApplicationBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Testing/MvcWebApplicationBuilder.cs
new file mode 100644
index 0000000000..04753a5260
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing/MvcWebApplicationBuilder.cs
@@ -0,0 +1,136 @@
+// 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.Reflection;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.ApplicationParts;
+using Microsoft.AspNetCore.Mvc.Internal;
+using Microsoft.AspNetCore.Mvc.Testing.Internal;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Mvc.Testing
+{
+ ///
+ /// Builder API for bootstraping an MVC application for functional tests.
+ ///
+ /// The application startup class.
+ public class MvcWebApplicationBuilder where TStartup : class
+ {
+ public string ContentRoot { get; set; }
+ public IList> ConfigureServicesBeforeStartup { get; set; } = new List>();
+ public IList> ConfigureServicesAfterStartup { get; set; } = new List>();
+ public List ApplicationAssemblies { get; set; } = new List();
+
+ ///
+ /// Configures services before runs.
+ ///
+ /// The to configure the services with.
+ /// An instance of this
+ public MvcWebApplicationBuilder ConfigureBeforeStartup(Action configure)
+ {
+ ConfigureServicesBeforeStartup.Add(configure);
+ return this;
+ }
+
+ ///
+ /// Configures services after runs.
+ ///
+ /// The to configure the services with.
+ /// An instance of this
+ public MvcWebApplicationBuilder ConfigureAfterStartup(Action configure)
+ {
+ ConfigureServicesAfterStartup.Add(configure);
+ return this;
+ }
+
+ ///
+ /// Configures to include the default set
+ /// of provided by .
+ ///
+ /// An instance of this
+ public MvcWebApplicationBuilder UseApplicationAssemblies()
+ {
+ var depsFileName = $"{typeof(TStartup).Assembly.GetName().Name}.deps.json";
+ var depsFile = new FileInfo(Path.Combine(Directory.GetCurrentDirectory(), depsFileName));
+ if (!depsFile.Exists)
+ {
+ throw new InvalidOperationException($"Can't find'{depsFile}'. This file is required for functional tests " +
+ $"to run properly. There should be a copy of the file on your source project bin folder. If thats not the " +
+ $"case, make sure that the property PreserveCompilationContext is set to true on your project file. E.g" +
+ $"'true'.");
+ }
+
+ ApplicationAssemblies.AddRange(DefaultAssemblyPartDiscoveryProvider
+ .DiscoverAssemblyParts(typeof(TStartup).Assembly.GetName().Name)
+ .Select(s => ((AssemblyPart)s).Assembly)
+ .ToList());
+
+ return this;
+ }
+
+ ///
+ /// Configures the application content root.
+ ///
+ /// The glob pattern to use for finding the solution.
+ /// The relative path to the content root from the solution file.
+ /// An instance of this
+ public MvcWebApplicationBuilder UseSolutionRelativeContentRoot(
+ string solutionRelativePath,
+ string solutionName = "*.sln")
+ {
+ var applicationBasePath = AppContext.BaseDirectory;
+
+ var directoryInfo = new DirectoryInfo(applicationBasePath);
+ do
+ {
+ var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, solutionName).FirstOrDefault();
+ if (solutionPath != null)
+ {
+ ContentRoot = Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath));
+ return this;
+ }
+
+ directoryInfo = directoryInfo.Parent;
+ }
+ while (directoryInfo.Parent != null);
+
+ throw new Exception($"Solution root could not be located using application root {applicationBasePath}.");
+ }
+
+ public TestServer Build()
+ {
+ var builder = new WebHostBuilder()
+ .UseStartup>()
+ // This is necessary so that IHostingEnvironment.ApplicationName has the right
+ // value and libraries depending on it (to load the dependency context, for example)
+ // work properly.
+ .UseSetting(WebHostDefaults.ApplicationKey, typeof(TStartup).Assembly.GetName().Name)
+ .UseContentRoot(ContentRoot)
+ .ConfigureServices(InitializeServices);
+
+ return new TestServer(builder);
+ }
+
+ protected virtual void InitializeServices(IServiceCollection services)
+ {
+ // Inject a custom application part manager. Overrides AddMvcCore() because that uses TryAdd().
+ var manager = new ApplicationPartManager();
+ foreach (var assembly in ApplicationAssemblies)
+ {
+ manager.ApplicationParts.Add(new AssemblyPart(assembly));
+ }
+
+ services.AddSingleton(manager);
+ services.AddSingleton(new TestServiceRegistrations
+ {
+ Before = ConfigureServicesBeforeStartup,
+ After = ConfigureServicesAfterStartup
+ });
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Testing/build/Microsoft.AspNetCore.Mvc.Testing.targets b/src/Microsoft.AspNetCore.Mvc.Testing/build/Microsoft.AspNetCore.Mvc.Testing.targets
new file mode 100644
index 0000000000..b128ceeb70
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Testing/build/Microsoft.AspNetCore.Mvc.Testing.targets
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+ true
+
+
+
+
+ x86
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
index 86ebaa991b..34bc035c86 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
@@ -1,5 +1,7 @@
+
+
netcoreapp2.0;net461
@@ -11,39 +13,15 @@
$(DefineConstants);__RemoveThisBitTo__GENERATE_BASELINES
$(DefineConstants);FUNCTIONAL_TESTS
-
- true
-
-
-
-
- x86
-
-
- true
- true
-
-
-
+
@@ -78,15 +56,4 @@
-
-
-
-
-
-
-
-
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcEncodedTestFixtureOfT.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcEncodedTestFixtureOfT.cs
index 16d6c494d6..396842ce1b 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcEncodedTestFixtureOfT.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcEncodedTestFixtureOfT.cs
@@ -2,19 +2,24 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text.Encodings.Web;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.WebEncoders.Testing;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class MvcEncodedTestFixture : MvcTestFixture
+ where TStartup : class
{
- protected override void InitializeServices(IServiceCollection services)
+ protected override void ConfigureApplication(MvcWebApplicationBuilder builder)
{
- base.InitializeServices(services);
- services.AddTransient();
- services.AddTransient();
- services.AddTransient();
+ base.ConfigureApplication(builder);
+ builder.ConfigureBeforeStartup(services =>
+ {
+ services.TryAddTransient();
+ services.TryAddTransient();
+ services.TryAddTransient();
+ });
}
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcSampleFixture.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcSampleFixture.cs
index 6a09db1a65..c17a3f504b 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcSampleFixture.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcSampleFixture.cs
@@ -4,10 +4,8 @@
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class MvcSampleFixture : MvcTestFixture
+ where TStartup : class
{
- public MvcSampleFixture()
- : base("samples")
- {
- }
+ public MvcSampleFixture() : base("samples") { }
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs
index 73658b514c..6fb5c1661e 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/MvcTestFixture.cs
@@ -1,71 +1,30 @@
// 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.IO;
-using System.Net.Http;
using System.Reflection;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Mvc.ApplicationParts;
-using Microsoft.AspNetCore.Mvc.Controllers;
-using Microsoft.AspNetCore.Mvc.ViewComponents;
-using Microsoft.AspNetCore.TestHost;
-using Microsoft.AspNetCore.Testing;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.AspNetCore.Mvc.Testing;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
- public class MvcTestFixture : IDisposable
+ public class MvcTestFixture : WebApplicationTestFixture
+ where TStartup : class
{
- private readonly TestServer _server;
-
public MvcTestFixture()
- : this(Path.Combine("test", "WebSites"))
+ : base(Path.Combine("test", "WebSites"))
{
}
protected MvcTestFixture(string solutionRelativePath)
+ : base(solutionRelativePath)
{
- // RequestLocalizationOptions saves the current culture when constructed, potentially changing response
- // localization i.e. RequestLocalizationMiddleware behavior. Ensure the saved culture
- // (DefaultRequestCulture) is consistent regardless of system configuration or personal preferences.
- using (new CultureReplacer())
- {
- var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
- var contentRoot = SolutionPathUtility.GetProjectPath(solutionRelativePath, startupAssembly);
-
- var builder = new WebHostBuilder()
- .UseContentRoot(contentRoot)
- .ConfigureServices(InitializeServices)
- .UseStartup(typeof(TStartup));
-
- _server = new TestServer(builder);
- }
-
- Client = _server.CreateClient();
- Client.BaseAddress = new Uri("http://localhost");
}
- public HttpClient Client { get; }
-
- public void Dispose()
+ protected override void ConfigureApplication(MvcWebApplicationBuilder builder)
{
- Client.Dispose();
- _server.Dispose();
- }
-
- protected virtual void InitializeServices(IServiceCollection services)
- {
- var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
-
- // Inject a custom application part manager. Overrides AddMvcCore() because that uses TryAdd().
- var manager = new ApplicationPartManager();
- manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
-
- manager.FeatureProviders.Add(new ControllerFeatureProvider());
- manager.FeatureProviders.Add(new ViewComponentFeatureProvider());
-
- services.AddSingleton(manager);
+ base.ConfigureApplication(builder);
+ builder.ApplicationAssemblies.Clear();
+ builder.ApplicationAssemblies.Add(typeof(TStartup).GetTypeInfo().Assembly);
}
}
}