JonKrupa-5607 avatar image
0 Votes"
JonKrupa-5607 asked PramodValavala-MSFT commented

Different routing behavior between Web API and Azure Function HTTP Trigger Causing Problems

There seems to be a major difference in the behavior of route templates in Azure Functions HTTP triggers vs. Web APIs. I have a Web API project (.NET 5) that has a number of controllers. I'm being tasked to divide it out into microservices using Azure Functions instead of an app service. There are three endpoints in one controller:

 IActionResult GetByCounty(string country, string stateProvince, string countyRegion)
 IActionResult GetByState(string country, string stateProvince)
 IActionResult GetByPostalCode(string country, string postalCode)

This is all well and good where everything works as expected. In the function app, I defined the three HTTP triggers:

 HttpResponseData GetByCounty([HttpTrigger(Route = "history/{country}/{stateProvince}/{countyRegion}")]
 HttpResponseData GetByPostalCode([HttpTrigger(Route = "history/postalcodes/{country}/{postalCode}")]
 HttpResponseData GetByState([HttpTrigger(Route = "history/{country}/{stateProvince}")]

GetByState and GetByCounty work fine. However, when trying to get by postal code in the function app, the function GetByCounty is being executed. The Web API app is honoring the static text "postalcodes" in the route. The functions app is not. It sees four levels in the path and picks the first one alphabetically which happens to be GetByCounty.

Other than adding an unnecessary path to get the number of levels to be unique, is there a way to make this work so the correct HTTP trigger is executed mimicking the behavior of standard Web APIs running in an app service?

  • Changing GetByPostalCode to "hickory/postalcodes/{country}/{postalcode} works

  • Changing GetByPostalCode to "postalcodes/history/{country}/{postalcode} works

  • Adding an additional path level "history/postalcodes/countries/{country}/{postalcode} works

But this breaks the API in terms of compatibility which isn't a good thing.

5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

PramodValavala-MSFT avatar image
0 Votes"
PramodValavala-MSFT answered PramodValavala-MSFT commented

@JonKrupa-5607 The default order is the order in which functions are registered, which is done by enumerating directories containing a function.json file making it lexicographically sorting all functions. A dirty fix seems to be adding prefixes to function names like below

public class RouteSpecificity
    [FunctionName("P00_" + nameof(GetDemoProduct))]
    public IActionResult GetDemoProduct(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "test/demo/{prod}")] HttpRequest req,
        ILogger log
    ) =>  new OkObjectResult("Getting Demo Product");

    [FunctionName("P01_" + nameof(GetProduct))]
    public IActionResult GetProduct(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "test/{cat}/{prod}")] HttpRequest req,
        ILogger log
    ) =>  new OkObjectResult("Getting Product");

There is an open issue that tracks this and has a mention of a community solution to tackle this.

· 2
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

I took a look at the issue you referenced. I think my situation and issue 3153, while similar, are very different. What my mind is stuck on is the routing scheme works perfectly fine in Web API but fails in Azure Functions. I tried updating to .NET 6 and it's the same behavior in .NET 6. The real answer may be that Azure Functions is NOT a replacement for web API. Developers, like all humans, get "Shiny Object Syndrome." A phenomenon where people flock to new and flashy things for the simple reason they're new and flashy and not because it's the right tool for the job. If Azure Functions is not a drop-in replacement, maybe it is not intended to be as sophisticated or as expansive in the problems where the technology is appropriate?

That all being said, your suggestion with the prefix works. I find it a bit odd as I was expecting the problem to simply be inverted where the county endpoint would end up being ignored but that's not the case. I don't know the intended behavior or specs behind the Azure Functions HTTP trigger routing engine but I do think there is room for improvement. The routing engine isn't sophisticated enough especially when compared to web API. In my case, there's enough uniqueness to the route where the routing engine should be able to figure it out but doesn't. As I pointed out in my original post, the Azure Functions router isn't smart enough to distinguish that /api/foo/{item} and /api/bar/{item} are different routes because it's not honoring the static parts of the route.

0 Votes 0 ·

@JonKrupa-5607 Yes. While the scenario described in the issue is not exactly the same as yours, I believe the fix for it should resolve both. AFAIK Azure Functions do not use the same routing mechanism since the functions are loaded and invoked dynamically as part of a function invocation instead of loading them as routes.

That being said, Azure Functions' main use case is the ability to run workloads with elastic scale and near zero cost when there is no activity but does have some limitations. You can however workaround some of these limitations by either using C# Isolated which gives you more control of the WebHost or Azure Container Apps (currently in preview) which let you run your existing applications in a serverless environment.

0 Votes 0 ·
vb2ae avatar image
0 Votes"
vb2ae answered JonKrupa-5607 commented

Not that this is a great solution but you could always check if the country is postalcodes and return the results from GetByPostalCode instead of GetByCountry because they both return an HttpResponseData

· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Agreed not an ideal solution but it's easy and clean. I use the comment technique //HACK: I know this is bad but... in this situation. Thanks for the idea. I may actually do this.

0 Votes 0 ·