React to routing breaking changes

This commit is contained in:
Ryan Nowak 2015-12-11 09:29:54 -08:00
parent f2bb90fa55
commit ed93a6c812
8 changed files with 94 additions and 187 deletions

View File

@ -59,73 +59,61 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
return; return;
} }
// Replacing the route data allows any code running here to dirty the route values or data-tokens
// without affecting something upstream.
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
if (actionDescriptor.RouteValueDefaults != null) if (actionDescriptor.RouteValueDefaults != null)
{ {
foreach (var kvp in actionDescriptor.RouteValueDefaults) foreach (var kvp in actionDescriptor.RouteValueDefaults)
{ {
if (!newRouteData.Values.ContainsKey(kvp.Key)) if (!context.RouteData.Values.ContainsKey(kvp.Key))
{ {
newRouteData.Values.Add(kvp.Key, kvp.Value); context.RouteData.Values.Add(kvp.Key, kvp.Value);
} }
} }
} }
// Removing RouteGroup from RouteValues to simulate the result of conventional routing // Removing RouteGroup from RouteValues to simulate the result of conventional routing
newRouteData.Values.Remove(TreeRouter.RouteGroupKey); context.RouteData.Values.Remove(TreeRouter.RouteGroupKey);
context.Handler = (c) => InvokeActionAsync(c, actionDescriptor);
}
private async Task InvokeActionAsync(HttpContext httpContext, ActionDescriptor actionDescriptor)
{
var routeData = httpContext.GetRouteData();
try try
{ {
context.RouteData = newRouteData; _diagnosticSource.BeforeAction(actionDescriptor, httpContext, routeData);
_diagnosticSource.BeforeAction(actionDescriptor, context.HttpContext, context.RouteData);
using (_logger.ActionScope(actionDescriptor)) using (_logger.ActionScope(actionDescriptor))
{ {
_logger.ExecutingAction(actionDescriptor); _logger.ExecutingAction(actionDescriptor);
var startTime = Environment.TickCount; var startTime = Environment.TickCount;
await InvokeActionAsync(context, actionDescriptor);
context.IsHandled = true; var actionContext = new ActionContext(httpContext, routeData, actionDescriptor);
if (_actionContextAccessor != null)
{
_actionContextAccessor.ActionContext = actionContext;
}
var invoker = _actionInvokerFactory.CreateInvoker(actionContext);
if (invoker == null)
{
throw new InvalidOperationException(
Resources.FormatActionInvokerFactory_CouldNotCreateInvoker(
actionDescriptor.DisplayName));
}
await invoker.InvokeAsync();
_logger.ExecutedAction(actionDescriptor, startTime); _logger.ExecutedAction(actionDescriptor, startTime);
} }
} }
finally finally
{ {
_diagnosticSource.AfterAction(actionDescriptor, context.HttpContext, context.RouteData); _diagnosticSource.AfterAction(actionDescriptor, httpContext, routeData);
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
} }
} }
private Task InvokeActionAsync(RouteContext context, ActionDescriptor actionDescriptor)
{
var actionContext = new ActionContext(context.HttpContext, context.RouteData, actionDescriptor);
if (_actionContextAccessor != null)
{
_actionContextAccessor.ActionContext = actionContext;
}
var invoker = _actionInvokerFactory.CreateInvoker(actionContext);
if (invoker == null)
{
throw new InvalidOperationException(
Resources.FormatActionInvokerFactory_CouldNotCreateInvoker(
actionDescriptor.DisplayName));
}
return invoker.InvokeAsync();
}
private void EnsureServices(HttpContext context) private void EnsureServices(HttpContext context)
{ {
if (_servicesRetrieved) if (_servicesRetrieved)

View File

@ -282,9 +282,14 @@ namespace Microsoft.AspNet.Mvc.Routing
routeInfo.Constraints = constraintBuilder.Build(); routeInfo.Constraints = constraintBuilder.Build();
routeInfo.Defaults = routeInfo.ParsedTemplate.Parameters routeInfo.Defaults = new RouteValueDictionary();
.Where(p => p.DefaultValue != null) foreach (var parameter in routeInfo.ParsedTemplate.Parameters)
.ToDictionary(p => p.Name, p => p.DefaultValue, StringComparer.OrdinalIgnoreCase); {
if (parameter.DefaultValue != null)
{
routeInfo.Defaults.Add(parameter.Name, parameter.DefaultValue);
}
}
return routeInfo; return routeInfo;
} }
@ -293,9 +298,9 @@ namespace Microsoft.AspNet.Mvc.Routing
{ {
public ActionDescriptor ActionDescriptor { get; set; } public ActionDescriptor ActionDescriptor { get; set; }
public IReadOnlyDictionary<string, IRouteConstraint> Constraints { get; set; } public IDictionary<string, IRouteConstraint> Constraints { get; set; }
public IReadOnlyDictionary<string, object> Defaults { get; set; } public RouteValueDictionary Defaults { get; set; }
public string ErrorMessage { get; set; } public string ErrorMessage { get; set; }

View File

@ -2,11 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Constraints; using Microsoft.AspNet.Routing.Constraints;
using Microsoft.AspNet.Routing.Template;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Moq; using Moq;
using Xunit; using Xunit;
@ -29,7 +27,7 @@ namespace Microsoft.AspNet.Builder
builder.MapAreaRoute(name: null, areaName: "admin", template: "site/Admin/"); builder.MapAreaRoute(name: null, areaName: "admin", template: "site/Admin/");
// Assert // Assert
var route = Assert.IsType<TemplateRoute>((Assert.Single(builder.Routes))); var route = Assert.IsType<Route>((Assert.Single(builder.Routes)));
Assert.Null(route.Name); Assert.Null(route.Name);
Assert.Equal("site/Admin/", route.RouteTemplate); Assert.Equal("site/Admin/", route.RouteTemplate);
@ -68,7 +66,7 @@ namespace Microsoft.AspNet.Builder
defaults: new { action = "Home" }); defaults: new { action = "Home" });
// Assert // Assert
var route = Assert.IsType<TemplateRoute>((Assert.Single(builder.Routes))); var route = Assert.IsType<Route>((Assert.Single(builder.Routes)));
Assert.Equal("admin_area", route.Name); Assert.Equal("admin_area", route.Name);
Assert.Equal("site/Admin/", route.RouteTemplate); Assert.Equal("site/Admin/", route.RouteTemplate);
@ -113,7 +111,7 @@ namespace Microsoft.AspNet.Builder
constraints: new { id = new IntRouteConstraint() }); constraints: new { id = new IntRouteConstraint() });
// Assert // Assert
var route = Assert.IsType<TemplateRoute>((Assert.Single(builder.Routes))); var route = Assert.IsType<Route>((Assert.Single(builder.Routes)));
Assert.Equal("admin_area", route.Name); Assert.Equal("admin_area", route.Name);
Assert.Equal("site/Admin/", route.RouteTemplate); Assert.Equal("site/Admin/", route.RouteTemplate);
@ -164,7 +162,7 @@ namespace Microsoft.AspNet.Builder
dataTokens: new { some_token = "hello" }); dataTokens: new { some_token = "hello" });
// Assert // Assert
var route = Assert.IsType<TemplateRoute>((Assert.Single(builder.Routes))); var route = Assert.IsType<Route>((Assert.Single(builder.Routes)));
Assert.Equal("admin_area", route.Name); Assert.Equal("admin_area", route.Name);
Assert.Equal("site/Admin/", route.RouteTemplate); Assert.Equal("site/Admin/", route.RouteTemplate);
@ -221,7 +219,7 @@ namespace Microsoft.AspNet.Builder
dataTokens: new { some_token = "hello" }); dataTokens: new { some_token = "hello" });
// Assert // Assert
var route = Assert.IsType<TemplateRoute>((Assert.Single(builder.Routes))); var route = Assert.IsType<Route>((Assert.Single(builder.Routes)));
Assert.Equal("admin_area", route.Name); Assert.Equal("admin_area", route.Name);
Assert.Equal("site/Admin/", route.RouteTemplate); Assert.Equal("site/Admin/", route.RouteTemplate);

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
public class MvcRouteHandlerTests public class MvcRouteHandlerTests
{ {
[Fact] [Fact]
public async Task RouteAsync_Success_LogsCorrectValues() public async Task RouteHandler_Success_LogsCorrectValues()
{ {
// Arrange // Arrange
var sink = new TestSink(); var sink = new TestSink();
@ -35,9 +35,10 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
var context = CreateRouteContext(actionDescriptor: actionDescriptor.Object, loggerFactory: loggerFactory); var context = CreateRouteContext(actionDescriptor: actionDescriptor.Object, loggerFactory: loggerFactory);
var handler = new MvcRouteHandler(); var handler = new MvcRouteHandler();
await handler.RouteAsync(context);
// Act // Act
await handler.RouteAsync(context); await context.Handler(context.HttpContext);
// Assert // Assert
Assert.Single(sink.Scopes); Assert.Single(sink.Scopes);
@ -77,10 +78,9 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
} }
[Fact] [Fact]
public async Task RouteAsync_CreatesNewRouteData() public async Task RouteHandler_RemovesRouteGroupFromRouteValues()
{ {
// Arrange // Arrange
RouteData actionRouteData = null;
var invoker = new Mock<IActionInvoker>(); var invoker = new Mock<IActionInvoker>();
invoker invoker
.Setup(i => i.InvokeAsync()) .Setup(i => i.InvokeAsync())
@ -91,49 +91,6 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
.Setup(f => f.CreateInvoker(It.IsAny<ActionContext>())) .Setup(f => f.CreateInvoker(It.IsAny<ActionContext>()))
.Returns<ActionContext>((c) => .Returns<ActionContext>((c) =>
{ {
actionRouteData = c.RouteData;
return invoker.Object;
});
var initialRouter = Mock.Of<IRouter>();
var context = CreateRouteContext(invokerFactory: invokerFactory.Object);
var handler = new MvcRouteHandler();
var originalRouteData = context.RouteData;
originalRouteData.Routers.Add(initialRouter);
originalRouteData.Values.Add("action", "Index");
// Act
await handler.RouteAsync(context);
// Assert
Assert.NotSame(originalRouteData, context.RouteData);
Assert.NotSame(originalRouteData, actionRouteData);
Assert.Same(actionRouteData, context.RouteData);
// The new routedata is a copy
Assert.Equal("Index", context.RouteData.Values["action"]);
Assert.Equal(initialRouter, Assert.Single(context.RouteData.Routers));
}
[Fact]
public async Task RouteAsync_RemovesRouteGroupFromRouteValues()
{
// Arrange
RouteData actionRouteData = null;
var invoker = new Mock<IActionInvoker>();
invoker
.Setup(i => i.InvokeAsync())
.Returns(Task.FromResult(true));
var invokerFactory = new Mock<IActionInvokerFactory>();
invokerFactory
.Setup(f => f.CreateInvoker(It.IsAny<ActionContext>()))
.Returns<ActionContext>((c) =>
{
actionRouteData = c.RouteData;
return invoker.Object; return invoker.Object;
}); });
@ -147,59 +104,14 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
await handler.RouteAsync(context); await handler.RouteAsync(context);
// Assert // Assert
Assert.NotSame(originalRouteData, context.RouteData); Assert.Same(originalRouteData, context.RouteData);
Assert.NotSame(originalRouteData, actionRouteData);
Assert.Same(actionRouteData, context.RouteData);
// The new routedata is a copy
Assert.False(context.RouteData.Values.ContainsKey(TreeRouter.RouteGroupKey)); Assert.False(context.RouteData.Values.ContainsKey(TreeRouter.RouteGroupKey));
} }
[Fact] [Fact]
public async Task RouteAsync_ResetsRouteDataOnException() public async Task RouteHandler_WritesDiagnostic_ActionSelected()
{
// Arrange
RouteData actionRouteData = null;
var invoker = new Mock<IActionInvoker>();
invoker
.Setup(i => i.InvokeAsync())
.Throws(new Exception());
var invokerFactory = new Mock<IActionInvokerFactory>();
invokerFactory
.Setup(f => f.CreateInvoker(It.IsAny<ActionContext>()))
.Returns<ActionContext>((c) =>
{
actionRouteData = c.RouteData;
c.RouteData.Values.Add("action", "Index");
return invoker.Object;
});
var context = CreateRouteContext(invokerFactory: invokerFactory.Object);
var handler = new MvcRouteHandler();
var initialRouter = Mock.Of<IRouter>();
var originalRouteData = context.RouteData;
originalRouteData.Routers.Add(initialRouter);
// Act
await Assert.ThrowsAsync<Exception>(() => handler.RouteAsync(context));
// Assert
Assert.Same(originalRouteData, context.RouteData);
Assert.NotSame(originalRouteData, actionRouteData);
Assert.NotSame(actionRouteData, context.RouteData);
// The new routedata is a copy
Assert.Null(context.RouteData.Values["action"]);
Assert.Equal("Index", actionRouteData.Values["action"]);
Assert.Equal(initialRouter, Assert.Single(actionRouteData.Routers));
}
[Fact]
public async Task RouteAsync_WritesDiagnostic_ActionSelected()
{ {
// Arrange // Arrange
var listener = new TestDiagnosticListener(); var listener = new TestDiagnosticListener();
@ -208,9 +120,10 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
context.RouteData.Values.Add("tag", "value"); context.RouteData.Values.Add("tag", "value");
var handler = new MvcRouteHandler(); var handler = new MvcRouteHandler();
await handler.RouteAsync(context);
// Act // Act
await handler.RouteAsync(context); await context.Handler(context.HttpContext);
// Assert // Assert
Assert.NotNull(listener.BeforeAction?.ActionDescriptor); Assert.NotNull(listener.BeforeAction?.ActionDescriptor);
@ -224,7 +137,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
} }
[Fact] [Fact]
public async Task RouteAsync_WritesDiagnostic_ActionInvoked() public async Task RouteHandler_WritesDiagnostic_ActionInvoked()
{ {
// Arrange // Arrange
var listener = new TestDiagnosticListener(); var listener = new TestDiagnosticListener();
@ -232,9 +145,10 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
var context = CreateRouteContext(diagnosticListener: listener); var context = CreateRouteContext(diagnosticListener: listener);
var handler = new MvcRouteHandler(); var handler = new MvcRouteHandler();
await handler.RouteAsync(context);
// Act // Act
await handler.RouteAsync(context); await context.Handler(context.HttpContext);
// Assert // Assert
Assert.NotNull(listener.AfterAction?.ActionDescriptor); Assert.NotNull(listener.AfterAction?.ActionDescriptor);
@ -293,22 +207,42 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
diagnosticSource.SubscribeWithAdapter(diagnosticListener); diagnosticSource.SubscribeWithAdapter(diagnosticListener);
} }
var routingFeature = new RoutingFeature();
var httpContext = new Mock<HttpContext>(); var httpContext = new Mock<HttpContext>();
httpContext.Setup(h => h.RequestServices.GetService(typeof(IActionContextAccessor))) httpContext
.Setup(h => h.RequestServices.GetService(typeof(IActionContextAccessor)))
.Returns(new ActionContextAccessor()); .Returns(new ActionContextAccessor());
httpContext.Setup(h => h.RequestServices.GetService(typeof(IActionSelector))) httpContext
.Setup(h => h.RequestServices.GetService(typeof(IActionSelector)))
.Returns(actionSelector); .Returns(actionSelector);
httpContext.Setup(h => h.RequestServices.GetService(typeof(IActionInvokerFactory))) httpContext
.Setup(h => h.RequestServices.GetService(typeof(IActionInvokerFactory)))
.Returns(invokerFactory); .Returns(invokerFactory);
httpContext.Setup(h => h.RequestServices.GetService(typeof(ILoggerFactory))) httpContext
.Setup(h => h.RequestServices.GetService(typeof(ILoggerFactory)))
.Returns(loggerFactory); .Returns(loggerFactory);
httpContext.Setup(h => h.RequestServices.GetService(typeof(MvcMarkerService))) httpContext
.Returns(new MvcMarkerService()); .Setup(h => h.RequestServices.GetService(typeof(MvcMarkerService)))
httpContext.Setup(h => h.RequestServices.GetService(typeof(IOptions<MvcOptions>))) .Returns(new MvcMarkerService());
.Returns(optionsAccessor); httpContext
httpContext.Setup(h => h.RequestServices.GetService(typeof(DiagnosticSource))) .Setup(h => h.RequestServices.GetService(typeof(IOptions<MvcOptions>)))
.Returns(optionsAccessor);
httpContext
.Setup(h => h.RequestServices.GetService(typeof(DiagnosticSource)))
.Returns(diagnosticSource); .Returns(diagnosticSource);
return new RouteContext(httpContext.Object); httpContext
.Setup(h => h.Features[typeof(IRoutingFeature)])
.Returns(routingFeature);
var routeContext = new RouteContext(httpContext.Object);
routingFeature.RouteData = routeContext.RouteData;
return routeContext;
}
private class RoutingFeature : IRoutingFeature
{
public RouteData RouteData { get; set; }
} }
} }
} }

