Under the hood of ASP.NET Core WebHooks - Introduction

I was thinking about digging into ASP.NET Core WebHooks for quite some time, but I had other things to do and I was telling myself it will be best to do it when it reaches RTM. Those other things are done now, but ASP.NET Core WebHooks has been moved out of ASP.NET Core 2.1. I could either keep waiting for RTM and postpone further or admit that it was only excuse and go for it. After briefly looking at code one more time I've decided I'm curious about the internals and I want to do it, especially that I had a project idea which will be a good context for it - implementing a WebSub compliant subscriber. WebSub is a specification of common mechanism for communication between publishers and subscribers of Web content. The content distribution requires subscriber to expose a web hook. This web hook is very specific so I'm not sure if I will be able to deliver it, but it certainly will be interesting.

With this post I'm aiming at starting a series which for me will be a log of what I have learned and for you (hopefully) a source of knowledge. I don't have full roadmap for this series. I'm planning on touching following subjects:

I'll keep this list updated so it could serve as TOC. Please also remember that I'm starting writing this based on 1.0.0-preview3-final version so things might change.

Let's start with general overview.

What happens when you "Add" WebHooks

An adventure with every WebHook starts with AddXXXWebHooks() extension method. In fact there are always two methods. One is extending IMvcCoreBuilder while the other is extending IMvcBuilder. The reason is that WebHooks doesn't require "full" ASP.NET Core MVC. If we are not setting them together with MVC based frontend or Web API following will be enough for working application.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvcCore()
            .AddWebSubWebHooks();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseMvc();
    }
}

Are both method the same? Usually not. The method which extends IMvcCoreBuilder must register required formatters even if it's JSON. Skipping this difference, the flow is the same.

public static IMvcCoreBuilder AddWebSubWebHooks(this IMvcCoreBuilder builder)
{
    if (builder == null)
    {
        throw new ArgumentNullException(nameof(builder));
    }

    WebHookMetadata.Register<WebSubMetadata>(builder.Services);

    // Registration of required formatters

    return builder.AddWebHooks();
}

Two things are happening here: metadata describing the receiver are being registered and the core part of WebHooks is being added. What's happening there?

The heart of ASP.NET Core WebHooks - Application Models

The components of an ASP.NET Core MVC application are represented by application model. Most of the time developers don't have to be aware of it. The default implementation registers filters, discovers actions, configures routing and provides conventions which are used to interact with it. But for special cases there is an option for modifying application model by providing implementations of IApplicationModelProvider. This is how WebHooks integrate themselves with routing and create dedicated filters pipeline. As part of AddWebHooks() four implementations are being registered (listed in order of execution):

  1. WebHookActionModelPropertyProvider - responsible for adding receiver metadata to ActionModel.Properties of related actions (later implementations depend on it).
  2. WebHookSelectorModelProvider - responsible for adding routing template and constraints.
  3. WebHookActionModelFilterProvider - responsible for adding common and receiver specific filters to related actions.
  4. WebHookBindingInfoProvider - responsible for adding model binding information.

The future posts in this series will look closer at some of them.

Describing a receiver with metadata

The subject of receiver metadata will be probably coming back in every post in this series. Most of the aspects of receiver behaviour are described by one of interfaces which can be used to compose metadata (those interfaces can be found in Microsoft.AspNetCore.WebHooks.Metadata namespace). The bare minimum which every metadata needs to implement is IWebHookBodyTypeMetadataService. This is enforced by WebHookMetadata base class which also takes care of most of the implementation.

public class WebSubMetadata : WebHookMetadata
{
    public WebSubMetadata()
        : base(WebSubConstants.ReceiverName)
    { }

    public override WebHookBodyType BodyType => WebHookBodyType.Xml;
}

This makes two information obligatory. First is receiver name and second its request body type. The body type is potentially problematic one. It can have one of three values: Form, Json or Xml. The WebHookActionModelPropertyProvider validates if one of those values is set before adding metadata to action and later WebHookVerifyBodyTypeFilter (added by WebHookActionModelFilterProvider) validates if Content-Type of incoming request is in line with the provided value. For WebSub the web hook should support multiple body types so this will be one of challenging parts.

There is one more requirement for the metadata, enforced by WebHookReceiverExistsFilter. The metadata must implement IWebHookVerifyCodeMetadata or provide filter which implements IWebHookReceiver (the only out-of-the-box implementations is WebHookVerifySignatureFilter). The goal here seems to be security - enforcing some kind of request validation. For cases where such validation is not (yet) needed one can implement IWebHookFilterMetadata for metadata to inject some simple implementation of IWebHookReceiver.

public class WebSubMetadata : WebHookMetadata, IWebHookFilterMetadata
{
    private class WebSubWebHookReceiverFilter : IFilterMetadata, IWebHookReceiver
    {
        public string ReceiverName => WebSubConstants.ReceiverName;

        public bool IsApplicable(string receiverName)
        {
            if (receiverName == null)
            {
                throw new ArgumentNullException(nameof(receiverName));
            }

            return String.Equals(ReceiverName, receiverName, StringComparison.OrdinalIgnoreCase);
        }
    }

    private WebSubWebHookReceiverFilter _webHookReceiverFilter = new WebSubWebHookReceiverFilter();

    ...

    public void AddFilters(WebHookFilterMetadataContext context)
    {
        context.Results.Add(_webHookReceiverFilter);
    }
}

Associating action with WebHooks endpoint

There is one more piece missing - a way to associate an action with WebHooks endpoint. This is achieved by decorating action with attribute derived from WebHookAttribute.

public class WebSubWebHookAttribute : WebHookAttribute
{
    public WebSubWebHookAttribute()
        : base(WebSubConstants.ReceiverName)
    { }
}

The receiver name must match the one provided in metadata.

This is enough to create a "working" receiver. Its capabilities are strongly limited, but it is capable of routing matching requests to an action. What that exactly means will be the focus of next post in this series.