Paul Gruffydd
Jun 10, 2021
  2213
(3 votes)

Triggering Webhooks from custom events

In my previous post I introduced the Kin and Carta Webhooks add-on and showed how it can be used to fire webhooks based on one of 5 out-of-the-box content events. But what if you want to trigger a webhook based on something else? While those 5 events cover the more common scenarios, there are plenty of situations which they don’t cover so in this post I’m going to show how you can register your own event types which can be used to set up webhooks in the add-on UI.

The basics

Setting up your own events is pretty straightforward and follows the same process regardless of what triggers your event. The basic steps are:

  1. Create an initializable module with a module dependency of "KinAndCarta.Connect.Webhooks.Setup.InitialiseWebhooks"
  2. Create a new EventType for your event (in the "KinAndCarta.Connect.Webhooks.Models" namespace)
  3. Register your event type by calling the "RegisterEventType" method of IWebhookRepository
  4. Add a translation to give your event a friendly name
  5. When your event occurs, trigger the webhooks using the "TriggerWebhooks" extension method

While the steps above may tell you what you need to do to get up and running with custom events, I always find it easier to look at an example rather than a list and, in this instance, instead of coming out with some examples myself, I thought I’d take inspiration from the comments on my previous post.

Creating a “First Publish” event

In response to the previous article, Stein-Viggo Grenersen mentioned using the add-on to post to social media when new articles are published. That scenario is a great example of how the add-on can be used and it’s really simple to set up using integration platforms like IFTTT, Zapier and Azure Logic Apps but, using the OOTB content publish event, there’s a risk that you’d post to social media every time the content is published in any language, even if you’re just fixing a typo. That could mean that you’re posting the same article again and again. That may work in a demo scenario but, in the real world you probably just want to post it once, the first time you publish or, for multilingual scenarios, maybe once per language. So let’s take a look at how custom events can help.

For this example, let’s create an EventType called “First publish in language” which will only fire the first time an article is published in a given language. An EventType in this context just requires a key (to identify the event) and a flag to indicate whether the event that has occurred impacts the descendants of the content item which triggered the event (for example, the Delete event sets ImpactsDescendants to true as all descendants will be deleted too). To trigger our event, we’ll need to hook in to the publishing and published events from IContentEvents. In the publishing event, we’ll check whether any published versions exist in the current language and pass that information through to the published event using the ContentEventArgs.Items Dictionary which is shared between events triggered by the same action. If no existing published versions exist, we then trigger our webhook from within the “published” event. To trigger the webhooks attached to a content item, we use the aptly named "TriggerWebhooks" extension method (from KinAndCarta.Connect.Webhooks.Extensions) and pass in our event object, the IWebhookRepository instance and any additional data we want to include about our event which, in this case, is null.

The code looks like this…

[InitializableModule]
[ModuleDependency(typeof(KinAndCarta.Connect.Webhooks.Setup.InitialiseWebhooks))]
public class FirstPublishWebhookEventsInitialisation : IInitializableModule
{
    private IWebhookRepository _webhookRepo;
    private IContentLoader _contentLoader;
    private IContentEvents _contentEvents;
    private EventType _customEvent;
    public void Initialize(InitializationEngine context)
    {
        //Get dependencies
        _contentEvents = context.Locate.Advanced.GetInstance<IContentEvents>();
        _webhookRepo = context.Locate.Advanced.GetInstance<IWebhookRepository>();
        _contentLoader = context.Locate.Advanced.GetInstance<IContentLoader>();

        //Create & register our event
        _customEvent = new EventType { ImpactsDescendants = false, Key = "FIRST_PUBLISH_IN_LANGUAGE" };
        _webhookRepo.RegisterEventType(_customEvent);

        //Setup Event handlers
        _contentEvents.PublishingContent += ContentPublishing;
        _contentEvents.PublishedContent += ContentPublished;
    }


