Last active
January 15, 2026 08:53
-
-
Save RichardD2/bffa095dbff1c35b35dcdd47a59c6f7e to your computer and use it in GitHub Desktop.
ASP.NET unit test class to resolve matching endpoints
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using Microsoft.AspNetCore.Http; | |
| using Microsoft.AspNetCore.Routing; | |
| using Microsoft.AspNetCore.Routing.Matching; | |
| using Microsoft.AspNetCore.Routing.Template; | |
| using Microsoft.Extensions.Logging; | |
| namespace UnitTests.Routing; | |
| public sealed class ApiRouteTester( | |
| IEnumerable<EndpointDataSource> endpointDataSources, | |
| EndpointSelector endpointSelector, | |
| ParameterPolicyFactory policyFactory, | |
| ILogger<ApiRouteTester> logger) | |
| { | |
| private readonly IEnumerable<EndpointDataSource> _endpointDataSources = endpointDataSources; | |
| private readonly EndpointSelector _endpointSelector = endpointSelector; | |
| private readonly ParameterPolicyFactory _policyFactory = policyFactory; | |
| private readonly ILogger<ApiRouteTester> _logger = logger; | |
| public async ValueTask<Endpoint?> ResolveServerRoute(string httpMethod, PathString path) | |
| { | |
| List<Endpoint> endpoints = []; | |
| List<RouteValueDictionary> endpointRouteValues = []; | |
| foreach (var endpoint in _endpointDataSources.SelectMany(s => s.Endpoints).Cast<RouteEndpoint>()) | |
| { | |
| RouteValueDictionary routeValues = []; | |
| if (!MatchesPath(endpoint, path, routeValues)) continue; | |
| endpoints.Add(endpoint); | |
| endpointRouteValues.Add(routeValues); | |
| } | |
| if (endpoints.Count == 0) | |
| { | |
| _logger.LogCritical("[{Method}] {Path}: No matching endpoints.", httpMethod, path); | |
| return null; | |
| } | |
| CandidateSet candidateSet = new([.. endpoints], [.. endpointRouteValues], new int[endpoints.Count]); | |
| for (int index = 0; index < endpoints.Count; index++) | |
| { | |
| var endpoint = (RouteEndpoint)endpoints[index]; | |
| var routeValues = endpointRouteValues[index]; | |
| if (!MatchesMethod(endpoint, httpMethod)) | |
| { | |
| candidateSet.SetValidity(index, false); | |
| continue; | |
| } | |
| if (!MatchesParameters(endpoint, routeValues)) | |
| { | |
| candidateSet.SetValidity(index, false); | |
| continue; | |
| } | |
| } | |
| HttpContext httpContext = new DefaultHttpContext | |
| { | |
| Request = | |
| { | |
| Method = httpMethod, | |
| Path = path, | |
| } | |
| }; | |
| try | |
| { | |
| httpContext.SetEndpoint(null); | |
| await _endpointSelector.SelectAsync(httpContext, candidateSet).ConfigureAwait(false); | |
| return httpContext.GetEndpoint(); | |
| } | |
| catch (Exception ex) | |
| { | |
| _logger.LogError("[{Method}] {Path}\n{Message}", httpMethod, path, ex.Message); | |
| return null; | |
| } | |
| } | |
| private static bool MatchesPath(RouteEndpoint endpoint, PathString path, RouteValueDictionary routeValues) | |
| { | |
| string routePattern = endpoint.RoutePattern.RawText ?? string.Empty; | |
| var template = TemplateParser.Parse(routePattern); | |
| TemplateMatcher matcher = new(template, GetDefaults(template)); | |
| return matcher.TryMatch(path, routeValues); | |
| } | |
| private static bool MatchesMethod(Endpoint endpoint, string httpMethod) | |
| { | |
| var methods = endpoint.Metadata.OfType<HttpMethodMetadata>().FirstOrDefault(); | |
| return methods?.HttpMethods.Contains(httpMethod) ?? false; | |
| } | |
| private bool MatchesParameters(RouteEndpoint endpoint, RouteValueDictionary values) | |
| { | |
| foreach (var parameter in endpoint.RoutePattern.Parameters) | |
| { | |
| foreach (var policyRef in parameter.ParameterPolicies) | |
| { | |
| switch (_policyFactory.Create(parameter, policyRef)) | |
| { | |
| case IRouteConstraint constraint: | |
| { | |
| if (!constraint.Match(null, null, parameter.Name, values, RouteDirection.IncomingRequest)) | |
| { | |
| return false; | |
| } | |
| break; | |
| } | |
| case { } policy: | |
| { | |
| _logger.LogWarning("TODO: Evaluate policy {Policy} on {Name}", policy, parameter.Name); | |
| break; | |
| } | |
| default: | |
| { | |
| _logger.LogWarning("Invalid policy {Policy} on {Name}", policyRef, parameter.Name); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| private static RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate) | |
| { | |
| RouteValueDictionary result = []; | |
| foreach (var parameter in parsedTemplate.Parameters) | |
| { | |
| if (parameter.Name is not null && parameter.DefaultValue is not null) | |
| { | |
| result.Add(parameter.Name, parameter.DefaultValue); | |
| } | |
| } | |
| return result; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment