public void CanMatchMultipleOptionalParameterWithConstraint() { // Arrange var template = "/optional/{value:datetime?}/{value2:datetime?}"; var contextUrl = "/optional//"; object convertedValue = null; var routeTable = new TestRouteTableBuilder().AddRoute(template).Build(); var context = new RouteContext(contextUrl); // Act routeTable.Route(context); // Assert if (context.Handler == null) { // Make it easier to track down failing tests when using MemberData throw new InvalidOperationException($"Failed to match template '{template}'."); } Assert.Equal(new Dictionary <string, object> { { "value", convertedValue }, { "value2", convertedValue } }, context.Parameters); }
public void Route(RouteContext routeContext) { for (var i = 0; i < Routes.Length; i++) { Routes[i].Match(routeContext); if (routeContext.Handler != null) { return; } } }
public void RouteMatchingIsCaseInsensitive() { // Arrange var routeTable = new TestRouteTableBuilder().AddRoute("/some/AWESOME/route/").Build(); var context = new RouteContext("/Some/awesome/RouTe"); // Act routeTable.Route(context); // Assert Assert.NotNull(context.Handler); }
public void DoesNotMatchIfDifferentNumberOfSegments(string path) { // Arrange var routeTable = new TestRouteTableBuilder().AddRoute("/some/awesome/route/").Build(); var context = new RouteContext(path); // Act routeTable.Route(context); // Assert Assert.Null(context.Handler); }
public void DoesNotMatchIfConstraintDoesNotMatch(string template, string contextUrl) { // Arrange var routeTable = new TestRouteTableBuilder().AddRoute(template).Build(); var context = new RouteContext(contextUrl); // Act routeTable.Route(context); // Assert Assert.Null(context.Handler); }
public void DoesNotMatchIfSegmentsDontMatch() { // Arrange var routeTable = new TestRouteTableBuilder().AddRoute("/some/AWESOME/route/").Build(); var context = new RouteContext("/some/brilliant/route"); // Act routeTable.Route(context); // Assert Assert.Null(context.Handler); }
public void CanMatchEncodedSegments() { // Arrange var routeTable = new TestRouteTableBuilder().AddRoute("/some/ünicõdē/🛣/").Build(); var context = new RouteContext("/some/%C3%BCnic%C3%B5d%C4%93/%F0%9F%9B%A3"); // Act routeTable.Route(context); // Assert Assert.NotNull(context.Handler); }
public void CanMatchRootTemplate() { // Arrange var routeTable = new TestRouteTableBuilder().AddRoute("/").Build(); var context = new RouteContext("/"); // Act routeTable.Route(context); // Assert Assert.NotNull(context.Handler); }
public void CanMatchTemplateWithMultipleLiterals() { // Arrange var routeTable = new TestRouteTableBuilder().AddRoute("/some/awesome/route/").Build(); var context = new RouteContext("/some/awesome/route"); // Act routeTable.Route(context); // Assert Assert.NotNull(context.Handler); }
public void CanMatchCatchAllParameterTemplate(string path, string expectedValue) { // Arrange var routeTable = new TestRouteTableBuilder().AddRoute("/blog/{*parameter}").Build(); var context = new RouteContext(path); // Act routeTable.Route(context); // Assert Assert.NotNull(context.Handler); Assert.Single(context.Parameters, p => p.Key == "parameter" && (string)p.Value == expectedValue); }
public void CanMatchSegmentWithMultipleConstraints(string template, string contextUrl, object convertedValue) { // Arrange var routeTable = new TestRouteTableBuilder().AddRoute(template).Build(); var context = new RouteContext(contextUrl); // Act routeTable.Route(context); // Assert Assert.Equal(new Dictionary <string, object> { { "value", convertedValue } }, context.Parameters); }
public void PrefersLiteralTemplateOverTemplateWithOptionalParameters() { // Arrange var routeTable = new TestRouteTableBuilder() .AddRoute("/users/1", typeof(TestHandler1)) .AddRoute("/users/{id?}", typeof(TestHandler2)) .Build(); var context = new RouteContext("/users/1"); // Act routeTable.Route(context); // Assert Assert.NotNull(context.Handler); Assert.Null(context.Parameters); }
public void PrefersLiteralTemplateOverTemplateWithParameters() { // Arrange var routeTable = new TestRouteTableBuilder() .AddRoute("/an/awesome/path", typeof(TestHandler1)) .AddRoute("/{some}/awesome/{route}/", typeof(TestHandler2)) .Build(); var context = new RouteContext("/an/awesome/path"); // Act routeTable.Route(context); // Assert Assert.NotNull(context.Handler); Assert.Null(context.Parameters); }
public void SuppliesNullForUnusedHandlerParameters() { // Arrange var routeTable = new TestRouteTableBuilder() .AddRoute("/", typeof(TestHandler1)) .AddRoute("/products/{param1:int}", typeof(TestHandler1)) .AddRoute("/products/{param2}/{PaRam1}", typeof(TestHandler1)) .AddRoute("/{unrelated}", typeof(TestHandler2)) .Build(); var context = new RouteContext("/products/456"); // Act routeTable.Route(context); // Assert Assert.Collection(routeTable.Routes, route => { Assert.Same(typeof(TestHandler1), route.Handler); Assert.Equal("/", route.Template.TemplateText); Assert.Equal(new[] { "param1", "param2" }, route.UnusedRouteParameterNames); }, route => { Assert.Same(typeof(TestHandler2), route.Handler); Assert.Equal("{unrelated}", route.Template.TemplateText); Assert.Equal(Array.Empty <string>(), route.UnusedRouteParameterNames); }, route => { Assert.Same(typeof(TestHandler1), route.Handler); Assert.Equal("products/{param1:int}", route.Template.TemplateText); Assert.Equal(new[] { "param2" }, route.UnusedRouteParameterNames); }, route => { Assert.Same(typeof(TestHandler1), route.Handler); Assert.Equal("products/{param2}/{PaRam1}", route.Template.TemplateText); Assert.Equal(Array.Empty <string>(), route.UnusedRouteParameterNames); }); Assert.Same(typeof(TestHandler1), context.Handler); Assert.Equal(new Dictionary <string, object> { { "param1", 456 }, { "param2", null }, }, context.Parameters); }
public void PrefersRoutesThatMatchMoreSegments() { // Arrange var routeTable = new TestRouteTableBuilder() .AddRoute("/{anythingGoes}", typeof(TestHandler1)) .AddRoute("/users/{id?}", typeof(TestHandler2)) .Build(); var context = new RouteContext("/users/1"); // Act routeTable.Route(context); // Assert Assert.NotNull(context.Handler); Assert.Equal(typeof(TestHandler2), context.Handler); Assert.NotNull(context.Parameters); }
public void PrefersMoreConstraintsOverFewer() { // Arrange var routeTable = new TestRouteTableBuilder() .AddRoute("/products/{id}") .AddRoute("/products/{id:int}").Build(); var context = new RouteContext("/products/456"); // Act routeTable.Route(context); // Assert Assert.NotNull(context.Handler); Assert.Equal(context.Parameters, new Dictionary <string, object> { { "id", 456 } }); }
public void PrefersLiteralTemplateOverParameterizedTemplates() { // Arrange var routeTable = new TestRouteTableBuilder() .AddRoute("/users/1/friends", typeof(TestHandler1)) .AddRoute("/users/{id}/{location}", typeof(TestHandler2)) .AddRoute("/users/1/{location}", typeof(TestHandler2)) .Build(); var context = new RouteContext("/users/1/friends"); // Act routeTable.Route(context); // Assert Assert.NotNull(context.Handler); Assert.Equal(typeof(TestHandler1), context.Handler); Assert.Null(context.Parameters); }
public void CanMatchTemplateWithMultipleParameters() { // Arrange var routeTable = new TestRouteTableBuilder().AddRoute("/{some}/awesome/{route}/").Build(); var context = new RouteContext("/an/awesome/path"); var expectedParameters = new Dictionary <string, object> { ["some"] = "an", ["route"] = "path" }; // Act routeTable.Route(context); // Assert Assert.NotNull(context.Handler); Assert.Equal(expectedParameters, context.Parameters); }
public void PrefersOptionalParamsOverNonOptionalParamsReverseOrder() { // Arrange var routeTable = new TestRouteTableBuilder() .AddRoute("/users/{id}", typeof(TestHandler1)) .AddRoute("/users/{id?}", typeof(TestHandler2)) .Build(); var contextWithParam = new RouteContext("/users/1"); var contextWithoutParam = new RouteContext("/users/"); // Act routeTable.Route(contextWithParam); routeTable.Route(contextWithoutParam); // Assert Assert.NotNull(contextWithParam.Handler); Assert.Equal(typeof(TestHandler1), contextWithParam.Handler); Assert.NotNull(contextWithoutParam.Handler); Assert.Equal(typeof(TestHandler2), contextWithoutParam.Handler); }
internal void Match(RouteContext context) { string?catchAllValue = null; // If this template contains a catch-all parameter, we can concatenate the pathSegments // at and beyond the catch-all segment's position. For example: // Template: /foo/bar/{*catchAll} // PathSegments: /foo/bar/one/two/three if (Template.ContainsCatchAllSegment && context.Segments.Length >= Template.Segments.Length) { catchAllValue = string.Join('/', context.Segments[Range.StartAt(Template.Segments.Length - 1)]); } // If there are no optional segments on the route and the length of the route // and the template do not match, then there is no chance of this matching and // we can bail early. else if (Template.OptionalSegmentsCount == 0 && Template.Segments.Length != context.Segments.Length) { return; } // Parameters will be lazily initialized. Dictionary <string, object> parameters = null; var numMatchingSegments = 0; for (var i = 0; i < Template.Segments.Length; i++) { var segment = Template.Segments[i]; if (segment.IsCatchAll) { numMatchingSegments += 1; parameters ??= new Dictionary <string, object>(StringComparer.Ordinal); parameters[segment.Value] = catchAllValue; break; } // If the template contains more segments than the path, then // we may need to break out of this for-loop. This can happen // in one of two cases: // // (1) If we are comparing a literal route with a literal template // and the route is shorter than the template. // (2) If we are comparing a template where the last value is an optional // parameter that the route does not provide. if (i >= context.Segments.Length) { // If we are under condition (1) above then we can stop evaluating // matches on the rest of this template. if (!segment.IsParameter && !segment.IsOptional) { break; } } string pathSegment = null; if (i < context.Segments.Length) { pathSegment = context.Segments[i]; } if (!segment.Match(pathSegment, out var matchedParameterValue)) { return; } else { numMatchingSegments++; if (segment.IsParameter) { parameters ??= new Dictionary <string, object>(StringComparer.Ordinal); parameters[segment.Value] = matchedParameterValue; } } } // In addition to extracting parameter values from the URL, each route entry // also knows which other parameters should be supplied with null values. These // are parameters supplied by other route entries matching the same handler. if (!Template.ContainsCatchAllSegment && UnusedRouteParameterNames.Length > 0) { parameters ??= new Dictionary <string, object>(StringComparer.Ordinal); for (var i = 0; i < UnusedRouteParameterNames.Length; i++) { parameters[UnusedRouteParameterNames[i]] = null; } } // We track the number of segments in the template that matched // against this particular route then only select the route that // matches the most number of segments on the route that was passed. // This check is an exactness check that favors the more precise of // two templates in the event that the following route table exists. // Route 1: /{anythingGoes} // Route 2: /users/{id:int} // And the provided route is `/users/1`. We want to choose Route 2 // over Route 1. // Furthermore, literal routes are preferred over parameterized routes. // If the two routes below are registered in the route table. // Route 1: /users/1 // Route 2: /users/{id:int} // And the provided route is `/users/1`. We want to choose Route 1 over // Route 2. var allRouteSegmentsMatch = numMatchingSegments >= context.Segments.Length; // Checking that all route segments have been matches does not suffice if we are // comparing literal templates with literal routes. For example, the template // `/this/is/a/template` and the route `/this/`. In that case, we want to ensure // that all non-optional segments have matched as well. var allNonOptionalSegmentsMatch = numMatchingSegments >= (Template.Segments.Length - Template.OptionalSegmentsCount); if (Template.ContainsCatchAllSegment || (allRouteSegmentsMatch && allNonOptionalSegmentsMatch)) { context.Parameters = parameters; context.Handler = Handler; } }