De routering van Azure Function wijkt af van de routering van een traditionele ASP.Net applicatie. Neem de volgende endpoints:
/api/users/{userId}
/api/users/current
Je zou terecht verwachten dat een request voor /api/users/current naar het tweede endpoint wordt gestuurd en een request voor /api/users/1 naar het eerste endpoint. De praktijk is dat je dat niet met zekerheid kunt zeggen. Ik gebruik Azure Functions App ook als API en zijn vaak tegen onverwachte resultaten aangelopen.
Ik kwam dit artikel tegen van Brian Dunnington. Hij beschrijft daarin hoe de routering werkt en zijn onderzoek naar mogelijkheden om de routering aan te passen. Ik zal niet herhalen hoe zijn onderzoek is verlopen, maar hij heeft uiteindelijk wel een oplossing gevonden. Door gebruik te maken van een WebJobsBuilderExtensions heeft hij een manier gevonden om tijdens het starten van de applicatie in te haken op de routing collection. Het resultaat is een startup class die je op kunt nemen in de root van je Function App. Deze kan gewoon naast de functions startup class worden opgenomen, als deze aanwezig is.
using Microsoft.AspNetCore.Routing;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host.Config;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Extensions.Hosting;
using System.Reflection;
[assembly: WebJobsStartup(typeof(ReorderRoutingStartUp))]
namespace Example;
public class ReorderRoutingStartUp : IWebJobsStartup
{
public void Configure(IWebJobsBuilder builder)
{
builder.AddRoutePriority();
}
}
public static class WebJobsBuilderExtensions
{
public static IWebJobsBuilder AddRoutePriority(this IWebJobsBuilder builder)
{
builder.AddExtension<RoutePriorityExtensionConfigProvider>();
return builder;
}
}
public static class IWebJobsRouterExtensions
{
public static List<Route> GetRoutes(this IWebJobsRouter router)
{
var type = typeof(WebJobsRouter);
var fields = type.GetRuntimeFields();
var field = fields.FirstOrDefault(f => f.Name == "_functionRoutes");
var functionRoutes = field.GetValue(router);
var routeCollection = (RouteCollection)functionRoutes;
var routes = GetRoutes(routeCollection);
return routes;
}
static List<Route> GetRoutes(RouteCollection collection)
{
var routes = new List<Route>();
for (var i = 0; i < collection.Count; i++)
{
var nestedCollection = collection[i] as RouteCollection;
if (nestedCollection != null)
{
routes.AddRange(GetRoutes(nestedCollection));
continue;
}
routes.Add((Route)collection[i]);
}
return routes;
}
}
public class RoutePriorityExtensionConfigProvider : IExtensionConfigProvider
{
IHostApplicationLifetime applicationLifetime;
IWebJobsRouter router;
public RoutePriorityExtensionConfigProvider(IHostApplicationLifetime applicationLifetime, IWebJobsRouter router)
{
this.applicationLifetime = applicationLifetime;
this.router = router;
this.applicationLifetime.ApplicationStarted.Register(() =>
{
ReorderRoutes();
});
}
public void Initialize(ExtensionConfigContext context)
{
}
public void ReorderRoutes()
{
var unorderedRoutes = router.GetRoutes();
var routePrecedence = Comparer<Route>.Create(RouteComparison);
var orderedRoutes = unorderedRoutes.OrderBy(id => id, routePrecedence);
var orderedCollection = new RouteCollection();
foreach (var route in orderedRoutes)
{
orderedCollection.Add(route);
}
router.ClearRoutes();
router.AddFunctionRoutes(orderedCollection, null);
}
static int RouteComparison(Route x, Route y)
{
var xTemplate = x.ParsedTemplate;
var yTemplate = y.ParsedTemplate;
for (var i = 0; i < xTemplate.Segments.Count; i++)
{
if (yTemplate.Segments.Count <= i)
{
return -1;
}
var xSegment = xTemplate.Segments[i].Parts[0];
var ySegment = yTemplate.Segments[i].Parts[0];
if (!xSegment.IsParameter && ySegment.IsParameter)
{
return -1;
}
if (xSegment.IsParameter && !ySegment.IsParameter)
{
return 1;
}
if (xSegment.IsParameter)
{
if (xSegment.InlineConstraints.Count() > ySegment.InlineConstraints.Count())
{
return -1;
}
else if (xSegment.InlineConstraints.Count() < ySegment.InlineConstraints.Count())
{
return 1;
}
}
else
{
var comparison = string.Compare(xSegment.Text, ySegment.Text, StringComparison.OrdinalIgnoreCase);
if (comparison != 0)
{
return comparison;
}
}
}
if (yTemplate.Segments.Count > xTemplate.Segments.Count)
{
return 1;
}
return 0;
}
}
Het resultaat komt aardig in de buurt van de routering van een traditionele ASP.Net applicatie, maar er zijn een paar verschillen. Voor mij geen probleem, dus een prima oplossing!