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);