Fix for #65,116 - Implement 'stack of routers'

This is the routing part of the fix. MVC will be updated as well
(attribute routing).

As the graph of routers is traversed, routers add themselves to the
current 'path', which unwinds on a failed path.

This mechanism is opt-in. Whoever adds something needs to remove it as
part of cleanup. If a router in the tree doesn't interact with the
.Routers property, then there are no consequences for those that do.

Additionally, fixing #116 as part of the same change. This means that we
create a nested 'RouteData' and then restore it on the way out. This is
simpler than just dealing with the .Routers property in isolation.
This commit is contained in:
Ryan Nowak 2014-10-29 18:24:50 -07:00
parent bc0b61b6f2
commit d78e5478a7
5 changed files with 175 additions and 86 deletions

View File

@ -57,10 +57,27 @@ namespace Microsoft.AspNet.Routing
{
var route = this[i];
await route.RouteAsync(context);
if (context.IsHandled)
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Routers.Add(route);
try
{
break;
context.RouteData = newRouteData;
await route.RouteAsync(context);
if (context.IsHandled)
{
break;
}
}
finally
{
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
}

View File

@ -1,24 +1,31 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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;
namespace Microsoft.AspNet.Routing
{
/// <summary>
/// Summary description for RouteData
/// </summary>
public class RouteData
{
public RouteData()
{
DataTokens = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
Routers = new List<IRouter>();
Values = new RouteValueDictionary();
}
public RouteData([NotNull] RouteData other)
{
DataTokens = new Dictionary<string, object>(other.DataTokens, StringComparer.OrdinalIgnoreCase);
Routers = new List<IRouter>(other.Routers);
Values = new RouteValueDictionary(other.Values);
}
public List<IRouter> Routers { get; private set; }
public IDictionary<string, object> Values { get; set; }
public IDictionary<string, object> Values { get; private set; }
public IDictionary<string, object> DataTokens { get; set; }
public IDictionary<string, object> DataTokens { get; private set; }
}
}

View File

@ -120,54 +120,57 @@ namespace Microsoft.AspNet.Routing.Template
// If we got back a null value set, that means the URI did not match
return;
}
else
if (!RouteConstraintMatcher.Match(
Constraints,
values,
context.HttpContext,
this,
RouteDirection.IncomingRequest,
_constraintLogger))
{
var originalValues = context.RouteData.Values;
context.RouteData.Values = MergeValues(originalValues, values);
try
if (_logger.IsEnabled(TraceType.Verbose))
{
if (RouteConstraintMatcher.Match(Constraints,
values,
context.HttpContext,
this,
RouteDirection.IncomingRequest,
_constraintLogger))
{
context.RouteData.DataTokens = _dataTokens;
await _target.RouteAsync(context);
if (_logger.IsEnabled(TraceType.Verbose))
{
_logger.WriteValues(CreateRouteAsyncValues(
requestPath,
values,
matchedValues: true,
matchedConstraints: true,
handled: context.IsHandled));
}
}
else
{
if (_logger.IsEnabled(TraceType.Verbose))
{
_logger.WriteValues(CreateRouteAsyncValues(
requestPath,
values,
matchedValues: true,
matchedConstraints: false,
handled: context.IsHandled));
}
}
_logger.WriteValues(CreateRouteAsyncValues(
requestPath,
values,
matchedValues: true,
matchedConstraints: false,
handled: context.IsHandled));
}
finally
return;
}
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
MergeValues(newRouteData.DataTokens, _dataTokens);
newRouteData.Routers.Add(_target);
MergeValues(newRouteData.Values, values);
try
{
context.RouteData = newRouteData;
await _target.RouteAsync(context);
if (_logger.IsEnabled(TraceType.Verbose))
{
if (!context.IsHandled)
{
// Restore the original route data if we didn't handle the route to prevent
// poluting the original route data with the values from this route.
context.RouteData.Values = originalValues;
}
_logger.WriteValues(CreateRouteAsyncValues(
requestPath,
values,
matchedValues: true,
matchedConstraints: true,
handled: context.IsHandled));
}
}
finally
{
// Restore the original values to prevent polluting the route data.
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
}
@ -304,26 +307,17 @@ namespace Microsoft.AspNet.Routing.Template
return values;
}
private static IDictionary<string, object> MergeValues(
IDictionary<string, object> originalValues,
private static void MergeValues(
IDictionary<string, object> destination,
IDictionary<string, object> values)
{
if (originalValues == null)
{
// There is nothing to merge
return values;
}
var result = new RouteValueDictionary(originalValues);
foreach (var kvp in values)
{
// This will replace the original value for the specified key.
// Values from the matched route will take preference over previous
// data in the route context.
result[kvp.Key] = kvp.Value;
destination[kvp.Key] = kvp.Value;
}
return result;
}
private void EnsureLoggers(HttpContext context)

View File

@ -114,6 +114,9 @@ namespace Microsoft.AspNet.Routing
route1.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(1));
route2.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(0));
Assert.True(context.IsHandled);
Assert.Equal(1, context.RouteData.Routers.Count);
Assert.Same(route1.Object, context.RouteData.Routers[0]);
}
[Fact]
@ -137,6 +140,9 @@ namespace Microsoft.AspNet.Routing
route1.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(1));
route2.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(1));
Assert.True(context.IsHandled);
Assert.Equal(1, context.RouteData.Routers.Count);
Assert.Same(route2.Object, context.RouteData.Routers[0]);
}
[Fact]
@ -160,6 +166,8 @@ namespace Microsoft.AspNet.Routing
route1.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(1));
route2.Verify(e => e.RouteAsync(It.IsAny<RouteContext>()), Times.Exactly(1));
Assert.False(context.IsHandled);
Assert.Empty(context.RouteData.Routers);
}
[Fact]

