asp.netvb.netasp.net-web-api

Routing in ASP.NET Web API


I'm trying to add a new Web API in a legacy web with VB.NET and .NET Framework 4.8. It has multiple Web API running correctly.

For example - CalendarController.vb:

<RoutePrefix("api/{Controller}")>
Public Class CalendarController
    Inherits ApiController

    <HttpGet>
    <Route("{id:int}")>
    Public Function GetCalendarEvent(id As Integer) As IHttpActionResult
        ...
        Return Ok(...)
    End Function
End Class

StockController.vb:

<RoutePrefix("api/{Controller}")>
Public Class StockController
    Inherits ApiController

    <HttpGet>
    <Route("{friendlyURL}")>
    Public Function GetProductStock(friendlyURL As String) As IHttpActionResult
        ...
        Return Ok(...)
    End Function
End Class

Global.asax.vb:

Sub Application_Start(sender As Object, e As EventArgs)
    With RouteTable.Routes
        .MapHttpRoute(name:="Calendar.GetNextCalendarEvents", routeTemplate:="api/{Controller}/Next/{maxEvents}")
        .MapHttpRoute(name:="Calendar.GetMonthEntries", routeTemplate:="api/{Controller}/{month}/{year}")
        .MapHttpRoute(name:="Calendar.GetCalendarEvent", routeTemplate:="api/{Controller}/{id}")
        .MapHttpRoute(name:="Map.GetMarkerById", routeTemplate:="api/{Controller}/{markerId:int}")
        .MapHttpRoute(name:="Map.GetAllMarkers", routeTemplate:="api/{Controller}")
        .MapHttpRoute(name:="Stock.GetProductStock", routeTemplate:="api/{Controller}/{friendlyURL}")
        '.MapHttpRoute(name:="Stock.GetProductStock", routeTemplate:="api/{Controller}/{friendlyURL}", defaults:=Nothing, constraints:=New With {.friendlyURL = "^[a-zA-Z]+[a-zA-Z0-9,_ -]*$"})
    End With
End Sub

Testing both APIs:

const response = await fetch(`/api/Calendar/3050`);
const event = await response.json();
console.log(event);
//Returns 200 with an object correctly
{ id: 3050, title: '...', ... }

const response = await fetch(`/api/Stock/my-product`);
const product = await response.json();
console.log(product);

//Cannot access the API
GET https://localhost:44330/api/Stock/my-product 404 (Not Found)

{ Message: "No HTTP resource was found that matches the request  'https://localhost:44330/api/Stock/my-product'.", 
  MessageDetail: "No action was found on the controller 'Stock' that matches the request." }

const response = await fetch(`/api/Stock?friendlyURL=my-product`);
//Returns 200 Http Code and correct stock.

I cannot find out the differences.

EDIT: It runs when parameter is passed as query string

Thank you!


Solution

  • I believe the request isn't getting to correct route registration. In the code below is a comment abiv ethe line I suspect is trying to route the request.

    Sub Application_Start(sender As Object, e As EventArgs)
        With RouteTable.Routes
            .MapHttpRoute(name:="Calendar.GetNextCalendarEvents", routeTemplate:="api/{Controller}/Next/{maxEvents}")
            .MapHttpRoute(name:="Calendar.GetMonthEntries", routeTemplate:="api/{Controller}/{month}/{year}")
            ' I'd guess this registration is trying to route the request
            .MapHttpRoute(name:="Calendar.GetCalendarEvent", routeTemplate:="api/{Controller}/{id}")
            .MapHttpRoute(name:="Map.GetMarkerById", routeTemplate:="api/{Controller}/{markerId:int}")
            .MapHttpRoute(name:="Map.GetAllMarkers", routeTemplate:="api/{Controller}")
            .MapHttpRoute(name:="Stock.GetProductStock", routeTemplate:="api/{Controller}/{friendlyURL}")
            '.MapHttpRoute(name:="Stock.GetProductStock", routeTemplate:="api/{Controller}/{friendlyURL}", defaults:=Nothing, constraints:=New With {.friendlyURL = "^[a-zA-Z]+[a-zA-Z0-9,_ -]*$"})
        End With
    End Sub
    

    The way to make it work is to order route registrations from the most specific to most general. I would personally leverage of default values and explicit route values instead of using {Controller} replacement for specific route templates.

    Sub Application_Start(sender As Object, e As EventArgs)
        With RouteTable.Routes
            .MapHttpRoute(name:="Calendar.GetNextCalendarEvents", routeTemplate:="api/Calendar/Next/{maxEvents}", defaults:=New With {.controller = "Calendar", .action = "GetNextCalendarEvents")
            .MapHttpRoute(name:="Calendar.GetMonthEntries", routeTemplate:="api/Calendar/{month}/{year}", defaults:=New With {.controller = "Calendar", .action = "GetMonthEntries"))
            .MapHttpRoute(name:="Map.GetAllMarkers", routeTemplate:="api/Map", defaults:=New With {.controller = "Map", .action = "GetAllMarkers"))
            .MapHttpRoute(name:="Map.GetMarkerById", routeTemplate:="api/Map/{markerId:int}", defaults:=New With {.controller = "Map", .action = "GetMarkerById"))
            .MapHttpRoute(name:="Stock.GetProductStock", routeTemplate:="api/Stock/{friendlyURL}", defaults:=New With {.controller = "Stock", .action = "GetProductStock"))
            ' Here I see eventual problem when a controller will have two actions with parameter id. It will throw an exception as it wouldn't know which one to choose
            .MapHttpRoute(name:="Default", routeTemplate:="api/{Controller}/{id}")
        End With
    End Sub
    

    I think this will do the trick. I would consider reviewing ASP.NET MVC Routing Overview (VB) and maybe adding default route from the overview.