Adding API for consuming url generation

This commit is contained in:
Ryan Nowak 2014-02-28 14:29:00 -08:00
parent 89eb6e6445
commit cd73fac433
17 changed files with 308 additions and 58 deletions

View File

@ -50,5 +50,10 @@ namespace RoutingSample
return null;
}
public RouteBindResult Bind(RouteBindContext context)
{
return null;
}
}
}

View File

@ -26,11 +26,11 @@ namespace RoutingSample
var endpoint1 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("match1"));
var endpoint2 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("Hello, World!"));
var rb1 = new RouteBuilder(endpoint1, routes);
var rb1 = new RouteBuilder(endpoint1, routes.Routes);
rb1.AddPrefixRoute("api/store");
rb1.AddTemplateRoute("api/{controller}/{*extra}", new { controller = "Store" });
var rb2 = new RouteBuilder(endpoint2, routes);
var rb2 = new RouteBuilder(endpoint2, routes.Routes);
rb2.AddPrefixRoute("hello/world");
rb2.AddPrefixRoute("");
}

View File

@ -1,5 +1,6 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Abstractions;
@ -20,13 +21,13 @@ namespace Microsoft.AspNet.Routing
public async Task<bool> Invoke(HttpContext context)
{
RouteContext routeContext = new RouteContext(context);
var routeContext = new RouteContext(context);
for (int i = 0; i < Routes.Count; i++)
for (var i = 0; i < Routes.Count; i++)
{
IRoute route = Routes[i];
var route = Routes[i];
RouteMatch match = route.Match(routeContext);
var match = route.Match(routeContext);
if (match != null)
{
context.SetFeature<IRouteValues>(new RouteValues(match.Values));
@ -41,5 +42,23 @@ namespace Microsoft.AspNet.Routing
return false;
}
public string GetUrl(HttpContext context, IDictionary<string, object> values)
{
var routeBindContext = new RouteBindContext(context, values);
for (var i = 0; i < Routes.Count; i++)
{
var route = Routes[i];
var result = route.Bind(routeBindContext);
if (result != null)
{
return result.Url;
}
}
return null;
}
}
}

View File

@ -5,5 +5,7 @@ namespace Microsoft.AspNet.Routing
public interface IRoute
{
RouteMatch Match(RouteContext context);
RouteBindResult Bind(RouteBindContext context);
}
}

View File

@ -1,5 +1,6 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Abstractions;
@ -7,6 +8,10 @@ namespace Microsoft.AspNet.Routing
{
public interface IRouteEngine
{
IRouteCollection Routes { get; }
Task<bool> Invoke(HttpContext context);
string GetUrl(HttpContext context, IDictionary<string, object> values);
}
}

View File

@ -6,14 +6,14 @@ namespace Microsoft.AspNet.Routing.Owin
{
public static class BuilderExtensions
{
public static IRouteCollection UseRouter(this IBuilder builder)
public static IRouteEngine UseRouter(this IBuilder builder)
{
var routes = new DefaultRouteCollection();
var engine = new DefaultRouteEngine(routes);
builder.Use((next) => new RouterMiddleware(next, engine).Invoke);
return routes;
return engine;
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Abstractions;
namespace Microsoft.AspNet.Routing
{
public class RouteBindContext
{
public RouteBindContext(HttpContext context, IDictionary<string, object> values)
{
Context = context;
Values = values;
if (Context != null)
{
var ambientValues = context.GetFeature<IRouteValues>();
AmbientValues = ambientValues == null ? null : ambientValues.Values;
}
}
public IDictionary<string, object> AmbientValues { get; private set; }
public HttpContext Context { get; private set; }
public IDictionary<string, object> Values { get; private set; }
}
}

View File

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Routing
{
public class RouteBindResult
{
public RouteBindResult(string url)
{
Url = url;
}
public string Url { get; private set; }
}
}

View File

@ -12,9 +12,6 @@ namespace Microsoft.AspNet.Routing.Template
{
private const string SeparatorString = "/";
private readonly TemplateMatcher _matcher;
private readonly TemplateBinder _binder;
public Template(List<TemplateSegment> segments)
{
if (segments == null)
@ -37,20 +34,12 @@ namespace Microsoft.AspNet.Routing.Template
}
}
}
_matcher = new TemplateMatcher(this);
_binder = new TemplateBinder(this);
}
public List<TemplatePart> Parameters { get; private set; }
public List<TemplateSegment> Segments { get; private set; }
public IDictionary<string, object> Match(string requestPath, IDictionary<string, object> defaults)
{
return _matcher.Match(requestPath, defaults);
}
private string DebuggerToString()
{
return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString()));

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNet.Routing.Template
public Template Template { get; private set; }
public BoundRouteTemplate Bind(IDictionary<string, object> defaults, IDictionary<string, object> ambientValues, IDictionary<string, object> values)
public string Bind(IDictionary<string, object> defaults, IDictionary<string, object> ambientValues, IDictionary<string, object> values)
{
if (values == null)
{
@ -184,7 +184,7 @@ namespace Microsoft.AspNet.Routing.Template
}
// Step 2: If the route is a match generate the appropriate URI
private BoundRouteTemplate BindValues(TemplateBindingContext bindingContext)
private string BindValues(TemplateBindingContext bindingContext)
{
var context = new UriBuildingContext();
@ -261,10 +261,7 @@ namespace Microsoft.AspNet.Routing.Template
encoded.Append(Uri.EscapeDataString(converted));
}
return new BoundRouteTemplate()
{
Path = encoded.ToString(),
};
return encoded.ToString();
}
private static string UriEncode(string str)