View File

@ -175,11 +175,12 @@ namespace Microsoft.AspNet.Routing.Template
var template = "{controller}/{action}/{id:int}";
var context = CreateRouteContext("/Home/Index/5");
var originalRouteDataValues = new Dictionary<string, object>
{
{ "country", "USA" },
};
context.RouteData.Values = originalRouteDataValues;
var originalRouteDataValues = context.RouteData.Values;
originalRouteDataValues.Add("country", "USA");
var originalDataTokens = context.RouteData.DataTokens;
originalDataTokens.Add("company", "Contoso");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
@ -192,7 +193,13 @@ namespace Microsoft.AspNet.Routing.Template
})
.Returns(Task.FromResult(true));
var route = new TemplateRoute(mockTarget.Object, template, _inlineConstraintResolver);
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: new RouteValueDictionary(new { today = "Friday" }),
inlineConstraintResolver: _inlineConstraintResolver);
// Act
await route.RouteAsync(context);
@ -209,8 +216,12 @@ namespace Microsoft.AspNet.Routing.Template
Assert.Equal("USA", context.RouteData.Values["country"]);
Assert.True(context.RouteData.Values.ContainsKey("id"));
Assert.Equal("5", context.RouteData.Values["id"]);
Assert.NotSame(originalRouteDataValues, context.RouteData.Values);
Assert.Equal("Contoso", context.RouteData.DataTokens["company"]);
Assert.Equal("Friday", context.RouteData.DataTokens["today"]);
Assert.NotSame(originalDataTokens, context.RouteData.DataTokens);
Assert.NotSame(route.DataTokens, context.RouteData.DataTokens);
}
[Fact]
@ -220,11 +231,11 @@ namespace Microsoft.AspNet.Routing.Template
var template = "{controller}/{action}/{id:int}";
var context = CreateRouteContext("/Home/Index/5");
var originalRouteDataValues = new Dictionary<string, object>
{
{ "country", "USA" },
};
context.RouteData.Values = originalRouteDataValues;
var originalRouteDataValues = context.RouteData.Values;
originalRouteDataValues.Add("country", "USA");
var originalDataTokens = context.RouteData.DataTokens;
originalDataTokens.Add("company", "Contoso");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
@ -237,7 +248,13 @@ namespace Microsoft.AspNet.Routing.Template
})
.Returns(Task.FromResult(true));
var route = new TemplateRoute(mockTarget.Object, template, _inlineConstraintResolver);
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: new RouteValueDictionary(new { today = "Friday" }),
inlineConstraintResolver: _inlineConstraintResolver);
// Act
await route.RouteAsync(context);
@ -252,8 +269,12 @@ namespace Microsoft.AspNet.Routing.Template
Assert.True(context.RouteData.Values.ContainsKey("country"));
Assert.Equal("USA", context.RouteData.Values["country"]);
Assert.False(context.RouteData.Values.ContainsKey("id"));
Assert.Same(originalRouteDataValues, context.RouteData.Values);
Assert.Equal("Contoso", context.RouteData.DataTokens["company"]);
Assert.False(context.RouteData.DataTokens.ContainsKey("today"));
Assert.Same(originalDataTokens, context.RouteData.DataTokens);
}
[Fact]
@ -263,11 +284,11 @@ namespace Microsoft.AspNet.Routing.Template
var template = "{controller}/{action}/{id:int}";
var context = CreateRouteContext("/Home/Index/5");
var originalRouteDataValues = new Dictionary<string, object>
{
{ "country", "USA" },
};
context.RouteData.Values = originalRouteDataValues;
var originalRouteDataValues = context.RouteData.Values;
originalRouteDataValues.Add("country", "USA");
var originalDataTokens = context.RouteData.DataTokens;
originalDataTokens.Add("company", "Contoso");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
@ -280,7 +301,13 @@ namespace Microsoft.AspNet.Routing.Template
})
.Throws(new Exception());
var route = new TemplateRoute(mockTarget.Object, template, _inlineConstraintResolver);
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: new RouteValueDictionary(new { today = "Friday" }),
inlineConstraintResolver: _inlineConstraintResolver);
// Act
var ex = await Assert.ThrowsAsync<Exception>(() => route.RouteAsync(context));
@ -295,8 +322,12 @@ namespace Microsoft.AspNet.Routing.Template
Assert.True(context.RouteData.Values.ContainsKey("country"));
Assert.Equal("USA", context.RouteData.Values["country"]);
Assert.False(context.RouteData.Values.ContainsKey("id"));
Assert.Same(originalRouteDataValues, context.RouteData.Values);
Assert.Equal("Contoso", context.RouteData.DataTokens["company"]);
Assert.False(context.RouteData.DataTokens.ContainsKey("today"));
Assert.Same(originalDataTokens, context.RouteData.DataTokens);
}
@ -397,7 +428,39 @@ namespace Microsoft.AspNet.Routing.Template
Assert.False(context.IsHandled);
// Issue #16 tracks this.
Assert.Null(context.RouteData.Values);
Assert.Empty(context.RouteData.Values);
}
[Fact]
public async Task Match_RejectedByHandler_ClearsRouters()
{
// Arrange
var route = CreateRoute("{controller}", accept: false);
var context = CreateRouteContext("/Home");
// Act
await route.RouteAsync(context);
// Assert
Assert.False(context.IsHandled);
Assert.Empty(context.RouteData.Routers);
}
[Fact]
public async Task Match_SetsRouters()
{
// Arrange
var target = CreateTarget(accept: true);
var route = CreateRoute(target, "{controller}");
var context = CreateRouteContext("/Home");
// Act
await route.RouteAsync(context);
// Assert
Assert.True(context.IsHandled);
Assert.Equal(1, context.RouteData.Routers.Count);
Assert.Same(target, context.RouteData.Routers[0]);
}
[Fact]