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:
Adding the Nugets
The support for sending WebHooks is provided by the following Nuget packages:
- Microsoft.AspNet.WebHooks.Custom: This package provides the core functionality for adding WebHook support to your ASP.NET project. The functionality enables users to register WebHooks using a simple pub/sub model and for your code to send WebHooks to receivers with matching WebHook registrations.
- Microsoft.AspNet.WebHooks.Custom.AzureStorageThis package provides optional support for persisting WebHook registrations in Microsoft Azure Table Storage.
- Microsoft.AspNet.WebHooks.Custom.MvcThis package exposes optional helpers for accessing WebHooks functionality from within ASP.NET MVC Controllers. The helpers assist in providing WebHook registration through MVC controllers as well as creating event notification to be sent to WebHook registrants.
- Microsoft.AspNet.WebHooks.Custom.ApiThis package contains an optional set of ASP.NET Web API Controllers for managing filters and registrations through a REST-style interface.
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 awildcard 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>
publicclass MyFilterProvider : IWebHookFilterProvider
{
privatereadonly 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:
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>
<addname="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]
publicclass NotifyController : Controller
{
[HttpPost]
public async Task<ActionResult> Submit()
{
// Create an event with action 'event1' and additional data
await this.NotifyAsync("event1", new { P1 = "p1" });
returnnew 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]
publicclass 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:
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.InitializeCustomWebHooks();
}
<appSettings>
<addkey="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);
}
}
<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); }
});
returnfalse;
}
function unsubscribe() {
$.ajax({
url: "/api/webhooks/registrations",
type: 'DELETE',
success: function (data, status) { alert(status); },
failure: function(errMsg) { alert(errMsg); }
});
returnfalse;
}
function notifymvc() {
$.post("/notify/submit",
{ },
function (data, status) { alert("Data: " + data + "\nStatus: " + status); });
returnfalse;
}
function notifyapi() {
$.post("/api/notifyapi",
{ },
function (data, status) { alert("Data: " + data + "\nStatus: " + status); });
returnfalse;
}
</script>
That should give you four very fancy buttons on the home page looking like this:
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:
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).
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:
That’s it – you now have full support for sending WebHooks!
Have fun!
Henrik