In my last blog post, I looked at the Webhooks Event Handlers introduced in Sitecore 10.3 and XM Cloud. In this post I will show how we can define our own events and use these together with out-of-the box webhook functionality with a customized payload.
To recap: As we saw last week, using the automation platform IFTTT, I was able to send emails when items in Sitecore was saved. I did this by configuring a Webhook Event Handler to be triggered on the item:saved
event, calling an endpoint on the IFTTT platform. I did this with no code being added to the Sitecore solution, and while I was impressed by the simplicity, I had some reservations about the content of the webhook payload which – in my opinion – missed some critical information like e.g., the full path of the item being saved.
In this post I will show how we can customize the content of the payload to suit or needs. I will focus on two events, item:saved
and publish:end
, and change the payload for these events so that we get the path of a saved item, and the URL for when a publish is done.
The way I am going to do this is two define two custom events: custom:item:saved
and custom:publish:end
. These events will be triggered when item:saved
and publish:end
are triggered (let us call the Sitecore event the parent event). This will allow us to inspect the default payload of the parent event before the webhook endpoint is called, changing the payload to suit our needs. So, let us get started.
Step 1: Define and wire up custom events
The build-in Sitecore events that we use when configuring a Webhook Event Handler are defined in /sitecore/system/Settings/Webhooks/Event Types
. To make our solution work seamlessly we are going to define our two custom events in a folder structure mimicking the Sitecore structure (having a folder for Item events and one for Publish event).
Notice that while the item names use underscores, the events themself contains colons:
We are then going to introduce a processor that will automatically wire our custom events up with the matching build-in Sitecore event (the parent event), e.g., calling custom:item:saved
when item:saved
is triggered:
namespace sc10xm.Pipelines.Initialize
{
using Sitecore.Data;
using Sitecore.Events;
using Sitecore.Pipelines;
using Sitecore.SecurityModel;
using System;
using System.Linq;
public class InitializeCustomWebhookEvents
{
const string eventDatabase = "master";
const string eventTemplateName = "Event";
const string eventNameFieldName = "Event Name";
const string customEventPrefix = "custom:";
const string customEventsRootPath =
"/sitecore/system/Settings/Webhooks/Event Types/Custom";
public void Process(PipelineArgs args)
{
var database = Database.GetDatabase(eventDatabase);
using (new SecurityDisabler())
{
var customEventsItems = database.GetItem(customEventsRootPath)?.
Axes.GetDescendants().Where(i => i.TemplateName.Equals(eventTemplateName));
foreach (var customEventsItem in customEventsItems)
{
var customEventName = customEventsItem?[eventNameFieldName];
if (customEventName == null || !customEventName.StartsWith(customEventPrefix))
continue;
var parentEventName = customEventName.Substring(customEventPrefix.Length);
Event.Subscribe(parentEventName, this.OnEvent);
}
};
}
public void OnEvent(object sender, EventArgs args)
{
if (args is SitecoreEventArgs sitecoreEventArgs)
{
var customEventName = $"{customEventPrefix}{sitecoreEventArgs.EventName}";
Event.RaiseEvent(customEventName);
}
}
}
}
As you can see, the code is generic – it simply loops through all my custom events, looking for events starting with the prefix custom:
, and then adding the OnEvent
event handler to each of the build-in parent Sitecore events.
The final piece of the code is the OnEvent
event handler which will make sure that our custom event is called. With this code in place our custom events will now “piggyback” on the build-in Sitecore event, and we now have the OnEvent
method as an entrypoint for customizing the payload of our custom events.
We of cause needs to patch in our new processor, but after that our custom events will start to fire once the matching parent Sitecore events are triggered:
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:role="http://www.sitecore.net/xmlconfig/role/">
<sitecore role:require="Standalone or ContentManagement">
<pipelines>
<initialize>
<processor type="sc10xm.Pipelines.Initialize.InitializeCustomWebhookEvents, sc10xm"/>
</initialize>
</pipelines>
</sitecore>
</configuration>
Step 2: Customizing the payload
We are now going to extend the OnEvent
method with some custom code to extract the parameters we would like to pass on to our webhook endpoint (I am simply going to show the complete code below):
namespace sc10xm.Pipelines.Initialize
{
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Events;
using Sitecore.Links;
using Sitecore.Links.UrlBuilders;
using Sitecore.Pipelines;
using Sitecore.Publishing;
using Sitecore.SecurityModel;
using System;
using System.Collections.Generic;
using System.Linq;
public class InitializeCustomWebhookEvents
{
const string eventDatabase = "master";
const string eventTemplateName = "Event";
const string eventNameFieldName = "Event Name";
const string customEventPrefix = "custom:";
const string customEventsRootPath =
"/sitecore/system/Settings/Webhooks/Event Types/Custom";
static readonly List<IParameterMapper> mappers =
new List<IParameterMapper>()
{
new ItemMapper(),
new PublisherMapper()
};
public void Process(PipelineArgs args)
{
var database = Database.GetDatabase(eventDatabase);
using (new SecurityDisabler())
{
var customEventsItems = database.GetItem(customEventsRootPath)?.
Axes.GetDescendants().Where(i => i.TemplateName.Equals(eventTemplateName));
foreach (var customEventsItem in customEventsItems)
{
var customEventName = customEventsItem?[eventNameFieldName];
if (customEventName == null || !customEventName.StartsWith(customEventPrefix))
continue;
var parentEventName = customEventName.Substring(customEventPrefix.Length);
Event.Subscribe(parentEventName, this.OnEvent);
}
};
}
public void OnEvent(object sender, EventArgs args)
{
if (args is SitecoreEventArgs sitecoreEventArgs)
{
var properties = new Properties();
foreach (var p in sitecoreEventArgs.Parameters)
{
foreach (var mapper in mappers.Where(m => m.CanMap(p)))
{
mapper.Map(p, properties);
}
}
var customEventName = $"{customEventPrefix}{sitecoreEventArgs.EventName}";
Event.RaiseEvent(
customEventName,
properties
);
}
}
private class Properties : Dictionary<string, object>
{
public Properties() : base()
{
}
}
private interface IParameterMapper
{
bool CanMap(object o);
void Map(object o, Properties properties);
}
private abstract class ParameterMapper<T> : IParameterMapper where T : class
{
public bool CanMap(object o)
{
return o is T;
}
public void Map(object o, Properties properties)
{
var t = o as T;
if (t != null)
DoMap(t, properties);
}
public abstract void DoMap(T t, Properties properties);
}
private class ItemMapper : ParameterMapper<Item>
{
public override void DoMap(Item item, Properties properties)
{
properties["Item"] = new
{
Name = item.Name,
Id = item.ID,
Path = item.Paths.FullPath,
Author = item.Statistics.UpdatedBy
};
}
}
private class PublisherMapper : ParameterMapper<Publisher>
{
public override void DoMap(Publisher publisher, Properties properties)
{
properties["Publisher"] = new
{
RootItem = new {
Name = publisher.Options.RootItem.Name,
Id = publisher.Options.RootItem.ID,
Path = publisher.Options.RootItem.Paths.FullPath,
Url = LinkManager.GetItemUrl(
publisher.Options.RootItem,
new ItemUrlBuilderOptions
{
AlwaysIncludeServerUrl = true,
Site = Sitecore.Sites.SiteContext.GetSite("website")
}
)
},
UserName = publisher.Options.UserName
};
}
}
}
}
There is a number of moving parts here: First of all you will notice that in the OnEvent
method loops through each parameter of the parent event and check if we are able to map the parameter type. If we are, we are mapping it into a properties dictionary which we in turn pass on to our custom event.
Depending on the type of parent event we will receive different types of parameters from Sitecore. The saved:item
event will include the saved item (as an Item
) whereas the publish:end
event will include the information about the publish in a Publisher
parameter. As you can see I have included mappers for these two types, extracting parameters from each of them and adding them to the properties dictionary.
These two mappers will cover many of the build in Sitecore event available for webhooks, but we could easily build other mappers – customizing both the structure and content of the payload even further.
And it works
We now have code in place that will make sure that if we define a custom event called custom:some:event
, when Sitecore triggers some:event
, our custom event is also triggered.
We also make sure that whatever parameters Sitecore passes to some:event
is run through our mappers, extracting properties from parameters we are able to map.
So let us see how this works in practice: I am now going to set up an applet in IFTTT (for a introduction of IFTTT applets see my last post). The applet will receive a customized payload from Sitecore and generate a email.
I am going to trigger this applet each time a publish is performed in Sitecore – but using my custom event (custom:publish:end
):
As the parameters are now passed through my PublisherMapper
, I get the following payload when the event is triggered:
{
"EventName":"custom:publish:end",
"Properties":{
"Publisher":{
"RootItem":{
"Name":"Content Page",
"Id":"7ee70eea-cb21-4b29-9f82-b3e76d7da1a1",
"Path":"/sitecore/content/Home/Content Page",
"Url":"https://sc103xmcm.dev.local/en/Content-Page"
},
"UserName":"sitecore\\Admin"
}
},
"WebhookItemId":"41559c28-51c1-40d4-ac8e-76e2b064a45e",
"WebhookItemName":"Publish end"
}
I am now able to add this filter to extract the fields and put them into an email:
With this setup, I will now receive emails when items are published including crucial information as e.g. the URL:
With this code in place, it should be quite straightforward to extend and customize the payload generated by build in Sitecore events. The use cases where a customized payload it needed could include e.g., cache clearing, pushing an item to a crawler or a search index or collecting statistics and logs, where we would most likely require information like e.g. the URL.