MVC startup experience (#8131)

This commit is contained in:
James Newton-King 2018-07-25 14:30:51 +12:00 committed by GitHub
parent 630aeade07
commit c08504b08a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 391 additions and 46 deletions

View File

@ -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
/// </summary>
public static class MvcApplicationBuilderExtensions
{
// Property key set in routing package by UseGlobalRouting to indicate middleware is registered
private const string GlobalRoutingRegisteredKey = "__GlobalRoutingMiddlewareRegistered";
/// <summary>
/// Adds MVC to the <see cref="IApplicationBuilder"/> request execution pipeline.
/// </summary>
@ -79,16 +84,91 @@ namespace Microsoft.AspNetCore.Builder
VerifyMvcIsRegistered(app);
var routes = new RouteBuilder(app)
var options = app.ApplicationServices.GetRequiredService<IOptions<MvcOptions>>();
if (options.Value.EnableGlobalRouting)
{
DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(),
};
var mvcEndpointDataSource = app.ApplicationServices
.GetRequiredService<IEnumerable<EndpointDataSource>>()
.OfType<MvcEndpointDataSource>()
.First();
var constraintResolver = app.ApplicationServices
.GetRequiredService<IInlineConstraintResolver>();
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<MvcRouteHandler>(),
};
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<IRouter>();
DefaultHandler = NullRouter.Instance;
}
public IApplicationBuilder ApplicationBuilder { get; }
public IRouter DefaultHandler { get; set; }
public IServiceProvider ServiceProvider
{
get { return ApplicationBuilder.ApplicationServices; }
}
public IList<IRouter> Routes { get; }
public IRouter Build()
{
throw new NotSupportedException();
}
}
public static IApplicationBuilder UseMvcWithEndpoint(

View File

@ -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
{

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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;
}
}
}

View File

@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.Mvc
private readonly CompatibilitySwitch<bool> _allowValidatingTopLevelNodes;
private readonly CompatibilitySwitch<InputFormatterExceptionPolicy> _inputFormatterExceptionPolicy;
private readonly CompatibilitySwitch<bool> _suppressBindingUndefinedValueToEnumType;
private readonly CompatibilitySwitch<bool> _enableGlobalRouting;
private readonly ICompatibilitySwitch[] _switches;
/// <summary>
@ -54,6 +55,7 @@ namespace Microsoft.AspNetCore.Mvc
_allowValidatingTopLevelNodes = new CompatibilitySwitch<bool>(nameof(AllowValidatingTopLevelNodes));
_inputFormatterExceptionPolicy = new CompatibilitySwitch<InputFormatterExceptionPolicy>(nameof(InputFormatterExceptionPolicy), InputFormatterExceptionPolicy.AllExceptions);
_suppressBindingUndefinedValueToEnumType = new CompatibilitySwitch<bool>(nameof(SuppressBindingUndefinedValueToEnumType));
_enableGlobalRouting = new CompatibilitySwitch<bool>(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;
}
/// <summary>
/// Gets or sets the flag which decides whether body model binding (for example, on an
/// action method parameter with <see cref="FromBodyAttribute"/>) should treat empty

View File

@ -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<BasicWebSite.StartupWithGlobalRouting>
@ -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<bool>(body);
Assert.True(result);
}
}
}

View File

@ -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<BasicWebSite.Startup>
@ -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<bool>(body);
Assert.False(result);
}
}
}

View File

@ -28,6 +28,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public HttpClient Client { get; }
[Fact]
public abstract Task HasEndpointMatch();
[Fact]
public async Task NoRequestContentType_SelectsActionWithoutConstraint()
{

View File

@ -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<bool>(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()
{

View File

@ -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<BasicWebSite.StartupWithGlobalRouting>
@ -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<bool>(body);
Assert.True(result);
}
}
}

View File

@ -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<BasicWebSite.Startup>
@ -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<bool>(body);
Assert.False(result);
}
}
}

View File

@ -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")]

View File

@ -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<bool>(body);
Assert.False(result);
}
[Fact]
public async override Task RouteData_Routers_ConventionalRoute()
{

View File

@ -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();

View File

@ -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<VersioningWebSite.StartupWithGlobalRouting>
@ -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<bool>(body);
Assert.True(result);
}
}
}

View File

@ -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<VersioningWebSite.Startup>
@ -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<bool>(body);
Assert.False(result);
}
}
}

View File

@ -25,6 +25,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public HttpClient Client { get; }
[Fact]
public abstract Task HasEndpointMatch();
[Theory]
[InlineData("1")]
[InlineData("2")]

View File

@ -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<IOptions<MvcOptions>>().Value;
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().Value;
var razorPagesOptions = services.GetRequiredService<IOptions<RazorPagesOptions>>().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.

View File

@ -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<IEndpointFeature>();
return Json(endpointFeature?.Endpoint != null);
}
}
}

View File

@ -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();

View File

@ -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" });

View File

@ -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<IEndpointFeature>();
return Json(endpointFeature?.Endpoint != null);
}
}
}

View File

@ -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<TestResponseGenerator>();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
@ -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?}");
});

View File

@ -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<IEndpointFeature>();
return Json(endpointFeature?.Endpoint != null);
}
}
}

View File

@ -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<TestResponseGenerator>();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
@ -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();
}
}
}