Publishing events into an ASP.NET Core webhook with Octopus

Dominic Burford
7 min readMay 13, 2021

--

I recently worked on a project that required listening to and acting on the events from our deployment software. We use Octopus for deploying our applications. One of the really useful features of Octopus is its ability to create subscriptions.

Subscriptions allow you to subscribe to events that are happening within Octopus, so you can be notified when events have occurred and react accordingly. So for example, you can create a subscription to be notified when a deployment is queued, starts, fails, completes etc. You may bind these subscription notifications into your Slack channel or other communication platform.

Our requirement was to create a custom Octopus subscription webhook using ASP.NET Core, and to send the results of the subscriptions back to our internal Customer Relationship Management (CRM) system. This would allow internal staff to view the status of a deployment without having to require access to Octopus.

The first step was therefore to create the subscription itself in Octopus. This article describes how to setup and configure a subscription within Octopus, so I won’t cover that here (the linked article does an excellent job of that).

Below shows a screenshot of the page for configuring subscriptions within Octopus.

Octopus sends a lot of very useful information. This is sent as a JSON payload via an HTTP POST method.

To inspect the contents and structure of the JSON payload that Octopus sends, you can create a very simple webhook using an excellent online tool called RequestBin. This is really useful when first creating your Octopus subscription, as you can inspect the JSON payload immediately and without having to write any code.

The Octopus subscription payload

Once we were satisfied we could use the contents of the JSON payload to meet the requirements of our application, we firstly mapped the JSON payload into a concrete POCO class as follows.

public class DeploymentWebhookSubscription
{
public DateTime Timestamp { get; set; }
public string EventType { get; set; }
public Payload Payload { get; set; }
}
public class Filter
{
public List<string> Users { get; set; }
public List<string> Projects { get; set; }
public List<string> ProjectGroups { get; set; }
public List<string> Environments { get; set; }
public List<string> EventGroups { get; set; }
public List<string> EventCategories { get; set; }
public List<string> EventAgents { get; set; }
public List<string> Tenants { get; set; }
public List<string> Tags { get; set; }
public List<string> DocumentTypes { get; set; }
}
public class EventNotificationSubscription
{
public Filter Filter { get; set; }
public List<string> EmailTeams { get; set; }
public string EmailFrequencyPeriod { get; set; }
public int EmailPriority { get; set; }
public string EmailShowDatesInTimeZoneId { get; set; }
public string WebhookURI { get; set; }
public List<object> WebhookTeams { get; set; }
public string WebhookTimeout { get; set; }
public string WebhookHeaderKey { get; set; }
public string WebhookHeaderValue { get; set; }
public DateTime WebhookLastProcessed { get; set; }
public int WebhookLastProcessedEventAutoId { get; set; }
}
public class Self
{
}
public class Links
{
public Self Self { get; set; }
}
public class Subscription
{
public string Id { get; set; }
public string Name { get; set; }
public int Type { get; set; }
public bool IsDisabled { get; set; }
public EventNotificationSubscription EventNotificationSubscription { get; set; }
public string SpaceId { get; set; }
public Links Links { get; set; }
}
public class MessageReference
{
public string ReferencedDocumentId { get; set; }
public int StartIndex { get; set; }
public int Length { get; set; }
}
public class LibraryVariableSetSnapshot
{
public string LibraryVariableSetId { get; set; }
public string VariableSetSnapshotId { get; set; }
}
public class SelectedPackage
{
public string ActionName { get; set; }
public string PackageReferenceName { get; set; }
public string Version { get; set; }
}
public class VersionControlReference
{
}
public class DocumentContext
{
public string Id { get; set; }
public string Version { get; set; }
public DateTime Assembled { get; set; }
public List<object> ReleaseDefects { get; set; }
public string ProjectId { get; set; }
public string ProjectVariableSetSnapshotId { get; set; }
public string ProjectDeploymentProcessSnapshotId { get; set; }
public List<LibraryVariableSetSnapshot> LibraryVariableSetSnapshots { get; set; }
public List<SelectedPackage> SelectedPackages { get; set; }
public string ChannelId { get; set; }
public List<object> BuildInformation { get; set; }
public VersionControlReference VersionControlReference { get; set; }
public string SpaceId { get; set; }
}
public class ChangeDetails
{
public DocumentContext DocumentContext { get; set; }
public List<object> Differences { get; set; }
}
public class Event
{
public string Id { get; set; }
public List<string> RelatedDocumentIds { get; set; }
public string Category { get; set; }
public string UserId { get; set; }
public string Username { get; set; }
public bool IsService { get; set; }
public string IdentityEstablishedWith { get; set; }
public string UserAgent { get; set; }
public DateTime Occurred { get; set; }
public string Message { get; set; }
public string MessageHtml { get; set; }
public List<MessageReference> MessageReferences { get; set; }
public ChangeDetails ChangeDetails { get; set; }
public string SpaceId { get; set; }
public Links Links { get; set; }
}
public class Payload
{
public DateTime BatchProcessingDate { get; set; }
public Subscription Subscription { get; set; }
public Event Event { get; set; }
public string BatchId { get; set; }
public int TotalEventsInBatch { get; set; }
public int EventNumberInBatch { get; set; }
}

