From c08504b08ab404e5c58699d94fc092e9e2e7f87a Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 25 Jul 2018 14:30:51 +1200 Subject: [PATCH] MVC startup experience (#8131) --- .../MvcApplicationBuilderExtensions.cs | 92 +++++++++++++++++-- .../Builder/MvcEndpointInfo.cs | 9 +- ...MvcOptionsConfigureCompatibilityOptions.cs | 5 + .../Internal/MvcEndpointDataSource.cs | 15 +-- .../Internal/NullRouter.cs | 27 ++++++ .../MvcOptions.cs | 10 ++ .../ConsumesAttributeGlobalRoutingTests.cs | 20 ++++ .../ConsumesAttributeTests.cs | 20 ++++ .../ConsumesAttributeTestsBase.cs | 3 + .../GlobalRoutingTest.cs | 15 +++ .../RequestServicesGlobalRoutingTest.cs | 20 ++++ .../RequestServicesTest.cs | 20 ++++ .../RequestServicesTestBase.cs | 3 + .../RoutingTests.cs | 15 +++ .../RoutingTestsBase.cs | 3 + .../VersioningGlobalRoutingTests.cs | 20 ++++ .../VersioningTests.cs | 20 ++++ .../VersioningTestsBase.cs | 3 + .../CompatibilitySwitchIntegrationTest.cs | 28 ++++++ .../Controllers/RoutingController.cs | 17 ++++ test/WebSites/BasicWebSite/Startup.cs | 3 + .../BasicWebSite/StartupWithGlobalRouting.cs | 6 +- .../Controllers/RoutingController.cs | 17 ++++ .../StartupWithGlobalRouting.cs | 15 ++- .../Controllers/RoutingController.cs | 17 ++++ .../StartupWithGlobalRouting.cs | 14 +-- 26 files changed, 391 insertions(+), 46 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Internal/NullRouter.cs create mode 100644 test/WebSites/BasicWebSite/Controllers/RoutingController.cs create mode 100644 test/WebSites/RoutingWebSite/Controllers/RoutingController.cs create mode 100644 test/WebSites/VersioningWebSite/Controllers/RoutingController.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs index 74e5f8e5ec..0b190b0231 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -16,6 +18,9 @@ namespace Microsoft.AspNetCore.Builder /// public static class MvcApplicationBuilderExtensions { + // Property key set in routing package by UseGlobalRouting to indicate middleware is registered + private const string GlobalRoutingRegisteredKey = "__GlobalRoutingMiddlewareRegistered"; + /// /// Adds MVC to the request execution pipeline. /// @@ -79,16 +84,91 @@ namespace Microsoft.AspNetCore.Builder VerifyMvcIsRegistered(app); - var routes = new RouteBuilder(app) + var options = app.ApplicationServices.GetRequiredService>(); + + if (options.Value.EnableGlobalRouting) { - DefaultHandler = app.ApplicationServices.GetRequiredService(), - }; + var mvcEndpointDataSource = app.ApplicationServices + .GetRequiredService>() + .OfType() + .First(); + var constraintResolver = app.ApplicationServices + .GetRequiredService(); - configureRoutes(routes); + var endpointRouteBuilder = new EndpointRouteBuilder(app); - routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices)); + configureRoutes(endpointRouteBuilder); - return app.UseRouter(routes.Build()); + foreach (var router in endpointRouteBuilder.Routes) + { + // Only accept Microsoft.AspNetCore.Routing.Route when converting to endpoint + // Sub-types could have additional customization that we can't knowingly convert + if (router is Route route && router.GetType() == typeof(Route)) + { + var endpointInfo = new MvcEndpointInfo( + route.Name, + route.RouteTemplate, + route.Defaults, + route.Constraints.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value), + route.DataTokens, + constraintResolver); + + mvcEndpointDataSource.ConventionalEndpointInfos.Add(endpointInfo); + } + else + { + throw new InvalidOperationException($"Cannot use '{router.GetType().FullName}' with Global Routing."); + } + } + + if (!app.Properties.TryGetValue(GlobalRoutingRegisteredKey, out _)) + { + // Matching middleware has not been registered yet + // For back-compat register middleware so an endpoint is matched and then immediately used + app.UseGlobalRouting(); + } + + return app.UseEndpoint(); + } + else + { + var routes = new RouteBuilder(app) + { + DefaultHandler = app.ApplicationServices.GetRequiredService(), + }; + + configureRoutes(routes); + + routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices)); + + return app.UseRouter(routes.Build()); + } + } + + private class EndpointRouteBuilder : IRouteBuilder + { + public EndpointRouteBuilder(IApplicationBuilder applicationBuilder) + { + ApplicationBuilder = applicationBuilder; + Routes = new List(); + DefaultHandler = NullRouter.Instance; + } + + public IApplicationBuilder ApplicationBuilder { get; } + + public IRouter DefaultHandler { get; set; } + + public IServiceProvider ServiceProvider + { + get { return ApplicationBuilder.ApplicationServices; } + } + + public IList Routes { get; } + + public IRouter Build() + { + throw new NotSupportedException(); + } } public static IApplicationBuilder UseMvcWithEndpoint( diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs index bb6a9df95f..ba59c34b44 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs @@ -94,10 +94,13 @@ namespace Microsoft.AspNetCore.Builder { if (parameter.DefaultValue != null) { - if (result.ContainsKey(parameter.Name)) + if (result.TryGetValue(parameter.Name, out var value)) { - throw new InvalidOperationException( - string.Format(CultureInfo.CurrentCulture, "The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.", parameter.Name)); + if (!object.Equals(value, parameter.DefaultValue)) + { + throw new InvalidOperationException( + string.Format(CultureInfo.CurrentCulture, "The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.", parameter.Name)); + } } else { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcOptionsConfigureCompatibilityOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcOptionsConfigureCompatibilityOptions.cs index 71957b40cf..28fd0605e7 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcOptionsConfigureCompatibilityOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/MvcOptionsConfigureCompatibilityOptions.cs @@ -32,6 +32,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure values[nameof(MvcOptions.SuppressBindingUndefinedValueToEnumType)] = true; } + if (Version >= CompatibilityVersion.Version_2_2) + { + values[nameof(MvcOptions.EnableGlobalRouting)] = true; + } + return values; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs index f725da108b..d6bc8c8f68 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs @@ -247,7 +247,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal // // REVIEW: This is really ugly if (endpointInfo.Constraints.TryGetValue(routeKey, out var constraint) - && !constraint.Match(new DefaultHttpContext() { RequestServices = _serviceProvider }, new DummyRouter(), routeKey, new RouteValueDictionary(action.RouteValues), RouteDirection.IncomingRequest)) + && !constraint.Match(new DefaultHttpContext() { RequestServices = _serviceProvider }, NullRouter.Instance, routeKey, new RouteValueDictionary(action.RouteValues), RouteDirection.IncomingRequest)) { // Did not match constraint return false; @@ -260,19 +260,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal return false; } - private class DummyRouter : IRouter - { - public VirtualPathData GetVirtualPath(VirtualPathContext context) - { - return null; - } - - public Task RouteAsync(RouteContext context) - { - return Task.CompletedTask; - } - } - private MatcherEndpoint CreateEndpoint( ActionDescriptor action, string routeName, diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/NullRouter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/NullRouter.cs new file mode 100644 index 0000000000..c1b800c5bd --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/NullRouter.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.Threading.Tasks; +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + internal class NullRouter : IRouter + { + public static IRouter Instance = new NullRouter(); + + private NullRouter() + { + } + + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + return null; + } + + public Task RouteAsync(RouteContext context) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs index 72634a49fa..e87ddb2678 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs @@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.Mvc private readonly CompatibilitySwitch _allowValidatingTopLevelNodes; private readonly CompatibilitySwitch _inputFormatterExceptionPolicy; private readonly CompatibilitySwitch _suppressBindingUndefinedValueToEnumType; + private readonly CompatibilitySwitch _enableGlobalRouting; private readonly ICompatibilitySwitch[] _switches; /// @@ -54,6 +55,7 @@ namespace Microsoft.AspNetCore.Mvc _allowValidatingTopLevelNodes = new CompatibilitySwitch(nameof(AllowValidatingTopLevelNodes)); _inputFormatterExceptionPolicy = new CompatibilitySwitch(nameof(InputFormatterExceptionPolicy), InputFormatterExceptionPolicy.AllExceptions); _suppressBindingUndefinedValueToEnumType = new CompatibilitySwitch(nameof(SuppressBindingUndefinedValueToEnumType)); + _enableGlobalRouting = new CompatibilitySwitch(nameof(EnableGlobalRouting)); _switches = new ICompatibilitySwitch[] { @@ -62,9 +64,17 @@ namespace Microsoft.AspNetCore.Mvc _allowValidatingTopLevelNodes, _inputFormatterExceptionPolicy, _suppressBindingUndefinedValueToEnumType, + _enableGlobalRouting, }; } + // REVIEW: Add documentation + public bool EnableGlobalRouting + { + get => _enableGlobalRouting.Value; + set => _enableGlobalRouting.Value = value; + } + /// /// Gets or sets the flag which decides whether body model binding (for example, on an /// action method parameter with ) should treat empty diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeGlobalRoutingTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeGlobalRoutingTests.cs index 889e77e80b..1683864c17 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeGlobalRoutingTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeGlobalRoutingTests.cs @@ -1,6 +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.Net; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Xunit; + namespace Microsoft.AspNetCore.Mvc.FunctionalTests { public class ConsumesAttributeGlobalRoutingTests : ConsumesAttributeTestsBase @@ -9,5 +14,20 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests : base(fixture) { } + + [Fact] + public async override Task HasEndpointMatch() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/Routing/HasEndpointMatch"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.True(result); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs index a8db21d806..d694aad1c4 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTests.cs @@ -1,6 +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.Net; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Xunit; + namespace Microsoft.AspNetCore.Mvc.FunctionalTests { public class ConsumesAttributeTests : ConsumesAttributeTestsBase @@ -9,5 +14,20 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests : base(fixture) { } + + [Fact] + public async override Task HasEndpointMatch() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/Routing/HasEndpointMatch"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.False(result); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs index 9a414488b1..acc68febbc 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs @@ -28,6 +28,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public HttpClient Client { get; } + [Fact] + public abstract Task HasEndpointMatch(); + [Fact] public async Task NoRequestContentType_SelectsActionWithoutConstraint() { diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/GlobalRoutingTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/GlobalRoutingTest.cs index 1ab9950d68..a95147250d 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/GlobalRoutingTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/GlobalRoutingTest.cs @@ -16,6 +16,21 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { } + [Fact] + public async override Task HasEndpointMatch() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/Routing/HasEndpointMatch"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.True(result); + } + [Fact(Skip = "Link generation issue in global routing. Need to fix - https://github.com/aspnet/Routing/issues/590")] public override Task AttributeRoutedAction_InArea_StaysInArea_ActionDoesntExist() { diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesGlobalRoutingTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesGlobalRoutingTest.cs index 0c1471df03..0ca18c250e 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesGlobalRoutingTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesGlobalRoutingTest.cs @@ -1,6 +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.Net; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Xunit; + namespace Microsoft.AspNetCore.Mvc.FunctionalTests { public class RequestServicesGlobalRoutingTest : RequestServicesTestBase @@ -9,5 +14,20 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests : base(fixture) { } + + [Fact] + public async override Task HasEndpointMatch() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/Routing/HasEndpointMatch"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.True(result); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTest.cs index f62521bd6c..73c0e5af15 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTest.cs @@ -1,6 +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.Net; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Xunit; + namespace Microsoft.AspNetCore.Mvc.FunctionalTests { public class RequestServicesTest : RequestServicesTestBase @@ -9,5 +14,20 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests : base(fixture) { } + + [Fact] + public async override Task HasEndpointMatch() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/Routing/HasEndpointMatch"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.False(result); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTestBase.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTestBase.cs index 425bda5ea5..cc4e06ba74 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTestBase.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestServicesTestBase.cs @@ -26,6 +26,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public HttpClient Client { get; } + [Fact] + public abstract Task HasEndpointMatch(); + [Theory] [InlineData("http://localhost/RequestScopedService/FromFilter")] [InlineData("http://localhost/RequestScopedService/FromView")] diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTests.cs index e4c3f088ea..a4fbb4ab58 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTests.cs @@ -17,6 +17,21 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { } + [Fact] + public async override Task HasEndpointMatch() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/Routing/HasEndpointMatch"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.False(result); + } + [Fact] public async override Task RouteData_Routers_ConventionalRoute() { diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs index 20918254b9..f707655dce 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs @@ -28,6 +28,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public HttpClient Client { get; } + [Fact] + public abstract Task HasEndpointMatch(); + [Fact] public abstract Task RouteData_Routers_ConventionalRoute(); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningGlobalRoutingTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningGlobalRoutingTests.cs index 8c022a076c..af2147949f 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningGlobalRoutingTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningGlobalRoutingTests.cs @@ -1,6 +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.Net; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Xunit; + namespace Microsoft.AspNetCore.Mvc.FunctionalTests { public class VersioningGlobalRoutingTests : VersioningTestsBase @@ -9,5 +14,20 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests : base(fixture) { } + + [Fact] + public async override Task HasEndpointMatch() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/Routing/HasEndpointMatch"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.True(result); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTests.cs index 8490fda117..40e14bdc57 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTests.cs @@ -1,6 +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.Net; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Xunit; + namespace Microsoft.AspNetCore.Mvc.FunctionalTests { public class VersioningTests : VersioningTestsBase @@ -9,5 +14,20 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests : base(fixture) { } + + [Fact] + public async override Task HasEndpointMatch() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/Routing/HasEndpointMatch"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.False(result); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTestsBase.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTestsBase.cs index 46cfdbe4a8..152c08a799 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTestsBase.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTestsBase.cs @@ -25,6 +25,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public HttpClient Client { get; } + [Fact] + public abstract Task HasEndpointMatch(); + [Theory] [InlineData("1")] [InlineData("2")] diff --git a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs index 998343d7bb..78f8ddb4ae 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/IntegrationTest/CompatibilitySwitchIntegrationTest.cs @@ -39,6 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.Equal(InputFormatterExceptionPolicy.AllExceptions, mvcOptions.InputFormatterExceptionPolicy); Assert.False(jsonOptions.AllowInputFormatterExceptionMessages); Assert.False(razorPagesOptions.AllowAreas); + Assert.False(mvcOptions.EnableGlobalRouting); } [Fact] @@ -63,6 +64,32 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.Equal(InputFormatterExceptionPolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionPolicy); Assert.True(jsonOptions.AllowInputFormatterExceptionMessages); Assert.True(razorPagesOptions.AllowAreas); + Assert.False(mvcOptions.EnableGlobalRouting); + } + + [Fact] + public void CompatibilitySwitches_Version_2_2() + { + // Arrange + var serviceCollection = new ServiceCollection(); + AddHostingServices(serviceCollection); + serviceCollection.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); + + var services = serviceCollection.BuildServiceProvider(); + + // Act + var mvcOptions = services.GetRequiredService>().Value; + var jsonOptions = services.GetRequiredService>().Value; + var razorPagesOptions = services.GetRequiredService>().Value; + + // Assert + Assert.True(mvcOptions.AllowCombiningAuthorizeFilters); + Assert.True(mvcOptions.AllowBindingHeaderValuesToNonStringModelTypes); + Assert.True(mvcOptions.SuppressBindingUndefinedValueToEnumType); + Assert.Equal(InputFormatterExceptionPolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionPolicy); + Assert.True(jsonOptions.AllowInputFormatterExceptionMessages); + Assert.True(razorPagesOptions.AllowAreas); + Assert.True(mvcOptions.EnableGlobalRouting); } [Fact] @@ -87,6 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest Assert.Equal(InputFormatterExceptionPolicy.MalformedInputExceptions, mvcOptions.InputFormatterExceptionPolicy); Assert.True(jsonOptions.AllowInputFormatterExceptionMessages); Assert.True(razorPagesOptions.AllowAreas); + Assert.True(mvcOptions.EnableGlobalRouting); } // This just does the minimum needed to be able to resolve these options. diff --git a/test/WebSites/BasicWebSite/Controllers/RoutingController.cs b/test/WebSites/BasicWebSite/Controllers/RoutingController.cs new file mode 100644 index 0000000000..afe914f572 --- /dev/null +++ b/test/WebSites/BasicWebSite/Controllers/RoutingController.cs @@ -0,0 +1,17 @@ +// 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 Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace BasicWebSite +{ + public class RoutingController : Controller + { + public ActionResult HasEndpointMatch() + { + var endpointFeature = HttpContext.Features.Get(); + return Json(endpointFeature?.Endpoint != null); + } + } +} \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Startup.cs b/test/WebSites/BasicWebSite/Startup.cs index 9e20cb447c..c6e406fb34 100644 --- a/test/WebSites/BasicWebSite/Startup.cs +++ b/test/WebSites/BasicWebSite/Startup.cs @@ -26,6 +26,9 @@ namespace BasicWebSite options.Conventions.Add(new ApplicationDescription("This is a basic website.")); // Filter that records a value in HttpContext.Items options.Filters.Add(new TraceResourceFilter()); + + // Remove when all URL generation tests are passing - https://github.com/aspnet/Routing/issues/590 + options.EnableGlobalRouting = false; }) .SetCompatibilityVersion(CompatibilityVersion.Latest) .AddXmlDataContractSerializerFormatters(); diff --git a/test/WebSites/BasicWebSite/StartupWithGlobalRouting.cs b/test/WebSites/BasicWebSite/StartupWithGlobalRouting.cs index 037a5312f8..d3a540a86c 100644 --- a/test/WebSites/BasicWebSite/StartupWithGlobalRouting.cs +++ b/test/WebSites/BasicWebSite/StartupWithGlobalRouting.cs @@ -15,7 +15,7 @@ namespace BasicWebSite services.AddRouting(); services.AddMvc() - .SetCompatibilityVersion(CompatibilityVersion.Latest) + .SetCompatibilityVersion(CompatibilityVersion.Latest) // this compat version enables global routing .AddXmlDataContractSerializerFormatters(); services.ConfigureBaseWebSiteAuthPolicies(); @@ -31,9 +31,9 @@ namespace BasicWebSite app.UseGlobalRouting(); - app.UseMvcWithEndpoint(routes => + app.UseMvc(routes => { - routes.MapEndpoint( + routes.MapRoute( "ActionAsMethod", "{controller}/{action}", defaults: new { controller = "Home", action = "Index" }); diff --git a/test/WebSites/RoutingWebSite/Controllers/RoutingController.cs b/test/WebSites/RoutingWebSite/Controllers/RoutingController.cs new file mode 100644 index 0000000000..354ff5539a --- /dev/null +++ b/test/WebSites/RoutingWebSite/Controllers/RoutingController.cs @@ -0,0 +1,17 @@ +// 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 Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace RoutingWebSite +{ + public class RoutingController : Controller + { + public ActionResult HasEndpointMatch() + { + var endpointFeature = HttpContext.Features.Get(); + return Json(endpointFeature?.Endpoint != null); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RoutingWebSite/StartupWithGlobalRouting.cs b/test/WebSites/RoutingWebSite/StartupWithGlobalRouting.cs index abc0abf4fb..62a84f5ecb 100644 --- a/test/WebSites/RoutingWebSite/StartupWithGlobalRouting.cs +++ b/test/WebSites/RoutingWebSite/StartupWithGlobalRouting.cs @@ -12,9 +12,8 @@ namespace RoutingWebSite // Set up application services public void ConfigureServices(IServiceCollection services) { - services.AddRouting(); - - services.AddMvc(); + services.AddMvc() + .AddMvcOptions(options => options.EnableGlobalRouting = true); services.AddScoped(); services.AddSingleton(); @@ -22,23 +21,21 @@ namespace RoutingWebSite public void Configure(IApplicationBuilder app) { - app.UseGlobalRouting(); - - app.UseMvcWithEndpoint(routes => + app.UseMvc(routes => { - routes.MapAreaEndpoint( + routes.MapAreaRoute( "flightRoute", "adminRoute", "{area:exists}/{controller}/{action}", new { controller = "Home", action = "Index" }, new { area = "Travel" }); - routes.MapEndpoint( + routes.MapRoute( "ActionAsMethod", "{controller}/{action}", defaults: new { controller = "Home", action = "Index" }); - routes.MapEndpoint( + routes.MapRoute( "RouteWithOptionalSegment", "{controller}/{action}/{path?}"); }); diff --git a/test/WebSites/VersioningWebSite/Controllers/RoutingController.cs b/test/WebSites/VersioningWebSite/Controllers/RoutingController.cs new file mode 100644 index 0000000000..aad2099d1d --- /dev/null +++ b/test/WebSites/VersioningWebSite/Controllers/RoutingController.cs @@ -0,0 +1,17 @@ +// 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 Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace VersioningWebSite +{ + public class RoutingController : Controller + { + public ActionResult HasEndpointMatch() + { + var endpointFeature = HttpContext.Features.Get(); + return Json(endpointFeature?.Endpoint != null); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/StartupWithGlobalRouting.cs b/test/WebSites/VersioningWebSite/StartupWithGlobalRouting.cs index c7ea9739f7..249aefabd4 100644 --- a/test/WebSites/VersioningWebSite/StartupWithGlobalRouting.cs +++ b/test/WebSites/VersioningWebSite/StartupWithGlobalRouting.cs @@ -13,10 +13,9 @@ namespace VersioningWebSite { public void ConfigureServices(IServiceCollection services) { - services.AddRouting(); - // Add MVC services to the services container - services.AddMvc(); + services.AddMvc() + .AddMvcOptions(options => options.EnableGlobalRouting = true); services.AddScoped(); services.AddSingleton(); @@ -24,14 +23,7 @@ namespace VersioningWebSite public void Configure(IApplicationBuilder app) { - app.UseGlobalRouting(); - - app.UseMvcWithEndpoint(endpoints => - { - endpoints.MapEndpoint( - name: "default", - template: "{controller=Home}/{action=Index}/{id?}"); - }); + app.UseMvcWithDefaultRoute(); } } } \ No newline at end of file