Sending WebHooks with ASP.NET WebHooks Preview

In the blog Introducing Microsoft ASP.NET WebHooks Preview, we gave an overview of how to work with Microsoft ASP.NET WebHooks. We mentioned that it is not only possible to receive WebHooks from others but also to add support for sending WebHooks from your Web Application. This blog goes into detail on how to do that.

Sending WebHooks is slightly more involved in that there are more things to keep track of. In order to support other APIs registering for WebHooks from your ASP.NET application, we need to provide support for:

  • Exposing which events subscribers can subscribe to, for example Item Created and Item Deleted;
  • Managing subscribers and their registered WebHooks which includes persisting them so that they don’t disappear;
  • Handling per-user events in the system and determine which WebHooks should get fired so that WebHooks go to the correct receivers. For example, if user A caused an Item Created event to fire then determine which WebHooks registered by user A should be sent. We don’t want events for user A to be sent to user B
  • Sending WebHooks to receivers with matching WebHook registrations.

Microsoft ASP.NET WebHooks help you throughout this process making it simpler to support WebHooks on your own:

  • It provides support for managing which events users can subscribe to and allowing these to be exposed to clients;
  • It provides a mechanism for managing registered WebHooks. This can be done using an API controller for registering/unregistering WebHooks using REST or an MVC controller for a UI oriented approach. In addition, it provides a pluggable model for persisting WebHook registrations and out-of-box support for storing WebHook registrations in Azure Table Storage;
  • It provides a mechanism for finding matching WebHook registrations and for determining which WebHooks to send as a result;
  • It supports sending WebHooks handling errors and retrying requests for a number of times before giving up.

Note: We are adding topics to the Microsoft ASP.NET WebHooks online documentation so please also check there for additional details as they become available.

Creating a Sample Web Application

First we need an ASP.NET Web Application which has some form of authentication enabled so that we know who the client user is. The reason for this is that we want to only have WebHooks go to the users who registered for them. In this example we create up an MVC + Web API project using Individual User Accounts as follows:

WebHookSenderProject

Btw, you can also look at the sample doing exactly this.

Adding the Nugets

The support for sending WebHooks is provided by the following Nuget packages:

In this example we will use all four packages for providing a basic WebHooks implementation which stores WebHook registrations in Azure Table Storage and exposes a REST API for subscribing to WebHooks. However, ASP.NET WebHooks is built around Dependency Injection and so most parts of the system are pluggable allowing you to provide alternative implementations as needed.

After adding the four packages to your project, initialize them by adding the following three lines to WebApiConfig.Register:

public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services

// Web API routes
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

// Load Web API controllers and Azure Storage store
config.InitializeCustomWebHooks();
config.InitializeCustomWebHooksAzureStorage();
config.InitializeCustomWebHooksApis();
}
}

Defining Event Filters

Now let’s look at defining the set of event filters that consumers of your WebHooks can use when subscribing for event notifications. The filters indicate which events the consumer is interested in. By default, the system registers a wildcard filter which allows users to register for all events. Event filters are registered by providing one or more implementations of the IWebHookFilterProvider interface which simply exposes GetFiltersAsync. Add a class called MyFilterProvider like below defining two events (event1 and event2):

/// <summary>
/// Use a IWebHookFilterProvider implementation to describe the events that users can
/// subscribe to. A wildcard is always registered meaning that users can register for
/// "all events". It is possible to have 0, 1, or more IWebHookFilterProvider
/// implementations.
/// </summary>
public class MyFilterProvider : IWebHookFilterProvider
{
private readonly Collection<WebHookFilter> filters = new Collection<WebHookFilter>
{
new WebHookFilter { Name = "event1", Description = "This event happened."},
new WebHookFilter { Name = "event2", Description = "This event happened."},
};

public Task<Collection<WebHookFilter>> GetFiltersAsync()
{
return Task.FromResult(this.filters);
}
}

When ASP.NET WebHooks starts up, it looks for all IWebHookFilterProvider implementations and gathers the combined set of event filters. The filters are made available so that they can be incorporated into an MVC UI or exposed through the REST-style API for managing registrations.

Managing WebHook Subscriptions

WebHook subscriptions are managed through an interface called IWebHookStore which provide an abstraction for querying, inserting, updating, and deleting subscriptions. The default IWebHookStore implementation is in-memory-only, but for more realistic scenarios, you can use the Azure Table Storage implementation, or write your own. Just like the store can be backed by any number of implementations, the store can be accessed in a number of ways:

SubscriptionStore

That is, you can for example write an MVC controller which exposes subscription management, or you can use the provided Web API implementation which exposes it using a REST-style interface. In this example we will go with the Web API and store the data in Azure Table Storage, the but you can provide the exact experience you want.

We just use the local Development Storage so set the MS_AzureStoreConnectionString connection string as follows:

<connectionStrings>
<add name="MS_AzureStoreConnectionString" connectionString="UseDevelopmentStorage=true;"/>
</connectionStrings>

Sending Notifications