    private void ContentPublishing(object sender, ContentEventArgs e)
    {
        var versionRepo = ServiceLocator.Current.GetInstance<IContentVersionRepository>();
        bool firstPublish;

        //Check whether a published version already exists
        //If content can be translated, check if a published version exists in the current language
        if (e.Content is ILocalizable localContent)
        {
            firstPublish = versionRepo.LoadPublished(e.ContentLink.ToReferenceWithoutVersion(), localContent.Language.Name) == null;
        }
        else
        {
            firstPublish = versionRepo.LoadPublished(e.ContentLink.ToReferenceWithoutVersion()) == null;
        }

        //Set a value for the subsequent publish event to check
        e.Items.Add("FirstPublish", firstPublish);
    }
    private void ContentPublished(object sender, ContentEventArgs e)
    {
        //Check for our FirstPublish valie
        if (e.Items["FirstPublish"] as bool? ?? false)
        {
            //TriggerWebhooks extension uses KinAndCarta.Connect.Webhooks.Extensions namespace
            e.Content.TriggerWebhooks(_customEvent, _webhookRepo, null);
        }
    }

    public void Uninitialize(InitializationEngine context)
    {
        _contentEvents.PublishingContent -= ContentPublishing;
        _contentEvents.PublishedContent -= ContentPublished;
    }
}

If we compile and run our code then look at a webhook in the Webhooks UI, here’s what we’ll see.

It’s functional, but not the most user friendly so let’s set up a translation for our event name. Translations of the event names are handled like any other translation in the CMS as you can see in the XML snippet below. 

<?xml version="1.0" encoding="utf-8" ?>
<languages>
  <language name="English" id="en">
    <Webhooks>
      <EventTypes>
        <PUBLISH>Content Published</PUBLISH>
        <MOVING>Content Moving</MOVING>
        <MOVE>Content Moved</MOVE>
        <RECYCLE>Content Sent to Recycle Bin</RECYCLE>
        <DELETE>Content Deleted from Recycle Bin</DELETE>
        <FIRST_PUBLISH_IN_LANGUAGE>First publish in language</FIRST_PUBLISH_IN_LANGUAGE>
      </EventTypes>
    </Webhooks>
  </language>
</languages>

Reloading the screen, we can now see our friendly event name in the list of events and use it to fire a webhook the first time a content item is published in a given language.

Reporting on Scheduled Job execution

In another comment on the original article, Jay Wilkinson suggested using webhooks to act on the outcome of a scheduled job execution, for example, sending an email if a job fails. As you’ve probably realised, we’re not restricted to just firing webhooks on content events. We can trigger webhooks from anywhere in our code, in response to any action, however, as seen in the example above, we need to trigger the webhook using a content item which will be passed in the payload of the webhook. In this particular example though, we’re not interested in the content so we’ll trigger the webhook using the global block folder, knowing it’s always there, easily referenced and contains minimal data of its own (it's not ILocalizable so won't contain language information unlike the content root node). We are, however, interested in information about the scheduled job execution and we can add that data to the webhook payload pretty easily using the “extraData” parameter when we call TriggerWebhooks. The extraData parameter is a Dictionary<string, object> so we can easily add pretty much any data we need.

In this example we’re going to hook into the Executed event exposed by IScheduledJobEvents which fires when a scheduled job has finished executing. The basics are the same as the example above but this time we’re going to extract some information on the job which has executed and pass it into the webhook payload via the extraData parameter. Putting it all together, here’s how it looks.

[InitializableModule]
[ModuleDependency(typeof(KinAndCarta.Connect.Webhooks.Setup.InitialiseWebhooks))]
public class ScheduledJobCompletionWebhookInitialisation : IInitializableModule
{
    private IScheduledJobEvents _scheduledJobEvents;
    private IWebhookRepository _webhookRepo;
    private IContentLoader _contentLoader;
    private EventType _customEvent;

    public void Initialize(InitializationEngine context)
    {
        //Get dependencies
        _webhookRepo = context.Locate.Advanced.GetInstance<IWebhookRepository>();
        _contentLoader = context.Locate.Advanced.GetInstance<IContentLoader>();
        _scheduledJobEvents = context.Locate.Advanced.GetInstance<IScheduledJobEvents>();

        //Create & register our event
        _customEvent = new EventType { ImpactsDescendants = false, Key = "SCHEDULED_JOB_EXECUTED" };
        _webhookRepo.RegisterEventType(_customEvent);

        //Attach to scheduled job executed event
        _scheduledJobEvents.Executed += executedScheduledJob;
    }

