public void Route(MultiTenantRouteContext routeContext) { for (var i = 0; i < Routes.Length; i++) { Routes[i].Match(routeContext); if (routeContext.Handler != null) { return; } } }
internal void Match(MultiTenantRouteContext context) { // 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. 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 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 = string.Empty; 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 (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 (allRouteSegmentsMatch && allNonOptionalSegmentsMatch) { context.Parameters = parameters; context.Handler = Handler; } }
internal void Match(MultiTenantRouteContext context) { var pathIndex = 0; var templateIndex = 0; Dictionary <string, object?>?parameters = null; // We will iterate over the path segments and the template segments until we have consumed // one of them. // There are three cases we need to account here for: // * Path is shorter than template -> // * This can match only if we have t-p optional parameters at the end. // * Path and template have the same number of segments // * This can happen when the catch-all segment matches 1 segment // * This can happen when an optional parameter has been specified. // * This can happen when the route only contains literals and parameters. // * Path is longer than template -> This can only match if the parameter has a catch-all at the end. // * We still need to iterate over all the path segments if the catch-all is constrained. // * We still need to iterate over all the template/path segments before the catch-all while (pathIndex < context.Segments.Length && templateIndex < Template.Segments.Length) { var pathSegment = context.Segments[pathIndex]; var templateSegment = Template.Segments[templateIndex]; var matches = templateSegment.Match(pathSegment, out var match); if (!matches) { // A constraint or literal didn't match return; } if (!templateSegment.IsCatchAll) { // We were dealing with a literal or a parameter, so just advance both cursors. pathIndex++; templateIndex++; if (templateSegment.IsParameter) { parameters ??= new(StringComparer.OrdinalIgnoreCase); parameters[templateSegment.Value] = match; } } else { if (templateSegment.Constraints.Length == 0) { // Unconstrained catch all, we can stop early parameters ??= new(StringComparer.OrdinalIgnoreCase); parameters[templateSegment.Value] = string.Join('/', context.Segments, pathIndex, context.Segments.Length - pathIndex); // Mark the remaining segments as consumed. pathIndex = context.Segments.Length; // Catch-alls are always last. templateIndex++; // We are done, so break out of the loop. break; } else { // For constrained catch-alls, we advance the path index but keep the template index on the catch-all. pathIndex++; if (pathIndex == context.Segments.Length) { parameters ??= new(StringComparer.OrdinalIgnoreCase); parameters[templateSegment.Value] = string.Join('/', context.Segments, templateIndex, context.Segments.Length - templateIndex); // This is important to signal that we consumed the entire template. templateIndex++; } } } } var hasRemainingOptionalSegments = templateIndex < Template.Segments.Length && RemainingSegmentsAreOptional(pathIndex, Template.Segments); if ((pathIndex == context.Segments.Length && templateIndex == Template.Segments.Length) || hasRemainingOptionalSegments) { if (hasRemainingOptionalSegments) { parameters ??= new Dictionary <string, object?>(StringComparer.Ordinal); AddDefaultValues(parameters, templateIndex, Template.Segments); } if (UnusedRouteParameterNames?.Length > 0) { parameters ??= new Dictionary <string, object?>(StringComparer.Ordinal); for (var i = 0; i < UnusedRouteParameterNames.Length; i++) { parameters[UnusedRouteParameterNames[i]] = null; } } context.Handler = Handler; context.Parameters = parameters; } }
private void Refresh(bool isNavigationIntercepted) { // If an `OnNavigateAsync` task is currently in progress, then wait // for it to complete before rendering. Note: because _previousOnNavigateTask // is initialized to a CompletedTask on initialization, this will still // allow first-render to complete successfully. if (_previousOnNavigateTask.Status != TaskStatus.RanToCompletion) { if (Navigating != null) { _renderHandle.Render(Navigating); } return; } RefreshRouteTable(); var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute); locationPath = StringUntilAny(locationPath, _queryOrHashStartChar); MultiTenantRouteContext context = null; try { context = new MultiTenantRouteContext(locationPath, Routes, LoggerFactory); } catch (TenantException tex) { _renderHandle.Render(NoTenant(tex.Message)); } if (context != null) { Routes.Route(context); if (context.Handler != null) { if (!typeof(IComponent).IsAssignableFrom(context.Handler)) { throw new InvalidOperationException($"The type {context.Handler.FullName} " + $"does not implement {typeof(IComponent).FullName}."); } Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri); var routeData = new RouteData( context.Handler, context.Parameters ?? _emptyParametersDictionary); _renderHandle.Render(Found(routeData)); } else { if (!isNavigationIntercepted) { Log.DisplayingNotFound(_logger, locationPath, _baseUri); // We did not find a Component that matches the route. // Only show the NotFound content if the application developer programatically got us here i.e we did not // intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content. _renderHandle.Render(NotFound); } else { Log.NavigatingToExternalUri(_logger, _locationAbsolute, locationPath, _baseUri); NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true); } } } }