diff --git a/samples/RoutingSample.Web/HttpContextRouteEndpoint.cs b/samples/RoutingSample.Web/DelegateRouteEndpoint.cs similarity index 65% rename from samples/RoutingSample.Web/HttpContextRouteEndpoint.cs rename to samples/RoutingSample.Web/DelegateRouteEndpoint.cs index 64c69b49e2..f8585e7700 100644 --- a/samples/RoutingSample.Web/HttpContextRouteEndpoint.cs +++ b/samples/RoutingSample.Web/DelegateRouteEndpoint.cs @@ -1,21 +1,22 @@ using System.Threading.Tasks; -using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Routing; namespace RoutingSample.Web { - public class HttpContextRouteEndpoint : IRouter + public class DelegateRouteEndpoint : IRouter { - private readonly RequestDelegate _appFunc; + public delegate Task RoutedDelegate(RouteContext context); - public HttpContextRouteEndpoint(RequestDelegate appFunc) + private readonly RoutedDelegate _appFunc; + + public DelegateRouteEndpoint(RoutedDelegate appFunc) { _appFunc = appFunc; } public async Task RouteAsync(RouteContext context) { - await _appFunc(context.HttpContext); + await _appFunc(context); context.IsHandled = true; } diff --git a/samples/RoutingSample.Web/DictionaryExtensions.cs b/samples/RoutingSample.Web/DictionaryExtensions.cs new file mode 100644 index 0000000000..7a76f5a22d --- /dev/null +++ b/samples/RoutingSample.Web/DictionaryExtensions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Linq; + +namespace RoutingSample.Web +{ + public static class DictionaryExtensions + { + public static string Print(this IDictionary routeValues) + { + var values = routeValues.Select(kvp => kvp.Key + ":" + kvp.Value.ToString()); + + return string.Join(" ", values); + } + } +} \ No newline at end of file diff --git a/samples/RoutingSample.Web/Startup.cs b/samples/RoutingSample.Web/Startup.cs index 03f6ec1bc5..1cc7d790c3 100644 --- a/samples/RoutingSample.Web/Startup.cs +++ b/samples/RoutingSample.Web/Startup.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNet.Abstractions; +using System.Text.RegularExpressions; +using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Routing; namespace RoutingSample.Web @@ -9,11 +10,23 @@ namespace RoutingSample.Web { var routes = builder.UseRouter(); - var endpoint1 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("match1")); - var endpoint2 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("Hello, World!")); + var endpoint1 = new DelegateRouteEndpoint(async (context) => + await context.HttpContext.Response.WriteAsync( + "match1, route values -" + context.Values.Print())); + var endpoint2 = new DelegateRouteEndpoint(async (context) => + await context.HttpContext.Response.WriteAsync("Hello, World!")); routes.DefaultHandler = endpoint1; routes.AddPrefixRoute("api/store"); + + routes.MapRoute("api/constraint/{controller}", null, new { controller = "my.*" }); + routes.MapRoute("api/rconstraint/{controller}", + new { foo = "Bar" }, + new { controller = new RegexConstraint("^(my.*)$") }); + routes.MapRoute("api/r2constraint/{controller}", + new { foo = "Bar2" }, + new { controller = new RegexConstraint(new Regex("^(my.*)$")) }); + routes.MapRoute("api/{controller}/{*extra}", new { controller = "Store" }); routes.AddPrefixRoute("hello/world", endpoint2); diff --git a/src/Microsoft.AspNet.Routing/IRouteConstraint.cs b/src/Microsoft.AspNet.Routing/IRouteConstraint.cs new file mode 100644 index 0000000000..7f19b60fed --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteConstraint.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteConstraint + { + bool Match([NotNull] HttpContext httpContext, + [NotNull] IRouter route, + [NotNull] string routeKey, + [NotNull] IDictionary values, + RouteDirection routeDirection); + } +} diff --git a/src/Microsoft.AspNet.Routing/IRouteValues.cs b/src/Microsoft.AspNet.Routing/IRouteValues.cs deleted file mode 100644 index 7e44c9fc86..0000000000 --- a/src/Microsoft.AspNet.Routing/IRouteValues.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing -{ - public interface IRouteValues - { - IDictionary Values - { - get; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/NotNullAttribute.cs b/src/Microsoft.AspNet.Routing/NotNullAttribute.cs new file mode 100644 index 0000000000..8c89a438a0 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/NotNullAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Microsoft.AspNet.Routing +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} diff --git a/src/Microsoft.AspNet.Routing/RegexConstraint.cs b/src/Microsoft.AspNet.Routing/RegexConstraint.cs new file mode 100644 index 0000000000..0667d0b341 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RegexConstraint.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Routing +{ + public class RegexConstraint : IRouteConstraint + { + public RegexConstraint([NotNull] Regex regex) + { + Constraint = regex; + } + + public RegexConstraint([NotNull] string regexPattern) + { + Constraint = new Regex(regexPattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + } + + public Regex Constraint { get; private set; } + + public bool Match([NotNull] HttpContext httpContext, + [NotNull] IRouter route, + [NotNull] string routeKey, + [NotNull] IDictionary routeValues, + RouteDirection routeDirection) + { + object routeValue; + + if (routeValues.TryGetValue(routeKey, out routeValue) + && routeValue != null) + { + var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture); + + return Constraint.IsMatch(parameterValueString); + } + + return false; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteCollection.cs b/src/Microsoft.AspNet.Routing/RouteCollection.cs index 724ce57523..42a3363322 100644 --- a/src/Microsoft.AspNet.Routing/RouteCollection.cs +++ b/src/Microsoft.AspNet.Routing/RouteCollection.cs @@ -1,5 +1,4 @@ // 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; diff --git a/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs b/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs index 9627b618cf..6bdb326628 100644 --- a/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Routing/RouteCollectionExtensions.cs @@ -27,7 +27,39 @@ namespace Microsoft.AspNet.Routing throw new ArgumentException("DefaultHandler must be set."); } - routes.Add(new TemplateRoute(routes.DefaultHandler, template, defaults)); + routes.Add(new TemplateRoute(routes.DefaultHandler, template, defaults, null)); + return routes; + } + + public static IRouteCollection MapRoute(this IRouteCollection routes, string template, object defaults, object constraints) + { + MapRoute(routes, template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints)); + return routes; + } + + public static IRouteCollection MapRoute(this IRouteCollection routes, string template, object defaults, + IDictionary constraints) + { + MapRoute(routes, template, new RouteValueDictionary(defaults), constraints); + return routes; + } + + public static IRouteCollection MapRoute(this IRouteCollection routes, string template, + IDictionary defaults, object constraints) + { + MapRoute(routes, template, defaults, new RouteValueDictionary(constraints)); + return routes; + } + + public static IRouteCollection MapRoute(this IRouteCollection routes, string template, + IDictionary defaults, IDictionary constraints) + { + if (routes.DefaultHandler == null) + { + throw new ArgumentException("DefaultHandler must be set."); + } + + routes.Add(new TemplateRoute(routes.DefaultHandler, template, defaults, constraints)); return routes; } } diff --git a/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs new file mode 100644 index 0000000000..625cfad237 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public class RouteConstraintBuilder + { + public static IDictionary + BuildConstraints(IDictionary inputConstraints) + { + if (inputConstraints == null || inputConstraints.Count == 0) + { + return null; + } + + var constraints = new Dictionary(inputConstraints.Count, StringComparer.OrdinalIgnoreCase); + + foreach (var kvp in inputConstraints) + { + var constraint = kvp.Value as IRouteConstraint; + + if (constraint == null) + { + var regexPattern = kvp.Value as string; + + if (regexPattern == null) + { + throw new InvalidOperationException("Constraint can be a valid regex string or an IRouteConstraint"); + } + + var constraintsRegEx = "^(" + regexPattern + ")$"; + + constraint = new RegexConstraint(constraintsRegEx); + } + + constraints.Add(kvp.Key, constraint); + } + + return constraints; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs b/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs new file mode 100644 index 0000000000..0197f06201 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Routing +{ + public static class RouteConstraintMatcher + { + public static bool Match([NotNull] IDictionary constraints, + [NotNull] IDictionary routeValues, + [NotNull] HttpContext httpContext, + [NotNull] IRouter route, + [NotNull] RouteDirection routeDirection) + { + if (constraints == null) + { + return true; + } + + foreach (var kvp in constraints) + { + var constraint = kvp.Value; + if (!constraint.Match(httpContext, route, kvp.Key, routeValues, routeDirection)) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteDirection.cs b/src/Microsoft.AspNet.Routing/RouteDirection.cs new file mode 100644 index 0000000000..cf23ab6384 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteDirection.cs @@ -0,0 +1,8 @@ +namespace Microsoft.AspNet.Routing +{ + public enum RouteDirection + { + IncomingRequest, + UrlGeneration, + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteValues.cs b/src/Microsoft.AspNet.Routing/RouteValues.cs deleted file mode 100644 index 01acc8e991..0000000000 --- a/src/Microsoft.AspNet.Routing/RouteValues.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing -{ - public class RouteValues : IRouteValues - { - public RouteValues(IDictionary values) - { - Values = values; - } - - public IDictionary Values - { - get; - private set; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index 2e664a0d26..6f9c2fc22a 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -2,27 +2,26 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; -using Microsoft.AspNet.Abstractions; namespace Microsoft.AspNet.Routing.Template { public class TemplateRoute : IRouter { private readonly IDictionary _defaults; + private readonly IDictionary _constraints; private readonly IRouter _target; - private readonly Template _parsedTemplate; private readonly string _routeTemplate; private readonly TemplateMatcher _matcher; private readonly TemplateBinder _binder; public TemplateRoute(IRouter target, string routeTemplate) - : this(target, routeTemplate, null) + : this(target, routeTemplate, null, null) { } - public TemplateRoute(IRouter target, string routeTemplate, IDictionary defaults) + public TemplateRoute(IRouter target, string routeTemplate, IDictionary defaults, + IDictionary constraints) { if (target == null) { @@ -32,11 +31,12 @@ namespace Microsoft.AspNet.Routing.Template _target = target; _routeTemplate = routeTemplate ?? string.Empty; _defaults = defaults ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + _constraints = RouteConstraintBuilder.BuildConstraints(constraints); // The parser will throw for invalid routes. - _parsedTemplate = TemplateParser.Parse(RouteTemplate); + var parsedTemplate = TemplateParser.Parse(RouteTemplate); - _matcher = new TemplateMatcher(_parsedTemplate); + _matcher = new TemplateMatcher(parsedTemplate); _binder = new TemplateBinder(_parsedTemplate, _defaults); } @@ -74,7 +74,14 @@ namespace Microsoft.AspNet.Routing.Template // Not currently doing anything to clean this up if it's not a match. Consider hardening this. context.Values = values; - await _target.RouteAsync(context); + if (RouteConstraintMatcher.Match(_constraints, + values, + context.HttpContext, + this, + RouteDirection.IncomingRequest)) + { + await _target.RouteAsync(context); + } } } diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json index 2a144b4a8e..f5a0844f8d 100644 --- a/src/Microsoft.AspNet.Routing/project.json +++ b/src/Microsoft.AspNet.Routing/project.json @@ -1,7 +1,8 @@ { "version": "0.1-alpha-*", "dependencies": { - "Microsoft.AspNet.Abstractions": "0.1-alpha-*" + "Microsoft.AspNet.Abstractions": "0.1-alpha-*", + "Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*" }, "configurations": { "net45": {}, @@ -19,6 +20,9 @@ "System.Runtime": "4.0.20.0", "System.Runtime.Extensions": "4.0.10.0", "System.Text.RegularExpressions": "4.0.0.0", + "System.Runtime.Hosting": "3.9.0.0", + "System.Runtime.InteropServices": "4.0.10.0", + "System.Text.Encoding": "4.0.10.0", "System.Threading.Tasks": "4.0.0.0" } } diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs index b7062a6f07..127350ad0b 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -196,7 +196,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests private static TemplateRoute CreateRoute(string template, object defaults, bool accept = true) { - return new TemplateRoute(CreateTarget(accept), template, new RouteValueDictionary(defaults)); + return new TemplateRoute(CreateTarget(accept), template, new RouteValueDictionary(defaults), null); } private static IRouter CreateTarget(bool accept = true)