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.