Under the hood of ASP.NET Core WebHooks - Routing

This is a second post in my series about ASP.NET Core WebHooks:

As promised it will be focused on machinery which makes it possible for a WebHook request to find matching action.

Basics of WebHooks routing

You may know from the previous post, that key component responsible for configuring routing is WebHookSelectorModelProvider. Its job is to iterate all discovered actions and for those decorated with WebHookAttribute check for conflicts, set attribute routing template and inject action constraints. The most important thing is the template. It has following value (currently there is no built-in customization mechanism available): /api/webhooks/incoming/{webHookReceiver}/{id?}. Let's split it up:

  • /api/webhooks/incoming - static part which is the same for all WebHooks receivers. It might be best to consider all paths starting with it as reserved.
  • {webHookReceiver} - route parameter which must provide the intended receiver name.
  • {id?} - route parameter which may provide unique identifier.

The fact, that all actions decorated with WebHookAttribute share the route template means that when WebHooks request comes in all registered WebHooks actions are possible candidates. How specific action is being chosen? As stated above the WebHookSelectorModelProvider injects actions constraints. The one constraint which is always injected is WebHookReceiverNameConstraint. It validates two things. First is check for receiver "completeness" (does it metadata implement IWebHookBodyTypeMetadataService) and second is comparison between webHookRecevier route parameter and receiver name from metadata. Only if both conditions are met the action will be selected.

More about the {id?}

The purpose of second parameter defined by route is to provide a way for WebHooks URLs to be unique and give them possibility of being capability URLs. Imagine a situation where your application gives users option to receive WebHooks from GitHub. In order to distinguish requests for different users it is best to have unique URLs, so the application can generate them like this: https://{host}/api/webhooks/incoming/github/user1, https://{host}/api/webhooks/incoming/github/user2. In result the action will be able to access the unique identifier through id parameter.

public class GitHubController : ControllerBase
{
    // /api/webhooks/incoming/github/{id}
    [GitHubWebHook]
    public IActionResult GitHubHandler(string id, ...)
    {
        ...
    }
}

That's not all. The unique identifier can also participate in action selection. The WebHookAttribute provides an Id property. If that property is set for specific action the WebHookSelectorModelProvider will inject WebHookIdConstraint. This constraint makes sure that value of id route parameter matches the value provided by property. Going back to the GitHub example, if the application knows that it will need to handle a specific WebHook (for example its own repository), it can create a dedicated action.

public class GitHubController : ControllerBase
{
    // /api/webhooks/incoming/github/tpeczek
    [GitHubWebHook(Id = "tpeczek")]
    public IActionResult GitHubHandlerForTPeczek(...)
    {
        ...
    }

    // /api/webhooks/incoming/github/{id}
    [GitHubWebHook]
    public IActionResult GitHubHandler(string id, ...)
    {
        ...
    }
}

Adding events to the mix

There is one more WebHooks concept which relates to routing - events. Some of the WebHooks providers use events to share information about action which has triggered the WebHook and ASP.NET Core WebHooks provides a set of building blocks for utilizing them. Adding support for event to WebHooks receiver starts by implementing IWebHookEventMetadata as part of metadata. The interface provides properties for defining source of events information. Currently one can provide a query parameter or HTTP header name. The presence of IWebHookEventMetadata will cause WebHookSelectorModelProvider to inject yet another constraint. The WebHookEventNameMapperConstraint constraint will retrieve value from request based on IWebHookEventMetadata and treat it as comma separated list of events. The list must contain at least one entry (unless default event have been specified through IWebHookEventMetadata.ConstantValue property) for constraint to accept the request. Additionally the events are being added to route values, which makes them accessible as action parameter (if multiple events are expected an array should be used as parameter type).

public class GitHubController : ControllerBase
{
    // /api/webhooks/incoming/github/tpeczek
    [GitHubWebHook(Id = "tpeczek")]
    public IActionResult GitHubHandlerForTPeczek(string @event, ...)
    {
        ...
    }

    // /api/webhooks/incoming/github/{id}
    [GitHubWebHook]
    public IActionResult GitHubHandler(string id, string @event, ...)
    {
        ...
    }
}

Events, similarly to unique identifier, can participate in action selection. To enable this the receivers WebHookAttribute must implement IWebHookEventSelectorMetadata, which will provide EventName property. As you probably already expect the WebHookSelectorModelProvider is just waiting to inject related constraint. In this case it's WebHookEventNameConstraint which validates EventName property value against previously parsed list of events, if event is present the action is selected.

public class GitHubController : ControllerBase
{
    // /api/webhooks/incoming/github/tpeczek [X-GitHub-Event --> push]
    [GitHubWebHook(Id = "tpeczek", EventName = "push")]
    public IActionResult GitHubHandlerForTPeczekPushEvent(...)
    {
        ...
    }

    // /api/webhooks/incoming/github/tpeczek
    [GitHubWebHook(Id = "tpeczek")]
    public IActionResult GitHubHandlerForTPeczek(string @event, ...)
    {
        ...
    }

    // /api/webhooks/incoming/github/{id} [X-GitHub-Event --> push]
    [GitHubWebHook(EventName = "push")]
    public IActionResult GitHubHandlerForPushEvent(string id, ...)
    {
        ...
    }

    // /api/webhooks/incoming/github/{id}
    [GitHubWebHook]
    public IActionResult GitHubHandler(string id, string @event, ...)
    {
        ...
    }
}

This covers the most important information regarding ASP.NET Core WebHooks routing. Of course I haven't described everything (for example special support for Ping event). In next post I'm planning to take a look at verification requests.