    private void executedScheduledJob(object sender, ScheduledJobEventArgs e)
    {
        //Build extra info to pass in payload
        var jobInfo = new Dictionary<string, object>();
        jobInfo.Add("Name", e.Job.Name);
        jobInfo.Add("Success", !e.Job.HasLastExecutionFailed);
        jobInfo.Add("Message", e.Job.LastExecutionMessage);

        //Trigger webhook on asset root page
        _contentLoader.Get<IContent>(ContentReference.GlobalBlockFolder).TriggerWebhooks(_customEvent, _webhookRepo, jobInfo);
    }

    public void Uninitialize(InitializationEngine context)
    {
        //Clean up event
        _scheduledJobEvents.Executed -= executedScheduledJob;
    }
}

As before, you’ll probably want to add a translation to give the event a friendly name. 

Once you’ve compiled and run the solution you can set up a webhook which fires on scheduled job executions, just remember to select the root node or the global blocks folder when setting up the webhook as that’s the node we’re attaching our custom event to. The payload data will look something like this.

{
  "ReferencedBy": [],
  "Content": {
    "ContentLink": {
      "Id": 3,
      "WorkId": 0,
      "GuidValue": "e56f85d0-e833-4e02-976a-2d11fe4d598c",
      "ProviderName": null,
      "Url": null,
      "Expanded": null
    },
    "Name": "For All Sites",
    "Language": null,
    "ExistingLanguages": [],
    "MasterLanguage": null,
    "ContentType": [
      "Folder",
      "SysContentFolder"
    ],
    "ParentLink": {
      "Id": 1,
      "WorkId": 0,
      "GuidValue": "43f936c9-9b23-4ea3-97b2-61c538ad07c9",
      "ProviderName": null,
      "Url": null,
      "Expanded": null
    },
    "RouteSegment": "SysGlobalAssets",
    "Url": null,
    "Changed": "1999-01-01T00:00:00Z",
    "Created": "1999-01-01T00:00:00Z",
    "StartPublish": null,
    "StopPublish": null,
    "Saved": "1999-01-01T00:00:00Z",
    "Status": null
  },
  "ExtraData": {
    "Name": "Publish Delayed Content Versions",
    "Success": true,
    "Message": "Nothing was published."
  },
  "ContentInfo": {
    "ContentId": 3,
    "ContentGuid": "e56f85d0-e833-4e02-976a-2d11fe4d598c",
    "ContentLanguage": "",
    "Name": "For All Sites",
    "Url": null
  },
  "EventType": "SCHEDULED_JOB_EXECUTED",
  "EventTime": "2021-06-10T10:00:10.7754622Z"
}

We can then easily set up a receiver in, for example, Azure Logic Apps to receive our webhook and use the data passed in the extraData parameter to perform actions such as sending a Slack message if a specific job fails.

So there we have it - just a couple of simple examples to show how you can use Webhooks with custom events but hopefully it’s conveyed how simple and flexible it is to set up your own event types. I look forward to hearing how you put it to use in your own projects.

As I mentioned in the previous post, the add-on is available in the Episerver NuGet feed under “KinAndCarta.Webhooks” and requires no particular set-up other than installing the package. Full source code is also available on GitHub under an MIT license.

Jun 10, 2021

Comments

Jun 11, 2021 09:19 AM

Nice one Paul, and thanks for providing an example on the Scheduled Jobs stuff, that's really useful.

James Wilkinson
James Wilkinson Jun 11, 2021 09:20 AM

Nice one Paul, and thanks for providing an example on the Scheduled Jobs stuff, that's really useful.

Please login to comment.
Latest blogs
Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog

ExcludeDeleted(): Prevent Trashed Content from Appearing in Search Results

Introduction In Optimizely CMS, content that is moved to the trash can still appear in search results if it’s not explicitly excluded using the...

Ashish Rasal | Nov 7, 2024

CMS + CMP + Graph integration

We have just released a new package https://nuget.optimizely.com/package/?id=EPiServer.Cms.WelcomeIntegration.Graph which changes the way CMS fetch...

Bartosz Sekula | Nov 5, 2024

Block type selection doesn't work

Imagine you're trying to create a new block in a specific content area. You click the "Create" link, expecting to see a CMS modal with a list of...

Damian Smutek | Nov 4, 2024 | Syndicated blog