As can be seen, a lot of information is sent with the JSON payload.

The Filter class contains the filters you configured with your subscription. Within Octopus, there are a huge number of ways for you to configure your subscription. Without any filters, you will literally receive a payload for absolutely every event triggered within Octopus. It it highly unlikely this is what you want.

Another useful class is the Event class. Of particular interest is the property called RelatedDocumentIds.

public class Event
{
public List<string> RelatedDocumentIds { get; set; }
//other properties removed for clarity
}

This is a list of the document IDs that are associated with your deployment. It contains such IDs including

  • Deployment ID
  • Tenant ID
  • Space ID
  • Environment ID
  • Release ID
  • Project ID

Our process saves all these IDs into a SQL table when initially creating the deployment in Octopus (all completely automated via the Octopus API). Our webhook then extracts these values from the subscription JSON payload. The important ID is the Deployment ID. This is how you uniquely identify a deployment and associate it with the subscription notifications received via your webhook.

Adding filters to your Octopus subscription

The screenshot below shows the filters that are available to you to help you narrow your subscription to only those events you are interested in. I would recommend leaving some filters deliberately empty while you configure your subscription (to capture more events) to help you understand what information is being returned, and when. When you are confident you understand this, you can apply more filters so that you are only capturing the events you care about.

Creating the webhook to receive the Octopus subscription

We then created an ASP.NET Core controller to receive the JSON payload. This would be our webhook. With each JSON payload received via the webhook, we would exract the various related document IDs previously described.

Here is a very simple example of a webhook capable to receiving an Octopus subscription. Please note that for clarity I have removed validation, logging, security etc.

[HttpPost]
public ActionResult Post([FromBody] JObject payload)
{
private static readonly int DEPLOYMENT_ID_INDEX = 0;
try
{
var webhookSubscriptionPayload = payload.ToObject<DeploymentWebhookSubscription>();
if (webhookSubscriptionPayload != null)
{
string deploymentId = webhookSubscriptionPayload.Payload.Event.RelatedDocumentIds[DEPLOYMENT_ID_INDEX];
//lookup the deployment ID in your table and do something useful here
return Ok(jobId);
}
return StatusCode(StatusCodes.Status500InternalServerError);
}
catch (Exception e)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
}

N.B. the above code will require you to install the Newtonsoft nuget package

Simple authentication / validation

It is worth noting that it is possible to implement simple authentication on your webhook. You can configure a header key and value that is sent to your webhook with each payload. This can be an encrypted string, a token, an API key or other means of authentication. You can pass it in as a key/value pair as you would in the header of a web request. Your webhook code then validates this and will accept/deny the webhook payload.

[HttpPost]
public ActionResult Post([FromBody] JObject payload)
{
private static readonly int DEPLOYMENT_ID_INDEX = 0;
try
{
var webhookSubscriptionPayload = payload.ToObject<DeploymentWebhookSubscription>();
if (webhookSubscriptionPayload != null)
{
//extract the header key and value from the payload
//and write some custom authentication logic
string headerKey = webhookSubscriptionPayload.Payload.Subscription.EventNotificationSubscription.WebhookHeaderKey;
string headerValue = webhookSubscriptionPayload.Payload.Subscription.EventNotificationSubscription.WebhookHeaderValue; //rest of your code goes here }
catch (Exception e)
{
return StatusCode(StatusCodes.Status500InternalServerError);
}
}

Summary

As you can see, it is relatively straight forward to create a subscription in Octopus and have a custom webhook writtten in ASP.NET Core process the events that are triggered by it. Your webhook can perform whatever actions you need. In our case, we updated our CRM to give visibility of deployments to non-technical users. However, you can process the Octopus subscription in whatever way you want.

I hope this article has given you some ideas on how you can use this powerful and useful feature of Octopus in your own organisation.

--

--

Dominic Burford

A father, cyclist, vegetarian, atheist, geek and multiple award winning technical author. Loves real ale, fine wine and good music. All round decent chap.