Skip to content

Instantly share code, notes, and snippets.

@RichardD2
Last active January 15, 2026 08:53
Show Gist options
  • Select an option

  • Save RichardD2/bffa095dbff1c35b35dcdd47a59c6f7e to your computer and use it in GitHub Desktop.

Select an option

Save RichardD2/bffa095dbff1c35b35dcdd47a59c6f7e to your computer and use it in GitHub Desktop.
ASP.NET unit test class to resolve matching endpoints
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