View File

@ -19,6 +19,8 @@ namespace Microsoft.AspNet.Mvc.Routing
{ {
public class AttributeRouteTest public class AttributeRouteTest
{ {
private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0);
// This test verifies that AttributeRoute can respond to changes in the AD collection. It does this // This test verifies that AttributeRoute can respond to changes in the AD collection. It does this
// by running a successful request, then removing that action and verifying the next route isn't // by running a successful request, then removing that action and verifying the next route isn't
// successful. // successful.
@ -29,7 +31,7 @@ namespace Microsoft.AspNet.Mvc.Routing
var handler = new Mock<IRouter>(MockBehavior.Strict); var handler = new Mock<IRouter>(MockBehavior.Strict);
handler handler
.Setup(h => h.RouteAsync(It.IsAny<RouteContext>())) .Setup(h => h.RouteAsync(It.IsAny<RouteContext>()))
.Callback<RouteContext>(c => c.IsHandled = true) .Callback<RouteContext>(c => c.Handler = NullHandler)
.Returns(Task.FromResult(true)) .Returns(Task.FromResult(true))
.Verifiable(); .Verifiable();
@ -85,7 +87,7 @@ namespace Microsoft.AspNet.Mvc.Routing
await route.RouteAsync(context); await route.RouteAsync(context);
// Assert 1 // Assert 1
Assert.True(context.IsHandled); Assert.NotNull(context.Handler);
Assert.Equal("5", context.RouteData.Values["id"]); Assert.Equal("5", context.RouteData.Values["id"]);
Assert.Equal("2", context.RouteData.Values[TreeRouter.RouteGroupKey]); Assert.Equal("2", context.RouteData.Values[TreeRouter.RouteGroupKey]);
@ -103,7 +105,7 @@ namespace Microsoft.AspNet.Mvc.Routing
await route.RouteAsync(context); await route.RouteAsync(context);
// Assert 2 // Assert 2
Assert.False(context.IsHandled); Assert.Null(context.Handler);
Assert.Empty(context.RouteData.Values); Assert.Empty(context.RouteData.Values);
handler.Verify(h => h.RouteAsync(It.IsAny<RouteContext>()), Times.Once()); handler.Verify(h => h.RouteAsync(It.IsAny<RouteContext>()), Times.Once());

View File

@ -1020,7 +1020,7 @@ namespace Microsoft.AspNet.Mvc.Routing
public Task RouteAsync(RouteContext context) public Task RouteAsync(RouteContext context)
{ {
context.IsHandled = true; context.Handler = (c) => Task.FromResult(0);
return Task.FromResult(false); return Task.FromResult(false);
} }
} }

View File

@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
new string[] new string[]
{ {
typeof(RouteCollection).FullName, typeof(RouteCollection).FullName,
typeof(TemplateRoute).FullName, typeof(Route).FullName,
typeof(MvcRouteHandler).FullName, typeof(MvcRouteHandler).FullName,
}, },
result.Routers); result.Routers);

View File

@ -32,32 +32,12 @@ namespace CustomRouteWebSite
public async Task RouteAsync(RouteContext context) public async Task RouteAsync(RouteContext context)
{ {
// Saving and restoring the original route data ensures that any values we context.RouteData.Routers.Add(_next);
// add won't 'leak' if action selection doesn't match.
var oldRouteData = context.RouteData;
// For diagnostics and link-generation purposes, routing should include
// a list of IRoute instances that lead to the ultimate destination.
// It's the responsibility of each IRouter to add the 'next' before
// calling it.
var newRouteData = new RouteData(oldRouteData);
newRouteData.Routers.Add(_next);
var locale = GetLocale(context.HttpContext) ?? "en-US"; var locale = GetLocale(context.HttpContext) ?? "en-US";
newRouteData.Values.Add("locale", locale); context.RouteData.Values.Add("locale", locale);
try await _next.RouteAsync(context);
{
context.RouteData = newRouteData;
await _next.RouteAsync(context);
}
finally
{
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
} }
private string GetLocale(HttpContext context) private string GetLocale(HttpContext context)