Let’s generate an event! Events are matched against the registered WebHooks and if matches are found then an event notification is fired in the form of a WebHook. Generating WebHook notifications typically happens from within an MVC controller or a Web API controller but can actually happen from anywhere. In the following we will focus on sending them from an MVC controller and a Web API controller.

Add an empty MVC Controller called NotifyController with a Submit action that generates an event using the NotifyAsync method:

[Authorize]
public class NotifyController : Controller
{
[HttpPost]
public async Task<ActionResult> Submit()
{
// Create an event with action 'event1' and additional data
await this.NotifyAsync("event1", new { P1 = "p1" });

return new EmptyResult();
}
}

Note that we only want WebHooks to be sent to the current user. That is, we don’t want user A’s events going to user B and vice versa. This means that to generate an event we must have a valid user ID. In the case of the controllers we do this by ensuring that the user is authenticated using the [Authorize] attribute.

The input data allows the submitter to include additional data in the WebHook which is then sent to matching receivers. This can be anything that the notification needs to convey data about the event.

Similarly, add an empty Web API Controller called NotifyApiController with a Post action that generates an event using the NotifyAsync method:

[Authorize]
public class NotifyApiController : ApiController
{
public async Task Post()
{
// Create an event with 'event2' and additional data
await this.NotifyAsync("event2", new { P1 = "p1" });
}
}

The use of actions and data is identical to that of MVC controllers and again, we require authentication to get a valid user.

Trying it Out

That’s all the configuration we need. However, in order to try it out we need to set up a test client that can subscribe and receive WebHooks. For this scenario we just add the test client straight to the current Web Application project. In a more realistic scenario, it would of course be a separate project, but this suffices to show the point. The flow we are going for is as follows:

WebHookFlow

To set up the test client, add the Microsoft.AspNet.WebHooks.Receivers.Custom to your Web Application project and add this to your WebApiConfig.Register method:

public static void Register(HttpConfiguration config)
{
// Web API configuration and services

// Web API routes
config.MapHttpAttributeRoutes();

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

// Load Web API controllers and Azure Storage store
config.InitializeCustomWebHooks();
config.InitializeCustomWebHooksAzureStorage();
config.InitializeCustomWebHooksApis();
config.InitializeReceiveCustomWebHooks();
}
 
Configure the receiver with the following Application Setting added to Web.Config:
 
<appSettings>
<add key="MS_WebHookReceiverSecret_Custom" value="12345678901234567890123456789012" />
</appSettings>

Next, we add a TestHandler class to process incoming WebHooks like this:
 
public class TestHandler : WebHookHandler
{
public override Task ExecuteAsync(string receiver, WebHookHandlerContext context)
{
return Task.FromResult(true);
}
}

To simulate subscribing and triggering events, we mock up a simple UI in the well-known ASP.NET home page. Open the file Views/Home/Index.cshtml and add the following content right under the jumbotron div element. In this scenario we subscribe to all events by not including any filters in the subscribe request.
 
Note: Make sure the WebHookURI property points to your actual server by setting the HTTP port to the correct value, for example http://localhost:59927/api/webhooks/incoming/custom:
 
<form onsubmit="return subscribe()">
Subscribe to all events <input type="submit" value="submit">
</form>
<form onsubmit="return unsubscribe()">
Unsubscribe from all events <input type="submit" value="submit">
</form>
<form onsubmit="return notifymvc()">
Trigger notification through MVC controller <input type="submit" value="submit">
</form>
<form onsubmit="return notifyapi()">
Trigger notification through Web API controller <input type="submit" value="submit">
</form>

<script>
function subscribe() {
$.ajax({
type: "POST",
url: "/api/webhooks/registrations",
data: JSON.stringify({
WebHookUri: "http://localhost:59927/api/webhooks/incoming/custom",
Secret: "12345678901234567890123456789012",
Description: "My first WebHook!"
}),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(data, status) { alert(status); },
failure: function(errMsg) { alert(errMsg); }
});
return false;
}

function unsubscribe() {
$.ajax({
url: "/api/webhooks/registrations",
type: 'DELETE',
success: function (data, status) { alert(status); },
failure: function(errMsg) { alert(errMsg); }
});
return false;
}

function notifymvc() {
$.post("/notify/submit",
{ },
function (data, status) { alert("Data: " + data + "\nStatus: " + status); });
return false;
}

function notifyapi() {
$.post("/api/notifyapi",
{ },
function (data, status) { alert("Data: " + data + "\nStatus: " + status); });
return false;
}
</script>

That should give you four very fancy buttons on the home page looking like this:

SenderTestPage

First you have to authenticate by logging in using the login flow in the top right corner. Just create a new user/password – the details don’t matter:

SenderUser

When you hit Subscribe, an entry should be stored in Azure Table Storage. You can inspect that this happened by looking at the store in Visual Studio (note that the content is encrypted with a roll-over key so that none of the client secrets are visible).

Subscription

Now hit either of the Trigger buttons on the home page which will fire off events. As a result, you should be able to see this in your handler like this:

HandlerDebug

That’s it – you now have full support for sending WebHooks!

Have fun!

Henrik