View File

@ -97,6 +97,10 @@ namespace Microsoft.AspNet.Routing.Template
{
values.Add(part.Name, defaultValue);
}
else if (part.IsOptional)
{
// This is optional (with no default value) - there's nothing to capture here, so just move on.
}
else
{
// There's no default for this parameter

View File

@ -11,6 +11,8 @@ namespace Microsoft.AspNet.Routing.Template
private readonly IRouteEndpoint _endpoint;
private readonly Template _parsedTemplate;
private readonly string _routeTemplate;
private readonly TemplateMatcher _matcher;
private readonly TemplateBinder _binder;
public TemplateRoute(IRouteEndpoint endpoint, string routeTemplate)
: this(endpoint, routeTemplate, null)
@ -25,11 +27,14 @@ namespace Microsoft.AspNet.Routing.Template
}
_endpoint = endpoint;
_routeTemplate = routeTemplate ?? String.Empty;
_routeTemplate = routeTemplate ?? string.Empty;
_defaults = defaults ?? new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
// The parser will throw for invalid routes.
_parsedTemplate = TemplateParser.Parse(RouteTemplate);
_matcher = new TemplateMatcher(_parsedTemplate);
_binder = new TemplateBinder(_parsedTemplate);
}
public IDictionary<string, object> Defaults
@ -55,12 +60,12 @@ namespace Microsoft.AspNet.Routing.Template
}
var requestPath = context.RequestPath;
if (!String.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
requestPath = requestPath.Substring(1);
}
IDictionary<string, object> values = _parsedTemplate.Match(requestPath, _defaults);
var values = _matcher.Match(requestPath, _defaults);
if (values == null)
{
// If we got back a null value set, that means the URI did not match
@ -71,5 +76,11 @@ namespace Microsoft.AspNet.Routing.Template
return new RouteMatch(_endpoint, values);
}
}
public RouteBindResult Bind(RouteBindContext context)
{
var path = _binder.Bind(_defaults, context.AmbientValues, context.Values);
return path == null ? null : new RouteBindResult(path);
}
}
}

View File

@ -1,28 +0,0 @@

using System;
using System.Collections.Generic;
using System.Reflection;
namespace Microsoft.AspNet.Routing.Template.Tests
{
// This is just a placeholder
public class RouteValueDictionary : Dictionary<string, object>
{
public RouteValueDictionary()
: base(StringComparer.OrdinalIgnoreCase)
{
}
public RouteValueDictionary(object obj)
: base(StringComparer.OrdinalIgnoreCase)
{
if (obj != null)
{
foreach (var property in obj.GetType().GetTypeInfo().GetProperties())
{
Add(property.Name, property.GetValue(obj));
}
}
}
}
}

View File

@ -139,7 +139,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
else
{
Assert.NotNull(boundTemplate);
Assert.Equal(expected, boundTemplate.Path);
Assert.Equal(expected, boundTemplate);
}
}
@ -989,7 +989,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// We want to chop off the query string and compare that using an unordered comparison
var expectedParts = new PathAndQuery(expected);
var actualParts = new PathAndQuery(boundTemplate.Path);
var actualParts = new PathAndQuery(boundTemplate);
Assert.Equal(expectedParts.Path, actualParts.Path);

View File

