From 36180ab6d0e48c42a1b69569897bdfb362f28c0c Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 9 Dec 2015 16:22:05 -0800 Subject: [PATCH] Add IRouteHandler, RouteBase, and IRoutingFeature Adds IRouterHandler, an abstraction for endpoints in the routing system that can't chain (example: delegates). The idea is that some kinds of routes aren't really friendly to chaining. If you don't support chaining, then accept IRouteHandler and work with that rather than IRouter. There's one implementation of IRouteHandler, RouteHandler. It implements both IRouter and IRouteHandler. Adds RouteBase as a base class for routes based on our template syntax and defaults/constraints/data-tokens. Updated a lot of signatures to be get/set virtual and mutable to facilitate or bigger variety of usage scenarios. Renamed TemplateRoute to just Route, now inherits from RouteBase. Adds IRoutingFeature for middleware scenarios where you don't have access to the route context. Also adds some basic extension methods for accessing route values. --- .../DelegateRouteEndpoint.cs | 31 -- .../RoutingSample.Web/DictionaryExtensions.cs | 18 - samples/RoutingSample.Web/PrefixRoute.cs | 14 +- .../RouteBuilderExtensions.cs | 27 +- samples/RoutingSample.Web/Startup.cs | 51 +- .../IRouteHandler.cs | 24 + .../IRoutingFeature.cs | 16 + .../RouteContext.cs | 21 +- .../RouteData.cs | 8 +- .../RoutingHttpContextExtensions.cs | 53 +++ .../VirtualPathContext.cs | 4 +- .../project.json | 1 + src/Microsoft.AspNet.Routing/IRouteBuilder.cs | 9 +- .../Internal/TaskCache.cs | 16 + src/Microsoft.AspNet.Routing/Route.cs | 76 +++ .../TemplateRoute.cs => RouteBase.cs} | 202 +++----- src/Microsoft.AspNet.Routing/RouteBuilder.cs | 6 +- .../RouteBuilderExtensions.cs | 103 ++-- .../RouteCollection.cs | 6 +- .../RouteConstraintBuilder.cs | 4 +- .../RouteConstraintMatcher.cs | 13 +- src/Microsoft.AspNet.Routing/RouteHandler.cs | 36 ++ .../RouterMiddleware.cs | 18 +- .../Tree/TreeRouteLinkGenerationEntry.cs | 23 +- .../Tree/TreeRouteMatchingEntry.cs | 2 +- .../Tree/TreeRouter.cs | 11 +- .../RouteCollectionTest.cs | 17 +- .../TemplateRouteTest.cs => RouteTest.cs} | 444 ++++-------------- .../RouterMiddlewareTest.cs | 2 +- .../TemplateParserDefaultValuesTests.cs | 4 +- .../Tree/TreeRouterTest.cs | 140 +++--- 31 files changed, 615 insertions(+), 785 deletions(-) delete mode 100644 samples/RoutingSample.Web/DelegateRouteEndpoint.cs delete mode 100644 samples/RoutingSample.Web/DictionaryExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing.Abstractions/IRouteHandler.cs create mode 100644 src/Microsoft.AspNet.Routing.Abstractions/IRoutingFeature.cs create mode 100644 src/Microsoft.AspNet.Routing.Abstractions/RoutingHttpContextExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/Internal/TaskCache.cs create mode 100644 src/Microsoft.AspNet.Routing/Route.cs rename src/Microsoft.AspNet.Routing/{Template/TemplateRoute.cs => RouteBase.cs} (52%) create mode 100644 src/Microsoft.AspNet.Routing/RouteHandler.cs rename test/Microsoft.AspNet.Routing.Tests/{Template/TemplateRouteTest.cs => RouteTest.cs} (78%) diff --git a/samples/RoutingSample.Web/DelegateRouteEndpoint.cs b/samples/RoutingSample.Web/DelegateRouteEndpoint.cs deleted file mode 100644 index 904404e1b4..0000000000 --- a/samples/RoutingSample.Web/DelegateRouteEndpoint.cs +++ /dev/null @@ -1,31 +0,0 @@ -// 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.AspNet.Routing; - -namespace RoutingSample.Web -{ - public class DelegateRouteEndpoint : IRouter - { - public delegate Task RoutedDelegate(RouteContext context); - - private readonly RoutedDelegate _appFunc; - - public DelegateRouteEndpoint(RoutedDelegate appFunc) - { - _appFunc = appFunc; - } - - public async Task RouteAsync(RouteContext context) - { - await _appFunc(context); - context.IsHandled = true; - } - - public VirtualPathData GetVirtualPath(VirtualPathContext context) - { - return null; - } - } -} diff --git a/samples/RoutingSample.Web/DictionaryExtensions.cs b/samples/RoutingSample.Web/DictionaryExtensions.cs deleted file mode 100644 index 6431b09d4c..0000000000 --- a/samples/RoutingSample.Web/DictionaryExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -// 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.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/PrefixRoute.cs b/samples/RoutingSample.Web/PrefixRoute.cs index ff8beda2a0..70d2f6d80c 100644 --- a/samples/RoutingSample.Web/PrefixRoute.cs +++ b/samples/RoutingSample.Web/PrefixRoute.cs @@ -7,12 +7,12 @@ using Microsoft.AspNet.Routing; namespace RoutingSample.Web { - internal class PrefixRoute : IRouter + public class PrefixRoute : IRouter { - private readonly IRouter _target; + private readonly IRouteHandler _target; private readonly string _prefix; - public PrefixRoute(IRouter target, string prefix) + public PrefixRoute(IRouteHandler target, string prefix) { _target = target; @@ -34,7 +34,7 @@ namespace RoutingSample.Web _prefix = prefix; } - public async Task RouteAsync(RouteContext context) + public Task RouteAsync(RouteContext context) { var requestPath = context.HttpContext.Request.Path.Value ?? string.Empty; if (requestPath.StartsWith(_prefix, StringComparison.OrdinalIgnoreCase)) @@ -44,12 +44,14 @@ namespace RoutingSample.Web var lastCharacter = requestPath[_prefix.Length]; if (lastCharacter != '/' && lastCharacter != '#' && lastCharacter != '?') { - return; + return Task.FromResult(0); } } - await _target.RouteAsync(context); + context.Handler = _target.GetRequestHandler(context.HttpContext, context.RouteData); } + + return Task.FromResult(0); } public VirtualPathData GetVirtualPath(VirtualPathContext context) diff --git a/samples/RoutingSample.Web/RouteBuilderExtensions.cs b/samples/RoutingSample.Web/RouteBuilderExtensions.cs index 4c382c516e..f2317dbcf1 100644 --- a/samples/RoutingSample.Web/RouteBuilderExtensions.cs +++ b/samples/RoutingSample.Web/RouteBuilderExtensions.cs @@ -1,34 +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 System; using Microsoft.AspNet.Routing; -using Microsoft.AspNet.Routing.Template; using Microsoft.Extensions.DependencyInjection; namespace RoutingSample.Web { public static class RouteBuilderExtensions { - public static IRouteBuilder AddPrefixRoute(this IRouteBuilder routeBuilder, - string prefix) - { - if (routeBuilder.DefaultHandler == null) - { - throw new InvalidOperationException("DefaultHandler must be set."); - } - - if (routeBuilder.ServiceProvider == null) - { - throw new InvalidOperationException("ServiceProvider must be set."); - } - - return AddPrefixRoute(routeBuilder, prefix, routeBuilder.DefaultHandler); - } - - public static IRouteBuilder AddPrefixRoute(this IRouteBuilder routeBuilder, - string prefix, - IRouter handler) + public static IRouteBuilder AddPrefixRoute( + this IRouteBuilder routeBuilder, + string prefix, + IRouteHandler handler) { routeBuilder.Routes.Add(new PrefixRoute(handler, prefix)); return routeBuilder; @@ -45,7 +28,7 @@ namespace RoutingSample.Web var constraintResolver = routeBuilder.ServiceProvider.GetService(); - var route = new TemplateRoute( + var route = new Route( target: routeBuilder.DefaultHandler, routeTemplate: routeTemplate, defaults: defaultsDictionary, diff --git a/samples/RoutingSample.Web/Startup.cs b/samples/RoutingSample.Web/Startup.cs index f0c08e97ad..416d1b4c2d 100644 --- a/samples/RoutingSample.Web/Startup.cs +++ b/samples/RoutingSample.Web/Startup.cs @@ -1,12 +1,9 @@ // 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; -using System.Text.RegularExpressions; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing; -using Microsoft.AspNet.Routing.Constraints; using Microsoft.Extensions.DependencyInjection; namespace RoutingSample.Web @@ -20,46 +17,20 @@ namespace RoutingSample.Web public void Configure(IApplicationBuilder builder) { - var endpoint1 = new DelegateRouteEndpoint(async (context) => - await context - .HttpContext - .Response - .WriteAsync( - "match1, route values -" + context.RouteData.Values.Print())); + var endpoint1 = new RouteHandler((c) => + { + return c.Response.WriteAsync($"match1, route values - {string.Join(", ", c.GetRouteData().Values)}"); + }); - var endpoint2 = new DelegateRouteEndpoint(async (context) => - await context - .HttpContext - .Response - .WriteAsync("Hello, World!")); + var endpoint2 = new RouteHandler((c) => c.Response.WriteAsync("Hello, World!")); - var routeBuilder = new RouteBuilder(); - routeBuilder.DefaultHandler = endpoint1; - routeBuilder.ServiceProvider = builder.ApplicationServices; - - routeBuilder.AddPrefixRoute("api/store"); - - routeBuilder.MapRoute("defaultRoute", - "api/constraint/{controller}", - null, - new { controller = "my.*" }); - routeBuilder.MapRoute("regexStringRoute", - "api/rconstraint/{controller}", - new { foo = "Bar" }, - new { controller = new RegexRouteConstraint("^(my.*)$") }); - routeBuilder.MapRoute("regexRoute", - "api/r2constraint/{controller}", - new { foo = "Bar2" }, - new - { - controller = new RegexRouteConstraint( - new Regex("^(my.*)$", RegexOptions.None, TimeSpan.FromSeconds(10))) - }); - - routeBuilder.MapRoute("parameterConstraintRoute", - "api/{controller}/{*extra}", - new { controller = "Store" }); + var routeBuilder = new RouteBuilder() + { + DefaultHandler = endpoint1, + ServiceProvider = builder.ApplicationServices, + }; + routeBuilder.AddPrefixRoute("api/store", endpoint1); routeBuilder.AddPrefixRoute("hello/world", endpoint2); routeBuilder.MapLocaleRoute("en-US", "store/US/{action}", new { controller = "Store" }); diff --git a/src/Microsoft.AspNet.Routing.Abstractions/IRouteHandler.cs b/src/Microsoft.AspNet.Routing.Abstractions/IRouteHandler.cs new file mode 100644 index 0000000000..584cebf321 --- /dev/null +++ b/src/Microsoft.AspNet.Routing.Abstractions/IRouteHandler.cs @@ -0,0 +1,24 @@ +// 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.AspNet.Http; + +namespace Microsoft.AspNet.Routing +{ + /// + /// Defines a contract for a handler of a route. + /// + public interface IRouteHandler + { + /// + /// Gets a to handle the request, based on the provided + /// . + /// + /// The associated with the current request. + /// The associated with the current routing match. + /// + /// A , or null if the handler cannot handle this request. + /// + RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData); + } +} diff --git a/src/Microsoft.AspNet.Routing.Abstractions/IRoutingFeature.cs b/src/Microsoft.AspNet.Routing.Abstractions/IRoutingFeature.cs new file mode 100644 index 0000000000..a74b321db4 --- /dev/null +++ b/src/Microsoft.AspNet.Routing.Abstractions/IRoutingFeature.cs @@ -0,0 +1,16 @@ +// 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. + +namespace Microsoft.AspNet.Routing +{ + /// + /// A feature interface for routing functionality. + /// + public interface IRoutingFeature + { + /// + /// Gets or sets the associated with the current request. + /// + RouteData RouteData { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Routing.Abstractions/RouteContext.cs b/src/Microsoft.AspNet.Routing.Abstractions/RouteContext.cs index 0c19bcea69..804e0a5378 100644 --- a/src/Microsoft.AspNet.Routing.Abstractions/RouteContext.cs +++ b/src/Microsoft.AspNet.Routing.Abstractions/RouteContext.cs @@ -6,10 +6,17 @@ using Microsoft.AspNet.Http; namespace Microsoft.AspNet.Routing { + /// + /// A context object for . + /// public class RouteContext { private RouteData _routeData; + /// + /// Creates a new for the provided . + /// + /// The associated with the current request. public RouteContext(HttpContext httpContext) { HttpContext = httpContext; @@ -17,10 +24,20 @@ namespace Microsoft.AspNet.Routing RouteData = new RouteData(); } - public HttpContext HttpContext { get; private set; } + /// + /// Gets or sets the handler for the request. An should set + /// when it matches. + /// + public RequestDelegate Handler { get; set; } - public bool IsHandled { get; set; } + /// + /// Gets the associated with the current request. + /// + public HttpContext HttpContext { get; } + /// + /// Gets or sets the associated with the current context. + /// public RouteData RouteData { get diff --git a/src/Microsoft.AspNet.Routing.Abstractions/RouteData.cs b/src/Microsoft.AspNet.Routing.Abstractions/RouteData.cs index 89104497e1..aaedaf3e9f 100644 --- a/src/Microsoft.AspNet.Routing.Abstractions/RouteData.cs +++ b/src/Microsoft.AspNet.Routing.Abstractions/RouteData.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Routing /// public class RouteData { - private Dictionary _dataTokens; + private RouteValueDictionary _dataTokens; private RouteValueDictionary _values; /// @@ -39,7 +39,7 @@ namespace Microsoft.AspNet.Routing // Perf: Avoid allocating DataTokens and RouteValues unless we need to make a copy. if (other._dataTokens != null) { - _dataTokens = new Dictionary(other._dataTokens, StringComparer.OrdinalIgnoreCase); + _dataTokens = new RouteValueDictionary(other._dataTokens); } if (other._values != null) @@ -51,13 +51,13 @@ namespace Microsoft.AspNet.Routing /// /// Gets the data tokens produced by routes on the current routing path. /// - public IDictionary DataTokens + public RouteValueDictionary DataTokens { get { if (_dataTokens == null) { - _dataTokens = new Dictionary(StringComparer.OrdinalIgnoreCase); + _dataTokens = new RouteValueDictionary(); } return _dataTokens; diff --git a/src/Microsoft.AspNet.Routing.Abstractions/RoutingHttpContextExtensions.cs b/src/Microsoft.AspNet.Routing.Abstractions/RoutingHttpContextExtensions.cs new file mode 100644 index 0000000000..7441555df4 --- /dev/null +++ b/src/Microsoft.AspNet.Routing.Abstractions/RoutingHttpContextExtensions.cs @@ -0,0 +1,53 @@ +// 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; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Routing +{ + /// + /// Extension methods for related to routing. + /// + public static class RoutingHttpContextExtensions + { + /// + /// Gets the associated with the provided . + /// + /// The associated with the current request. + /// The , or null. + public static RouteData GetRouteData(this HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var routingFeature = httpContext.Features[typeof(IRoutingFeature)] as IRoutingFeature; + return routingFeature?.RouteData; + } + + /// + /// Gets a route value from associated with the provided + /// . + /// + /// The associated with the current request. + /// The key of the route value. + /// The corresponding route value, or null. + public static object GetRouteValue(this HttpContext httpContext, string key) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var routingFeature = httpContext.Features[typeof(IRoutingFeature)] as IRoutingFeature; + return routingFeature?.RouteData.Values[key]; + } + } +} diff --git a/src/Microsoft.AspNet.Routing.Abstractions/VirtualPathContext.cs b/src/Microsoft.AspNet.Routing.Abstractions/VirtualPathContext.cs index deb7b1b97d..e6d86db08c 100644 --- a/src/Microsoft.AspNet.Routing.Abstractions/VirtualPathContext.cs +++ b/src/Microsoft.AspNet.Routing.Abstractions/VirtualPathContext.cs @@ -59,8 +59,8 @@ namespace Microsoft.AspNet.Routing public string RouteName { get; } /// - /// Gets the set of new values provided for virtual path generation. + /// Gets or sets the set of new values provided for virtual path generation. /// - public RouteValueDictionary Values { get; } + public RouteValueDictionary Values { get; set; } } } diff --git a/src/Microsoft.AspNet.Routing.Abstractions/project.json b/src/Microsoft.AspNet.Routing.Abstractions/project.json index 739134749d..fb32415e68 100644 --- a/src/Microsoft.AspNet.Routing.Abstractions/project.json +++ b/src/Microsoft.AspNet.Routing.Abstractions/project.json @@ -21,6 +21,7 @@ "dotnet5.4": { "dependencies": { "System.Collections.Concurrent": "4.0.11-*", + "System.Threading.Tasks": "4.0.11-*", "System.Reflection.Extensions": "4.0.1-*" } } diff --git a/src/Microsoft.AspNet.Routing/IRouteBuilder.cs b/src/Microsoft.AspNet.Routing/IRouteBuilder.cs index a1b9ee5a24..37e1076b28 100644 --- a/src/Microsoft.AspNet.Routing/IRouteBuilder.cs +++ b/src/Microsoft.AspNet.Routing/IRouteBuilder.cs @@ -7,15 +7,20 @@ using System.Collections.Generic; namespace Microsoft.AspNet.Routing { /// - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// Defines a contract for a route builder in an application. A route builder specifies the routes for + /// an application. /// public interface IRouteBuilder { /// - /// Gets or sets the default that is used if an is added to the list of routes but does not specify its own. + /// Gets or sets the default that is used as a handler if an + /// is added to the list of routes but does not specify its own. /// IRouter DefaultHandler { get; set; } + /// + /// Gets the sets the used to resolve services for routes. + /// IServiceProvider ServiceProvider { get; } /// diff --git a/src/Microsoft.AspNet.Routing/Internal/TaskCache.cs b/src/Microsoft.AspNet.Routing/Internal/TaskCache.cs new file mode 100644 index 0000000000..4278b001ee --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Internal/TaskCache.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.AspNet.Routing.Internal +{ + public static class TaskCache + { +#if DOTNET5_4 + public static readonly Task CompletedTask = Task.CompletedTask; +#else + public static readonly Task CompletedTask = Task.FromResult(0); +#endif + } +} diff --git a/src/Microsoft.AspNet.Routing/Route.cs b/src/Microsoft.AspNet.Routing/Route.cs new file mode 100644 index 0000000000..9106ec00aa --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Route.cs @@ -0,0 +1,76 @@ +// 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; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing +{ + public class Route : RouteBase + { + private readonly IRouter _target; + + public Route( + IRouter target, + string routeTemplate, + IInlineConstraintResolver inlineConstraintResolver) + : this( + target, + routeTemplate, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: inlineConstraintResolver) + { + } + + public Route( + IRouter target, + string routeTemplate, + RouteValueDictionary defaults, + IDictionary constraints, + RouteValueDictionary dataTokens, + IInlineConstraintResolver inlineConstraintResolver) + : this(target, null, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver) + { + } + + public Route( + IRouter target, + string routeName, + string routeTemplate, + RouteValueDictionary defaults, + IDictionary constraints, + RouteValueDictionary dataTokens, + IInlineConstraintResolver inlineConstraintResolver) + : base( + routeTemplate, + routeName, + inlineConstraintResolver, + defaults, + constraints, + dataTokens) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + _target = target; + } + + public string RouteTemplate => ParsedTemplate.TemplateText; + + protected override Task OnRouteMatched(RouteContext context) + { + context.RouteData.Routers.Add(_target); + return _target.RouteAsync(context); + } + + protected override VirtualPathData OnVirtualPathGenerated(VirtualPathContext context) + { + return _target.GetVirtualPath(context); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/RouteBase.cs similarity index 52% rename from src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs rename to src/Microsoft.AspNet.Routing/RouteBase.cs index c9e3bdd365..ddc53d30d3 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/RouteBase.cs @@ -1,118 +1,75 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Internal; +using Microsoft.AspNet.Routing.Template; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNet.Routing.Template +namespace Microsoft.AspNet.Routing { - public class TemplateRoute : INamedRouter + public abstract class RouteBase : IRouter, INamedRouter { - private readonly IReadOnlyDictionary _constraints; - private readonly IReadOnlyDictionary _dataTokens; - private readonly IReadOnlyDictionary _defaults; - private readonly IRouter _target; - private readonly RouteTemplate _parsedTemplate; - private readonly string _routeTemplate; - private readonly TemplateMatcher _matcher; - private readonly TemplateBinder _binder; + private TemplateMatcher _matcher; + private TemplateBinder _binder; private ILogger _logger; private ILogger _constraintLogger; - public TemplateRoute( - IRouter target, - string routeTemplate, - IInlineConstraintResolver inlineConstraintResolver) - : this( - target, - routeTemplate, - defaults: null, - constraints: null, - dataTokens: null, - inlineConstraintResolver: inlineConstraintResolver) - { - } - - public TemplateRoute( - IRouter target, - string routeTemplate, - IDictionary defaults, + public RouteBase( + string template, + string name, + IInlineConstraintResolver constraintResolver, + RouteValueDictionary defaults, IDictionary constraints, - IDictionary dataTokens, - IInlineConstraintResolver inlineConstraintResolver) - : this(target, null, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver) + RouteValueDictionary dataTokens) { - } - - public TemplateRoute( - IRouter target, - string routeName, - string routeTemplate, - IDictionary defaults, - IDictionary constraints, - IDictionary dataTokens, - IInlineConstraintResolver inlineConstraintResolver) - { - if (target == null) + if (constraintResolver == null) { - throw new ArgumentNullException(nameof(target)); + throw new ArgumentNullException(nameof(constraintResolver)); } - _target = target; - _routeTemplate = routeTemplate ?? string.Empty; - Name = routeName; - - _dataTokens = dataTokens == null ? new RouteValueDictionary() : new RouteValueDictionary(dataTokens); + template = template ?? string.Empty; + Name = name; + ConstraintResolver = constraintResolver; + DataTokens = dataTokens ?? new RouteValueDictionary(); // Data we parse from the template will be used to fill in the rest of the constraints or // defaults. The parser will throw for invalid routes. - _parsedTemplate = TemplateParser.Parse(RouteTemplate); + ParsedTemplate = TemplateParser.Parse(template); - _constraints = GetConstraints(inlineConstraintResolver, RouteTemplate, _parsedTemplate, constraints); - _defaults = GetDefaults(_parsedTemplate, defaults); - - _matcher = new TemplateMatcher(_parsedTemplate, Defaults); - _binder = new TemplateBinder(_parsedTemplate, Defaults); + Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints); + Defaults = GetDefaults(ParsedTemplate, defaults); } - public string Name { get; private set; } + public virtual IDictionary Constraints { get; protected set; } - public IReadOnlyDictionary Defaults - { - get { return _defaults; } - } + protected virtual IInlineConstraintResolver ConstraintResolver { get; set; } - public IReadOnlyDictionary DataTokens - { - get { return _dataTokens; } - } + public virtual RouteValueDictionary DataTokens { get; protected set; } - public RouteTemplate ParsedTemplate - { - get { return _parsedTemplate; } - } + public virtual RouteValueDictionary Defaults { get; protected set; } - public string RouteTemplate - { - get { return _routeTemplate; } - } + public virtual string Name { get; protected set; } - public IReadOnlyDictionary Constraints - { - get { return _constraints; } - } + public virtual RouteTemplate ParsedTemplate { get; protected set; } - public async virtual Task RouteAsync(RouteContext context) + protected abstract Task OnRouteMatched(RouteContext context); + + protected abstract VirtualPathData OnVirtualPathGenerated(VirtualPathContext context); + + /// + public virtual Task RouteAsync(RouteContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } + EnsureMatcher(); EnsureLoggers(context.HttpContext); var requestPath = context.HttpContext.Request.Path; @@ -121,57 +78,46 @@ namespace Microsoft.AspNet.Routing.Template if (values == null) { // If we got back a null value set, that means the URI did not match - return; + return TaskCache.CompletedTask; } - var oldRouteData = context.RouteData; - - var newRouteData = new RouteData(oldRouteData); - - // Perf: Avoid accessing data tokens if you don't need to write to it, these dictionaries are all + // Perf: Avoid accessing dictionaries if you don't need to write to them, these dictionaries are all // created lazily. - if (_dataTokens.Count > 0) + if (DataTokens.Count > 0) { - MergeValues(newRouteData.DataTokens, _dataTokens); + MergeValues(context.RouteData.DataTokens, DataTokens); } - newRouteData.Routers.Add(_target); - MergeValues(newRouteData.Values, values); + if (values.Count > 0) + { + MergeValues(context.RouteData.Values, values); + } if (!RouteConstraintMatcher.Match( Constraints, - newRouteData.Values, + context.RouteData.Values, context.HttpContext, this, RouteDirection.IncomingRequest, _constraintLogger)) { - return; + return TaskCache.CompletedTask; } _logger.LogDebug( "Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'.", Name, - RouteTemplate); + ParsedTemplate.TemplateText); - try - { - context.RouteData = newRouteData; - - await _target.RouteAsync(context); - } - finally - { - // Restore the original values to prevent polluting the route data. - if (!context.IsHandled) - { - context.RouteData = oldRouteData; - } - } + return OnRouteMatched(context); } + /// public virtual VirtualPathData GetVirtualPath(VirtualPathContext context) { + EnsureBinder(); + EnsureLoggers(context.HttpContext); + var values = _binder.GetValues(context.AmbientValues, context.Values); if (values == null) { @@ -179,7 +125,6 @@ namespace Microsoft.AspNet.Routing.Template return null; } - EnsureLoggers(context.HttpContext); if (!RouteConstraintMatcher.Match( Constraints, values.CombinedValues, @@ -191,7 +136,9 @@ namespace Microsoft.AspNet.Routing.Template return null; } - var pathData = _target.GetVirtualPath(context); + context.Values = values.CombinedValues; + + var pathData = OnVirtualPathGenerated(context); if (pathData != null) { // If the target generates a value then that can short circuit. @@ -202,13 +149,13 @@ namespace Microsoft.AspNet.Routing.Template // to see if the values were validated. // When we still cannot produce a value, this should return null. - var tempPath = _binder.BindValues(values.AcceptedValues); - if (tempPath == null) + var virtualPath = _binder.BindValues(values.AcceptedValues); + if (virtualPath == null) { return null; } - pathData = new VirtualPathData(this, tempPath); + pathData = new VirtualPathData(this, virtualPath); if (DataTokens != null) { foreach (var dataToken in DataTokens) @@ -220,13 +167,12 @@ namespace Microsoft.AspNet.Routing.Template return pathData; } - private static IReadOnlyDictionary GetConstraints( + protected static IDictionary GetConstraints( IInlineConstraintResolver inlineConstraintResolver, - string template, RouteTemplate parsedTemplate, IDictionary constraints) { - var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, template); + var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, parsedTemplate.TemplateText); if (constraints != null) { @@ -252,12 +198,10 @@ namespace Microsoft.AspNet.Routing.Template return constraintBuilder.Build(); } - private static RouteValueDictionary GetDefaults( + protected static RouteValueDictionary GetDefaults( RouteTemplate parsedTemplate, - IDictionary defaults) + RouteValueDictionary defaults) { - // Do not use RouteValueDictionary.Empty for defaults, it might be modified inside - // UpdateInlineDefaultValuesAndConstraints() var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults); foreach (var parameter in parsedTemplate.Parameters) @@ -293,17 +237,11 @@ namespace Microsoft.AspNet.Routing.Template } } - // Needed because IDictionary<> is not an IReadOnlyDictionary<> - private static void MergeValues( - IDictionary destination, - IReadOnlyDictionary values) + private void EnsureBinder() { - foreach (var kvp in values) + if (_binder == null) { - // 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. - destination[kvp.Key] = kvp.Value; + _binder = new TemplateBinder(ParsedTemplate, Defaults); } } @@ -312,14 +250,22 @@ namespace Microsoft.AspNet.Routing.Template if (_logger == null) { var factory = context.RequestServices.GetRequiredService(); - _logger = factory.CreateLogger(); + _logger = factory.CreateLogger(typeof(RouteBase).FullName); _constraintLogger = factory.CreateLogger(typeof(RouteConstraintMatcher).FullName); } } + private void EnsureMatcher() + { + if (_matcher == null) + { + _matcher = new TemplateMatcher(ParsedTemplate, Defaults); + } + } + public override string ToString() { - return _routeTemplate; + return ParsedTemplate.TemplateText; } } } diff --git a/src/Microsoft.AspNet.Routing/RouteBuilder.cs b/src/Microsoft.AspNet.Routing/RouteBuilder.cs index 3b482a61a5..666add5418 100644 --- a/src/Microsoft.AspNet.Routing/RouteBuilder.cs +++ b/src/Microsoft.AspNet.Routing/RouteBuilder.cs @@ -17,11 +17,7 @@ namespace Microsoft.AspNet.Routing public IServiceProvider ServiceProvider { get; set; } - public IList Routes - { - get; - private set; - } + public IList Routes { get; } public IRouter Build() { diff --git a/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs index 07d24d34c9..c825f4b4d1 100644 --- a/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs +++ b/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs @@ -21,9 +21,10 @@ namespace Microsoft.AspNet.Builder /// The name of the route. /// The URL pattern of the route. /// A reference to this instance after the operation has completed. - public static IRouteBuilder MapRoute(this IRouteBuilder routeBuilder, - string name, - string template) + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string name, + string template) { MapRoute(routeBuilder, name, template, defaults: null); return routeBuilder; @@ -35,50 +36,73 @@ namespace Microsoft.AspNet.Builder /// The to add the route to. /// The name of the route. /// The URL pattern of the route. - /// An object that contains default values for route parameters. The object's properties represent the names and values of the default values. + /// + /// An object that contains default values for route parameters. The object's properties represent the names + /// and values of the default values. + /// /// A reference to this instance after the operation has completed. - public static IRouteBuilder MapRoute(this IRouteBuilder routeBuilder, - string name, - string template, - object defaults) + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string name, + string template, + object defaults) { return MapRoute(routeBuilder, name, template, defaults, constraints: null); } /// - /// Adds a route to the with the specified name, template, default values, and constraints. + /// Adds a route to the with the specified name, template, default values, and + /// constraints. /// /// The to add the route to. /// The name of the route. /// The URL pattern of the route. - /// An object that contains default values for route parameters. The object's properties represent the names and values of the default values. - /// An object that contains constraints for the route. The object's properties represent the names and values of the constraints. + /// + /// An object that contains default values for route parameters. The object's properties represent the names + /// and values of the default values. + /// + /// + /// An object that contains constraints for the route. The object's properties represent the names and values + /// of the constraints. + /// /// A reference to this instance after the operation has completed. - public static IRouteBuilder MapRoute(this IRouteBuilder routeBuilder, - string name, - string template, - object defaults, - object constraints) + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string name, + string template, + object defaults, + object constraints) { return MapRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null); } /// - /// Adds a route to the with the specified name, template, default values, and data tokens. + /// Adds a route to the with the specified name, template, default values, and + /// data tokens. /// /// The to add the route to. /// The name of the route. /// The URL pattern of the route. - /// An object that contains default values for route parameters. The object's properties represent the names and values of the default values. - /// An object that contains constraints for the route. The object's properties represent the names and values of the constraints. - /// An object that contains data tokens for the route. The object's properties represent the names and values of the data tokens. + /// + /// An object that contains default values for route parameters. The object's properties represent the names + /// and values of the default values. + /// + /// + /// An object that contains constraints for the route. The object's properties represent the names and values + /// of the constraints. + /// + /// + /// An object that contains data tokens for the route. The object's properties represent the names and values + /// of the data tokens. + /// /// A reference to this instance after the operation has completed. - public static IRouteBuilder MapRoute(this IRouteBuilder routeBuilder, - string name, - string template, - object defaults, - object constraints, - object dataTokens) + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string name, + string template, + object defaults, + object constraints, + object dataTokens) { if (routeBuilder.DefaultHandler == null) { @@ -88,26 +112,17 @@ namespace Microsoft.AspNet.Builder var inlineConstraintResolver = routeBuilder .ServiceProvider .GetRequiredService(); - routeBuilder.Routes.Add(new TemplateRoute(routeBuilder.DefaultHandler, - name, - template, - ObjectToDictionary(defaults), - ObjectToDictionary(constraints), - ObjectToDictionary(dataTokens), - inlineConstraintResolver)); + + routeBuilder.Routes.Add(new Route( + routeBuilder.DefaultHandler, + name, + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(constraints), + new RouteValueDictionary(dataTokens), + inlineConstraintResolver)); return routeBuilder; } - - private static IDictionary ObjectToDictionary(object value) - { - var dictionary = value as IDictionary; - if (dictionary != null) - { - return dictionary; - } - - return new RouteValueDictionary(value); - } } } diff --git a/src/Microsoft.AspNet.Routing/RouteCollection.cs b/src/Microsoft.AspNet.Routing/RouteCollection.cs index d5851a99db..6672b74088 100644 --- a/src/Microsoft.AspNet.Routing/RouteCollection.cs +++ b/src/Microsoft.AspNet.Routing/RouteCollection.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.Extensions.DependencyInjection; @@ -69,14 +68,15 @@ namespace Microsoft.AspNet.Routing context.RouteData = newRouteData; await route.RouteAsync(context); - if (context.IsHandled) + + if (context.Handler != null) { break; } } finally { - if (!context.IsHandled) + if (context.Handler == null) { context.RouteData = oldRouteData; } diff --git a/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs index b984ab889f..b26a76c6b4 100644 --- a/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs +++ b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs @@ -50,8 +50,8 @@ namespace Microsoft.AspNet.Routing /// /// Builds a mapping of constraints. /// - /// An of the constraints. - public IReadOnlyDictionary Build() + /// An of the constraints. + public IDictionary Build() { var constraints = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var kvp in _constraints) diff --git a/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs b/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs index c4ecf93a59..260da1693d 100644 --- a/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs +++ b/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs @@ -10,12 +10,13 @@ namespace Microsoft.AspNet.Routing { public static class RouteConstraintMatcher { - public static bool Match(IReadOnlyDictionary constraints, - IDictionary routeValues, - HttpContext httpContext, - IRouter route, - RouteDirection routeDirection, - ILogger logger) + public static bool Match( + IDictionary constraints, + IDictionary routeValues, + HttpContext httpContext, + IRouter route, + RouteDirection routeDirection, + ILogger logger) { if (routeValues == null) { diff --git a/src/Microsoft.AspNet.Routing/RouteHandler.cs b/src/Microsoft.AspNet.Routing/RouteHandler.cs new file mode 100644 index 0000000000..fdb05ce9f1 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteHandler.cs @@ -0,0 +1,36 @@ +// 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.AspNet.Http; +using Microsoft.AspNet.Routing.Internal; + +namespace Microsoft.AspNet.Routing +{ + public class RouteHandler : IRouteHandler, IRouter + { + private readonly RequestDelegate _requestDelegate; + + public RouteHandler(RequestDelegate requestDelegate) + { + _requestDelegate = requestDelegate; + } + + public RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData) + { + return _requestDelegate; + } + + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + // Nothing to do. + return null; + } + + public Task RouteAsync(RouteContext context) + { + context.Handler = _requestDelegate; + return TaskCache.CompletedTask; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouterMiddleware.cs b/src/Microsoft.AspNet.Routing/RouterMiddleware.cs index 68a526f81b..e2d149544e 100644 --- a/src/Microsoft.AspNet.Routing/RouterMiddleware.cs +++ b/src/Microsoft.AspNet.Routing/RouterMiddleware.cs @@ -5,8 +5,6 @@ using System; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing; -using Microsoft.AspNet.Routing.Logging; -using Microsoft.AspNet.Routing.Logging.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNet.Builder @@ -35,12 +33,26 @@ namespace Microsoft.AspNet.Builder await _router.RouteAsync(context); - if (!context.IsHandled) + if (context.Handler == null) { _logger.LogDebug("Request did not match any routes."); + httpContext.Features[typeof(IRoutingFeature)] = new RoutingFeature() + { + RouteData = context.RouteData, + }; + await _next.Invoke(httpContext); } + else + { + await context.Handler(context.HttpContext); + } + } + + private class RoutingFeature : IRoutingFeature + { + public RouteData RouteData { get; set; } } } } diff --git a/src/Microsoft.AspNet.Routing/Tree/TreeRouteLinkGenerationEntry.cs b/src/Microsoft.AspNet.Routing/Tree/TreeRouteLinkGenerationEntry.cs index 15f06da204..4a9ad4a1a8 100644 --- a/src/Microsoft.AspNet.Routing/Tree/TreeRouteLinkGenerationEntry.cs +++ b/src/Microsoft.AspNet.Routing/Tree/TreeRouteLinkGenerationEntry.cs @@ -13,47 +13,48 @@ namespace Microsoft.AspNet.Routing.Tree public class TreeRouteLinkGenerationEntry { /// - /// The . + /// Gets or sets the . /// public TemplateBinder Binder { get; set; } /// - /// The route constraints. + /// Gets or sets the route constraints. /// - public IReadOnlyDictionary Constraints { get; set; } + public IDictionary Constraints { get; set; } /// - /// The route defaults. + /// Gets or sets the route defaults. /// - public IReadOnlyDictionary Defaults { get; set; } + public IDictionary Defaults { get; set; } /// - /// The order of the template. + /// Gets or sets the order of the template. /// public int Order { get; set; } /// - /// The precedence of the template for link generation. Greater number means higher precedence. + /// Gets or sets the precedence of the template for link generation. A greater value of + /// means that an entry is considered first. /// public decimal GenerationPrecedence { get; set; } /// - /// The name of the route. + /// Gets or sets the name of the route. /// public string Name { get; set; } /// - /// The route group. + /// Gets or sets the route group. /// public string RouteGroup { get; set; } /// - /// The set of values that must be present for link genration. + /// Gets or sets the set of values that must be present for link genration. /// public IDictionary RequiredLinkValues { get; set; } /// - /// The . + /// Gets or sets the . /// public RouteTemplate Template { get; set; } } diff --git a/src/Microsoft.AspNet.Routing/Tree/TreeRouteMatchingEntry.cs b/src/Microsoft.AspNet.Routing/Tree/TreeRouteMatchingEntry.cs index 3a0e99e840..520ec5e2c7 100644 --- a/src/Microsoft.AspNet.Routing/Tree/TreeRouteMatchingEntry.cs +++ b/src/Microsoft.AspNet.Routing/Tree/TreeRouteMatchingEntry.cs @@ -45,6 +45,6 @@ namespace Microsoft.AspNet.Routing.Tree /// /// The route constraints. /// - public IReadOnlyDictionary Constraints { get; set; } + public IDictionary Constraints { get; set; } } } diff --git a/src/Microsoft.AspNet.Routing/Tree/TreeRouter.cs b/src/Microsoft.AspNet.Routing/Tree/TreeRouter.cs index ec48e11312..f9470a1527 100644 --- a/src/Microsoft.AspNet.Routing/Tree/TreeRouter.cs +++ b/src/Microsoft.AspNet.Routing/Tree/TreeRouter.cs @@ -199,20 +199,19 @@ namespace Microsoft.AspNet.Routing.Tree try { await match.Entry.Target.RouteAsync(context); + if (context.Handler != null) + { + return; + } } finally { - if (!context.IsHandled) + if (context.Handler == null) { // Restore the original values to prevent polluting the route data. context.RouteData = oldRouteData; } } - - if (context.IsHandled) - { - return; - } } } } diff --git a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs index e3ca1b34fa..5ce6300173 100644 --- a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; -using Microsoft.AspNet.Routing.Logging; using Microsoft.AspNet.Routing.Template; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging; @@ -18,6 +17,8 @@ namespace Microsoft.AspNet.Routing { public class RouteCollectionTest { + private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0); + [Theory] [InlineData(@"Home/Index/23", "/home/index/23", true, false)] [InlineData(@"Home/Index/23", "/Home/Index/23", false, false)] @@ -143,7 +144,7 @@ namespace Microsoft.AspNet.Routing // Assert route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(0)); - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.Equal(1, context.RouteData.Routers.Count); Assert.Same(route1.Object, context.RouteData.Routers[0]); @@ -169,7 +170,7 @@ namespace Microsoft.AspNet.Routing // Assert route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.Equal(1, context.RouteData.Routers.Count); Assert.Same(route2.Object, context.RouteData.Routers[0]); @@ -194,7 +195,7 @@ namespace Microsoft.AspNet.Routing // Assert route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); - Assert.False(context.IsHandled); + Assert.Null(context.Handler); Assert.Empty(context.RouteData.Routers); } @@ -485,14 +486,14 @@ namespace Microsoft.AspNet.Routing target .Setup(e => e.RouteAsync(It.IsAny())) - .Callback((c) => c.IsHandled = accept) + .Callback((c) => c.Handler = accept ? NullHandler : null) .Returns(Task.FromResult(null)) .Verifiable(); return target.Object; } - private static TemplateRoute CreateTemplateRoute( + private static Route CreateTemplateRoute( string template, string routerName = null, RouteValueDictionary dataTokens = null) @@ -504,7 +505,7 @@ namespace Microsoft.AspNet.Routing var resolverMock = new Mock(); - return new TemplateRoute( + return new Route( target.Object, routerName, template, @@ -610,7 +611,7 @@ namespace Microsoft.AspNet.Routing target .Setup(e => e.RouteAsync(It.IsAny())) - .Callback((c) => c.IsHandled = accept) + .Callback((c) => c.Handler = accept ? NullHandler : null) .Returns(Task.FromResult(null)) .Verifiable(); diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouteTest.cs similarity index 78% rename from test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs rename to test/Microsoft.AspNet.Routing.Tests/RouteTest.cs index 5775c61f3e..4f2cc35a66 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/RouteTest.cs @@ -8,54 +8,19 @@ using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing.Constraints; -using Microsoft.AspNet.Routing.Logging; using Microsoft.AspNet.Testing; -using Microsoft.Extensions.Logging.Internal; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging; using Moq; using Xunit; -namespace Microsoft.AspNet.Routing.Template +namespace Microsoft.AspNet.Routing { - public class TemplateRouteTest + public class RouteTest { + private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0); private static IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver(); - private async Task> SetUp( - bool loggerEnabled, - string routeName, - string template, - string requestPath, - TestSink testSink = null) - { - if (testSink == null) - { - testSink = new TestSink( - TestSink.EnableWithTypeName, - TestSink.EnableWithTypeName); - } - - var loggerFactory = new TestLoggerFactory(testSink, loggerEnabled); - - TemplateRoute route; - if (!string.IsNullOrEmpty(routeName)) - { - route = CreateRoute(routeName, template); - } - else - { - route = CreateRoute(template); - } - - var context = CreateRouteContext(requestPath, loggerFactory); - - // Act - await route.RouteAsync(context); - - return Tuple.Create(testSink, context); - } - [Fact] public void CreateTemplate_InlineConstraint_Regex_Malformed() { @@ -67,7 +32,7 @@ namespace Microsoft.AspNet.Routing.Template "'IInlineConstraintResolverProxy'."; var exception = Assert.Throws( - () => new TemplateRoute( + () => new Route( mockTarget.Object, template, defaults: null, @@ -78,67 +43,6 @@ namespace Microsoft.AspNet.Routing.Template Assert.Equal(expected, exception.Message); } - [Fact] - public async Task RouteAsync_MatchSuccess_LogsCorrectValues() - { - // Arrange & Act - var routeName = "Default"; - var template = "{controller}/{action}"; - var result = await SetUp( - routeName: routeName, - template: template, - requestPath: "/Home/Index", - loggerEnabled: true); - var sink = result.Item1; - var expected = "Request successfully matched the route with " + - $"name '{routeName}' and template '{template}'."; - - // Assert - Assert.Empty(sink.Scopes); - Assert.Single(sink.Writes); - Assert.Equal(expected, sink.Writes[0].State?.ToString()); - } - - [Fact] - public async Task RouteAsync_MatchFailOnConstraints_LogsCorrectValues() - { - // Arrange & Act - var sink = new TestSink((writeEnabled) => true, (scopeEnabled) => true); - var template = "{controller}/{action}/{id:int}"; - var result = await SetUp( - routeName: "Default", - template: template, - requestPath: "/Home/Index/Failure", - loggerEnabled: true, - testSink: sink); - var expectedMessage = "Route value 'Failure' with key 'id' did not match the " + - $"constraint '{typeof(IntRouteConstraint).FullName}'."; - var expectedLogValues = new[] - { - new KeyValuePair("RouteValue", "Failure"), - new KeyValuePair("RouteKey", "id"), - new KeyValuePair("RouteConstraint", typeof(IntRouteConstraint).FullName) - }; - - // Assert - Assert.Empty(sink.Scopes); - Assert.Single(sink.Writes); - var sinkWrite = sink.Writes[0]; - var formattedLogValues = Assert.IsType(sinkWrite.State); - Assert.Equal(expectedMessage, formattedLogValues.ToString()); - var actualLogValues = formattedLogValues.GetValues(); - foreach(var expectedPair in expectedLogValues) - { - Assert.Contains( - actualLogValues, - (kvp) => - { - return string.Equals(expectedPair.Key, kvp.Key) - && string.Equals(expectedPair.Value?.ToString(), kvp.Value?.ToString()); - }); - } - } - [Fact] public async Task RouteAsync_MergesExistingRouteData_IfRouteMatches() { @@ -160,11 +64,11 @@ namespace Microsoft.AspNet.Routing.Template .Callback(ctx => { routeValues = ctx.RouteData.Values; - ctx.IsHandled = true; + ctx.Handler = NullHandler; }) .Returns(Task.FromResult(true)); - var route = new TemplateRoute( + var route = new Route( mockTarget.Object, template, defaults: null, @@ -187,12 +91,11 @@ 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.Same(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); + Assert.Same(originalDataTokens, context.RouteData.DataTokens); } [Fact] @@ -215,13 +118,13 @@ namespace Microsoft.AspNet.Routing.Template .Callback(ctx => { routeValues = ctx.RouteData.Values; - ctx.IsHandled = true; + ctx.Handler = NullHandler; }) .Returns(Task.FromResult(true)); var constraint = new CapturingConstraint(); - var route = new TemplateRoute( + var route = new Route( mockTarget.Object, template, defaults: null, @@ -249,171 +152,10 @@ 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.Same(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] - public async Task RouteAsync_CleansUpMergedRouteData_IfRouteDoesNotMatch() - { - // Arrange - var template = "{controller}/{action}/{id:int}"; - - var context = CreateRouteContext("/Home/Index/5"); - var originalRouteDataValues = context.RouteData.Values; - originalRouteDataValues.Add("country", "USA"); - - var originalDataTokens = context.RouteData.DataTokens; - originalDataTokens.Add("company", "Contoso"); - - IDictionary routeValues = null; - var mockTarget = new Mock(MockBehavior.Strict); - mockTarget - .Setup(s => s.RouteAsync(It.IsAny())) - .Callback(ctx => - { - routeValues = ctx.RouteData.Values; - ctx.IsHandled = false; - }) - .Returns(Task.FromResult(true)); - - var route = new TemplateRoute( - mockTarget.Object, - template, - defaults: null, - constraints: null, - dataTokens: new RouteValueDictionary(new { today = "Friday" }), - inlineConstraintResolver: _inlineConstraintResolver); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(routeValues); - - Assert.True(routeValues.ContainsKey("country")); - Assert.Equal("USA", routeValues["country"]); - Assert.True(routeValues.ContainsKey("id")); - Assert.Equal("5", routeValues["id"]); - - 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] - public async Task RouteAsync_CleansUpMergedRouteData_IfHandlerThrows() - { - // Arrange - var template = "{controller}/{action}/{id:int}"; - - var context = CreateRouteContext("/Home/Index/5"); - var originalRouteDataValues = context.RouteData.Values; - originalRouteDataValues.Add("country", "USA"); - - var originalDataTokens = context.RouteData.DataTokens; - originalDataTokens.Add("company", "Contoso"); - - IDictionary routeValues = null; - var mockTarget = new Mock(MockBehavior.Strict); - mockTarget - .Setup(s => s.RouteAsync(It.IsAny())) - .Callback(ctx => - { - routeValues = ctx.RouteData.Values; - ctx.IsHandled = false; - }) - .Throws(new Exception()); - - 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(() => route.RouteAsync(context)); - - // Assert - Assert.NotNull(routeValues); - - Assert.True(routeValues.ContainsKey("country")); - Assert.Equal("USA", routeValues["country"]); - Assert.True(routeValues.ContainsKey("id")); - Assert.Equal("5", routeValues["id"]); - - 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] - public async Task RouteAsync_CleansUpMergedRouteData_IfConstraintThrows() - { - // Arrange - var template = "{controller}/{action}/{id:int}"; - - var context = CreateRouteContext("/Home/Index/5"); - var originalRouteDataValues = context.RouteData.Values; - originalRouteDataValues.Add("country", "USA"); - - var originalDataTokens = context.RouteData.DataTokens; - originalDataTokens.Add("company", "Contoso"); - - var mockTarget = new Mock(MockBehavior.Strict); - mockTarget - .Setup(s => s.RouteAsync(It.IsAny())) - .Callback(ctx => - { - ctx.IsHandled = true; - }) - .Returns(Task.FromResult(true)); - - var constraint = new Mock(MockBehavior.Strict); - constraint - .Setup(c => c.Match( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Callback(() => { throw new Exception(); }); - - var route = new TemplateRoute( - mockTarget.Object, - template, - defaults: null, - constraints: new RouteValueDictionary(new { action = constraint.Object }), - dataTokens: new RouteValueDictionary(new { today = "Friday" }), - inlineConstraintResolver: _inlineConstraintResolver); - - // Act - var ex = await Assert.ThrowsAsync(() => route.RouteAsync(context)); - - // Assert - 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); } @@ -432,11 +174,11 @@ namespace Microsoft.AspNet.Routing.Template .Callback(ctx => { routeValues = ctx.RouteData.Values; - ctx.IsHandled = true; + ctx.Handler = NullHandler; }) .Returns(Task.FromResult(true)); - var route = new TemplateRoute( + var route = new Route( mockTarget.Object, template, defaults: null, @@ -451,7 +193,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.True(routeValues.ContainsKey("id")); Assert.Equal("5", routeValues["id"]); @@ -474,11 +216,11 @@ namespace Microsoft.AspNet.Routing.Template .Callback(ctx => { routeValues = ctx.RouteData.Values; - ctx.IsHandled = true; + ctx.Handler = NullHandler; }) .Returns(Task.FromResult(true)); - var route = new TemplateRoute( + var route = new Route( mockTarget.Object, template, defaults: null, @@ -493,7 +235,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.True(routeValues.ContainsKey("ssn")); Assert.Equal("123-456-7890", routeValues["ssn"]); @@ -516,11 +258,11 @@ namespace Microsoft.AspNet.Routing.Template .Callback(ctx => { routeValues = ctx.RouteData.Values; - ctx.IsHandled = true; + ctx.Handler = NullHandler; }) .Returns(Task.FromResult(true)); - var route = new TemplateRoute( + var route = new Route( mockTarget.Object, template, defaults: null, @@ -535,7 +277,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.NotNull(routeValues); Assert.False(routeValues.ContainsKey("id")); Assert.False(context.RouteData.Values.ContainsKey("id")); @@ -556,14 +298,14 @@ namespace Microsoft.AspNet.Routing.Template .Callback(ctx => { routeValues = ctx.RouteData.Values; - ctx.IsHandled = true; + ctx.Handler = NullHandler; }) .Returns(Task.FromResult(true)); var constraints = new Dictionary(); constraints.Add("id", new RangeRouteConstraint(1, 20)); - var route = new TemplateRoute( + var route = new Route( mockTarget.Object, template, defaults: null, @@ -585,7 +327,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.True(routeValues.ContainsKey("id")); Assert.Equal("5", routeValues["id"]); @@ -608,11 +350,11 @@ namespace Microsoft.AspNet.Routing.Template .Callback(ctx => { routeValues = ctx.RouteData.Values; - ctx.IsHandled = true; + ctx.Handler = NullHandler; }) .Returns(Task.FromResult(true)); - var route = new TemplateRoute( + var route = new Route( mockTarget.Object, template, defaults: null, @@ -627,7 +369,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.False(context.IsHandled); + Assert.Null(context.Handler); } #region Route Matching @@ -644,7 +386,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.Equal(2, context.RouteData.Values.Count); Assert.Equal("Home", context.RouteData.Values["controller"]); Assert.Equal("Index", context.RouteData.Values["action"]); @@ -661,7 +403,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.Equal(0, context.RouteData.Values.Count); } @@ -676,7 +418,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.Equal(2, context.RouteData.Values.Count); Assert.Equal("Home", context.RouteData.Values["controller"]); Assert.Equal("Index", context.RouteData.Values["action"]); @@ -695,7 +437,7 @@ namespace Microsoft.AspNet.Routing.Template // Act await route.RouteAsync(context); - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); // This should not affect the route - RouteData.DataTokens is a copy context.RouteData.DataTokens.Add("company", "contoso"); @@ -716,7 +458,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.False(context.IsHandled); + Assert.Null(context.Handler); } [Fact] @@ -730,25 +472,11 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.False(context.IsHandled); + Assert.Null(context.Handler); - // Issue #16 tracks this. - Assert.Empty(context.RouteData.Values); - } - - [Fact] - public async Task Match_RejectedByHandler_ClearsRouters() - { - // Arrange - var route = CreateRoute("{controller}", handleRequest: false); - var context = CreateRouteContext("/Home"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.False(context.IsHandled); - Assert.Empty(context.RouteData.Routers); + var value = Assert.Single(context.RouteData.Values); + Assert.Equal("controller", value.Key); + Assert.Equal("Home", Assert.IsType(value.Value)); } [Fact] @@ -763,7 +491,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.Equal(1, context.RouteData.Routers.Count); Assert.Same(target, context.RouteData.Routers[0]); } @@ -793,7 +521,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.Equal(3, context.RouteData.Values.Count); Assert.Equal("Home", context.RouteData.Values["controller"]); Assert.Equal("Create", context.RouteData.Values["action"]); @@ -811,7 +539,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.Equal(2, context.RouteData.Values.Count); Assert.Equal("Home", context.RouteData.Values["controller"]); Assert.Equal("Create", context.RouteData.Values["action"]); @@ -828,7 +556,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.Equal(3, context.RouteData.Values.Count); Assert.Equal("Home", context.RouteData.Values["controller"]); Assert.Equal("Create", context.RouteData.Values["action"]); @@ -846,7 +574,7 @@ namespace Microsoft.AspNet.Routing.Template await route.RouteAsync(context); // Assert - Assert.False(context.IsHandled); + Assert.Null(context.Handler); } private static RouteContext CreateRouteContext(string requestPath, ILoggerFactory factory = null) @@ -1021,14 +749,14 @@ namespace Microsoft.AspNet.Routing.Template // Arrange var context = CreateVirtualPathContext(new { p1 = "abcd" }); - TemplateRoute r = CreateRoute( + var route = CreateRoute( "{p1}/{p2}", new { p2 = "catchall" }, true, new RouteValueDictionary(new { p2 = "\\d{4}" })); // Act - var virtualPath = r.GetVirtualPath(context); + var virtualPath = route.GetVirtualPath(context); // Assert Assert.Null(virtualPath); @@ -1062,14 +790,14 @@ namespace Microsoft.AspNet.Routing.Template // Arrange var context = CreateVirtualPathContext(new { p1 = "abcd" }); - TemplateRoute r = CreateRoute( + var route = CreateRoute( "{p1}/{*p2}", new { p2 = "catchall" }, true, new RouteValueDictionary(new { p2 = "\\d{4}" })); // Act - var virtualPath = r.GetVirtualPath(context); + var virtualPath = route.GetVirtualPath(context); // Assert Assert.Null(virtualPath); @@ -1081,7 +809,7 @@ namespace Microsoft.AspNet.Routing.Template // Arrange var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" }); - TemplateRoute route = CreateRoute( + var route = CreateRoute( "{p1}/{*p2}", new { p2 = "catchall" }, true, @@ -1112,7 +840,7 @@ namespace Microsoft.AspNet.Routing.Template .Returns(true) .Verifiable(); - TemplateRoute route = CreateRoute( + var route = CreateRoute( "{p1}/{p2}", new { p2 = "catchall" }, true, @@ -1623,7 +1351,7 @@ namespace Microsoft.AspNet.Routing.Template dataTokens: dataToken); // Assert - var templateRoute = (TemplateRoute)routeBuilder.Routes[0]; + var templateRoute = (Route)routeBuilder.Routes[0]; // Assert Assert.Equal(expectedDictionary.Count, templateRoute.DataTokens.Count); @@ -1664,7 +1392,7 @@ namespace Microsoft.AspNet.Routing.Template defaults: null, constraints: new { controller = "a.*", action = mockConstraint }); - var constraints = ((TemplateRoute)routeBuilder.Routes[0]).Constraints; + var constraints = ((Route)routeBuilder.Routes[0]).Constraints; // Assert Assert.Equal(2, constraints.Count); @@ -1685,7 +1413,7 @@ namespace Microsoft.AspNet.Routing.Template constraints: new { id = "1*" }); // Assert - var constraints = ((TemplateRoute)routeBuilder.Routes[0]).Constraints; + var constraints = ((Route)routeBuilder.Routes[0]).Constraints; Assert.Equal(1, constraints.Count); var constraint = (CompositeRouteConstraint)constraints["id"]; Assert.IsType(constraint); @@ -1706,7 +1434,7 @@ namespace Microsoft.AspNet.Routing.Template constraints: null); // Assert - var constraints = ((TemplateRoute)routeBuilder.Routes[0]).Constraints; + var constraints = ((Route)routeBuilder.Routes[0]).Constraints; Assert.Equal(1, constraints.Count); Assert.IsType(constraints["id"]); } @@ -1720,7 +1448,7 @@ namespace Microsoft.AspNet.Routing.Template routeBuilder.MapRoute(name: "RouteName", template: "{controller}/{action}", defaults: null); // Act - var name = ((TemplateRoute)routeBuilder.Routes[0]).Name; + var name = ((Route)routeBuilder.Routes[0]).Name; // Assert Assert.Equal("RouteName", name); @@ -1738,7 +1466,7 @@ namespace Microsoft.AspNet.Routing.Template constraints: null); // Act - var name = ((TemplateRoute)routeBuilder.Routes[0]).Name; + var name = ((Route)routeBuilder.Routes[0]).Name; // Assert Assert.Equal("RouteName", name); @@ -1761,7 +1489,7 @@ namespace Microsoft.AspNet.Routing.Template { var routeBuilder = new RouteBuilder(); - routeBuilder.DefaultHandler = new Mock().Object; + routeBuilder.DefaultHandler = new RouteHandler(NullHandler); var serviceProviderMock = new Mock(); serviceProviderMock.Setup(o => o.GetService(typeof(IInlineConstraintResolver))) @@ -1771,9 +1499,9 @@ namespace Microsoft.AspNet.Routing.Template return routeBuilder; } - private static TemplateRoute CreateRoute(string routeName, string template, bool handleRequest = true) + private static Route CreateRoute(string routeName, string template, bool handleRequest = true) { - return new TemplateRoute( + return new Route( CreateTarget(handleRequest), routeName, template, @@ -1783,49 +1511,51 @@ namespace Microsoft.AspNet.Routing.Template inlineConstraintResolver: _inlineConstraintResolver); } - private static TemplateRoute CreateRoute(string template, bool handleRequest = true) + private static Route CreateRoute(string template, bool handleRequest = true) { - return new TemplateRoute(CreateTarget(handleRequest), template, _inlineConstraintResolver); + return new Route(CreateTarget(handleRequest), template, _inlineConstraintResolver); } - private static TemplateRoute CreateRoute(string template, - object defaults, - bool handleRequest = true, - object constraints = null, - object dataTokens = null) + private static Route CreateRoute( + string template, + object defaults, + bool handleRequest = true, + object constraints = null, + object dataTokens = null) { - return new TemplateRoute(CreateTarget(handleRequest), - template, - new RouteValueDictionary(defaults), - (constraints as IDictionary) ?? - new RouteValueDictionary(constraints), - (dataTokens as IDictionary) ?? - new RouteValueDictionary(dataTokens), - _inlineConstraintResolver); + return new Route( + CreateTarget(handleRequest), + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(constraints), + new RouteValueDictionary(dataTokens), + _inlineConstraintResolver); } - private static TemplateRoute CreateRoute(IRouter target, string template) + private static Route CreateRoute(IRouter target, string template) { - return new TemplateRoute(target, - template, - new RouteValueDictionary(), - constraints: null, - dataTokens: null, - inlineConstraintResolver: _inlineConstraintResolver); + return new Route( + target, + template, + new RouteValueDictionary(), + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); } - private static TemplateRoute CreateRoute( + private static Route CreateRoute( IRouter target, string template, object defaults, RouteValueDictionary dataTokens = null) { - return new TemplateRoute(target, - template, - new RouteValueDictionary(defaults), - constraints: null, - dataTokens: dataTokens, - inlineConstraintResolver: _inlineConstraintResolver); + return new Route( + target, + template, + new RouteValueDictionary(defaults), + constraints: null, + dataTokens: dataTokens, + inlineConstraintResolver: _inlineConstraintResolver); } private static IRouter CreateTarget(bool handleRequest = true) @@ -1837,7 +1567,7 @@ namespace Microsoft.AspNet.Routing.Template target .Setup(e => e.RouteAsync(It.IsAny())) - .Callback((c) => c.IsHandled = handleRequest) + .Callback((c) => c.Handler = handleRequest ? NullHandler : null) .Returns(Task.FromResult(null)); return target.Object; diff --git a/test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs index ecb7c7590b..3a3fa0b0d9 100644 --- a/test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs @@ -91,7 +91,7 @@ namespace Microsoft.AspNet.Routing public Task RouteAsync(RouteContext context) { - context.IsHandled = _isHandled; + context.Handler = _isHandled ? (RequestDelegate)((c) => Task.FromResult(0)) : null; return Task.FromResult(null); } } diff --git a/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs b/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs index cbe6504429..fbea8c950d 100644 --- a/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Routing.Tests constraints: null); // Assert - var defaults = ((Template.TemplateRoute)routeBuilder.Routes[0]).Defaults; + var defaults = ((Route)routeBuilder.Routes[0]).Defaults; Assert.Equal("12", defaults["id"]); } @@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Routing.Tests constraints: null); // Assert - var defaults = ((Template.TemplateRoute)routeBuilder.Routes[0]).Defaults; + var defaults = ((Route)routeBuilder.Routes[0]).Defaults; Assert.Equal(value, defaults["p1"]); } diff --git a/test/Microsoft.AspNet.Routing.Tests/Tree/TreeRouterTest.cs b/test/Microsoft.AspNet.Routing.Tests/Tree/TreeRouterTest.cs index 768f59cd55..fe8a4c1f10 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Tree/TreeRouterTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Tree/TreeRouterTest.cs @@ -17,6 +17,8 @@ namespace Microsoft.AspNet.Routing.Tree { public class TreeRouterTest { + private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0); + [Theory] [InlineData("template/5", "template/{parameter:int}")] [InlineData("template/5", "template/{parameter}")] @@ -36,15 +38,12 @@ namespace Microsoft.AspNet.Routing.Tree // Arrange var expectedRouteGroup = string.Format("{0}&&{1}", 0, firstTemplate); - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => c.Handler = NullHandler) + .Returns(Task.FromResult(true)) + .Verifiable(); var firstRoute = CreateMatchingEntry(next.Object, firstTemplate, order: 0); var secondRoute = CreateMatchingEntry(next.Object, secondTemplate, order: 0); @@ -84,15 +83,12 @@ namespace Microsoft.AspNet.Routing.Tree // Arrange var expectedRouteGroup = string.Format("{0}&&{1}", 0, secondTemplate); - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => c.Handler = NullHandler) + .Returns(Task.FromResult(true)) + .Verifiable(); var firstRoute = CreateMatchingEntry(next.Object, firstTemplate, order: 1); var secondRoute = CreateMatchingEntry(next.Object, secondTemplate, order: 0); @@ -126,15 +122,12 @@ namespace Microsoft.AspNet.Routing.Tree // Arrange var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => c.Handler = NullHandler) + .Returns(Task.FromResult(true)) + .Verifiable(); var firstRoute = CreateMatchingEntry(next.Object, template, order: 1); var secondRoute = CreateMatchingEntry(next.Object, template, order: 0); @@ -166,15 +159,12 @@ namespace Microsoft.AspNet.Routing.Tree // Arrange var expectedRouteGroup = string.Format("{0}&&{1}", 0, first); - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => c.Handler = NullHandler) + .Returns(Task.FromResult(true)) + .Verifiable(); var secondRouter = new Mock(MockBehavior.Strict); @@ -211,15 +201,12 @@ namespace Microsoft.AspNet.Routing.Tree // Arrange var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => c.Handler = NullHandler) + .Returns(Task.FromResult(true)) + .Verifiable(); var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); @@ -239,12 +226,12 @@ namespace Microsoft.AspNet.Routing.Tree // Assert if (expectedResult) { - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); } else { - Assert.False(context.IsHandled); + Assert.Null(context.Handler); } } @@ -277,15 +264,12 @@ namespace Microsoft.AspNet.Routing.Tree // Arrange var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => c.Handler = NullHandler) + .Returns(Task.FromResult(true)) + .Verifiable(); var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); @@ -300,7 +284,7 @@ namespace Microsoft.AspNet.Routing.Tree await route.RouteAsync(context); // Assert - Assert.True(context.IsHandled); + Assert.NotNull(context.Handler); if (p1 != null) { Assert.Equal(p1, context.RouteData.Values["p1"]); @@ -335,15 +319,12 @@ namespace Microsoft.AspNet.Routing.Tree // Arrange var expectedRouteGroup = string.Format("{0}&&{1}", 0, template); - // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn. - var numberOfCalls = 0; - Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; }; - var next = new Mock(); - next.Setup(r => r.RouteAsync(It.IsAny())) - .Callback(callBack) - .Returns(Task.FromResult(true)) - .Verifiable(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => c.Handler = NullHandler) + .Returns(Task.FromResult(true)) + .Verifiable(); var firstRoute = CreateMatchingEntry(next.Object, template, order: 0); @@ -359,7 +340,7 @@ namespace Microsoft.AspNet.Routing.Tree await route.RouteAsync(context); // Assert - Assert.False(context.IsHandled); + Assert.Null(context.Handler); } [Theory] @@ -1358,15 +1339,17 @@ namespace Microsoft.AspNet.Routing.Tree { // Arrange RouteData nestedRouteData = null; + var next = new Mock(); next .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => + .Callback(c => { nestedRouteData = c.RouteData; - c.IsHandled = true; + c.Handler = NullHandler; }) - .Returns(Task.FromResult(true)); + .Returns(Task.FromResult(true)) + .Verifiable(); var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); var route = CreateAttributeRoute(next.Object, entry); @@ -1399,11 +1382,9 @@ namespace Microsoft.AspNet.Routing.Tree var router = new Mock(); router .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => - { - c.IsHandled = true; - }) - .Returns(Task.FromResult(true)); + .Callback(c => c.Handler = NullHandler) + .Returns(Task.FromResult(true)) + .Verifiable(); var entry = CreateMatchingEntry(router.Object, "Foo/{*path}", order: 0); var route = CreateAttributeRoute(router.Object, entry); @@ -1427,11 +1408,9 @@ namespace Microsoft.AspNet.Routing.Tree var router = new Mock(); router .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => - { - c.IsHandled = true; - }) - .Returns(Task.FromResult(true)); + .Callback(c => c.Handler = NullHandler) + .Returns(Task.FromResult(true)) + .Verifiable(); var entry = CreateMatchingEntry(router.Object, "Foo/{*path}", order: 0); var route = CreateAttributeRoute(router.Object, entry); @@ -1456,12 +1435,12 @@ namespace Microsoft.AspNet.Routing.Tree var next = new Mock(); next .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => + .Callback(c => { nestedRouteData = c.RouteData; - c.IsHandled = false; }) - .Returns(Task.FromResult(true)); + .Returns(Task.FromResult(true)) + .Verifiable(); var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); var route = CreateAttributeRoute(next.Object, entry); @@ -1499,10 +1478,9 @@ namespace Microsoft.AspNet.Routing.Tree var next = new Mock(); next .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => + .Callback(c => { nestedRouteData = c.RouteData; - c.IsHandled = false; }) .Throws(new Exception()); @@ -1755,7 +1733,7 @@ namespace Microsoft.AspNet.Routing.Tree return builder.Build(version: 1); } - private static IReadOnlyDictionary GetRouteConstriants( + private static IDictionary GetRouteConstriants( IInlineConstraintResolver inlineConstraintResolver, string template, RouteTemplate parsedRouteTemplate) @@ -1797,11 +1775,11 @@ namespace Microsoft.AspNet.Routing.Tree { if (MatchingDelegate == null) { - context.IsHandled = true; + context.Handler = NullHandler; } else { - context.IsHandled = MatchingDelegate(context); + context.Handler = MatchingDelegate(context) ? NullHandler : null; } return Task.FromResult(true);