@ -733,6 +733,37 @@ namespace Microsoft.AspNet.Routing.Template.Tests
Assert.False(match.ContainsKey("action"));
}
[Fact]
public void MatchDoesNotSetOptionalParameter_EmptyString()
{
// Arrange
var route = CreateMatcher("{controller?}");
var url = "";
// Act
var match = route.Match(url, null);
// Assert
Assert.NotNull(match);
Assert.Equal(0, match.Values.Count);
Assert.False(match.ContainsKey("controller"));
}
[Fact]
public void Match_EmptyRouteWith_EmptyString()
{
// Arrange
var route = CreateMatcher("");
var url = "";
// Act
var match = route.Match(url, null);
// Assert
Assert.NotNull(match);
Assert.Equal(0, match.Values.Count);
}
[Fact]
public void MatchMultipleOptionalParameters()
{

View File

@ -0,0 +1,172 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Abstractions;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Routing.Template.Tests
{
public class TemplateRouteTests
{
#region Route Matching
// PathString in HttpAbstractions guarantees a leading slash - so no value in testing other cases.
[Fact]
public void Match_Success_LeadingSlash()
{
// Arrange
var route = CreateRoute("{controller}/{action}");
var context = CreateRouteContext("/Home/Index");
// Act
var match = route.Match(context);
// Assert
Assert.NotNull(match);
Assert.Equal(2, match.Values.Count);
Assert.Equal("Home", match.Values["controller"]);
Assert.Equal("Index", match.Values["action"]);
}
[Fact]
public void Match_Success_RootUrl()
{
// Arrange
var route = CreateRoute("");
var context = CreateRouteContext("/");
// Act
var match = route.Match(context);
// Assert
Assert.NotNull(match);
Assert.Equal(0, match.Values.Count);
}
[Fact]
public void Match_Success_Defaults()
{
// Arrange
var route = CreateRoute("{controller}/{action}", new { action = "Index" });
var context = CreateRouteContext("/Home");
// Act
var match = route.Match(context);
// Assert
Assert.NotNull(match);
Assert.Equal(2, match.Values.Count);
Assert.Equal("Home", match.Values["controller"]);
Assert.Equal("Index", match.Values["action"]);
}
[Fact]
public void Match_Fails()
{
// Arrange
var route = CreateRoute("{controller}/{action}");
var context = CreateRouteContext("/Home");
// Act
var match = route.Match(context);
// Assert
Assert.Null(match);
}
private static RouteContext CreateRouteContext(string requestPath)
{
var request = new Mock<HttpRequest>(MockBehavior.Strict);
request.SetupGet(r => r.Path).Returns(new PathString(requestPath));
var context = new Mock<HttpContext>(MockBehavior.Strict);
context.SetupGet(c => c.Request).Returns(request.Object);
return new RouteContext(context.Object);
}
#endregion
#region Route Binding
[Fact]
public void Bind_Success()
{
// Arrange
var route = CreateRoute("{controller}");
var context = CreateRouteBindContext(new {controller = "Home"});
// Act
var bind = route.Bind(context);
// Assert
Assert.NotNull(bind);
Assert.Equal("Home", bind.Url);
}
[Fact]
public void Bind_Fail()
{
// Arrange
var route = CreateRoute("{controller}/{action}");
var context = CreateRouteBindContext(new { controller = "Home" });
// Act
var bind = route.Bind(context);
// Assert
Assert.Null(bind);
}
[Fact]
public void Bind_Success_AmbientValues()
{
// Arrange
var route = CreateRoute("{controller}/{action}");
var context = CreateRouteBindContext(new { action = "Index"}, new { controller = "Home" });
// Act
var bind = route.Bind(context);
// Assert
Assert.NotNull(bind);
Assert.Equal("Home/Index", bind.Url);
}
private static RouteBindContext CreateRouteBindContext(object values)
{
return CreateRouteBindContext(new RouteValueDictionary(values), null);
}
private static RouteBindContext CreateRouteBindContext(object values, object ambientValues)
{
return CreateRouteBindContext(new RouteValueDictionary(values), new RouteValueDictionary(ambientValues));
}
private static RouteBindContext CreateRouteBindContext(IDictionary<string, object> values, IDictionary<string, object> ambientValues)
{
var context = new Mock<HttpContext>(MockBehavior.Strict);
context.Setup(c => c.GetFeature<IRouteValues>()).Returns(new RouteValues(ambientValues));
return new RouteBindContext(context.Object, values);
}
#endregion
private static TemplateRoute CreateRoute(string template)
{
return new TemplateRoute(CreateEndpoint(), template);
}
private static TemplateRoute CreateRoute(string template, object defaults)
{
return new TemplateRoute(CreateEndpoint(), template, new RouteValueDictionary(defaults));
}
private static IRouteEndpoint CreateEndpoint()
{
return new Mock<IRouteEndpoint>(MockBehavior.Strict).Object;
}
}
}

View File

@ -7,6 +7,7 @@
"configurations": {
"net45": {
"dependencies": {
"Moq": "4.2.1402.2112",
"Owin": "1.0",
"xunit": "1.9.2",
"xunit.extensions": "1.9.2"