In my previous post, we looked at how to extend a Sitecore XP implementation to capture granular publishing events and fire off non-blocking telemetry signals. By adopting a fire-and-forget approach, we ensured that the CMS remains fast and unaffected by any potential downstream network latency.
But once those publishing signals leave Sitecore, the real heavy lifting begins. A simple ping from the CMS stating that an item has been updated isn’t enough. We need to answer a vital architectural question: What exactly changed? Which assets were newly introduced, and which ones were abandoned?
In this third post of the series, we step out of the legacy CMS framework and dive into the cloud layer. We will look at how to design a lightweight, event-driven middleware using a modern .NET 8 Isolated Worker Azure Function, handle high-volume publishing runs cheaply using serverless architecture, and maintain structural sync using a MongoDB state store and the Sitecore Content Hub SDK.
The Serverless Choice: Event-Driven Scaling
When choosing an environment to process publishing telemetry, serverless is a natural choice. Content authoring is highly unpredictable; the system might sit completely idle for hours, only to be hit with a massive wave of event spikes when a content editor triggers a bulk publication run across hundreds of items.
By building our middleware on Azure Functions, we gain three massive benefits:
- Cost efficiency: We only pay for the exact execution time utilized during active publish windows, leaving the quiet hours completely free.
- Automatic elasticity: The platform automatically provisions compute resources horizontally to handle parallel requests during high-concurrency peak runs.
- Decoupled architecture: By separating the ingestion endpoint from the data processing loops, we protect our application from hitting memory or socket exhaustion limits.
[Sitecore CMS]
│ (Async HTTP Post)
▼
[SitecorePublishFunction API]
│
├── (Service Bus Enabled?)
│ ├── YES ──► [PublicLink Queue] ──► [Delta Queue] ──► [PushToDAM Queue]
│ └── NO ──► (Inline Fallback Processing Engine)
Resilient Ingestion & The Inline Fallback Pattern.
The entry point of our cloud layer is the SitecorePublishFunction, which exposes an HTTP POST endpoint to capture the JSON telemetry coming from Sitecore. However, raw production environments are volatile. What happens if a supporting message queue is temporarily unavailable or misconfigured?
To make our middleware completely robust, our AssetItemController implements an excellent Inline Fallback Pattern. It checks the connectivity and existence of the Azure Service Bus queue infrastructure on the fly. If the queues are healthy, it delegates the messages asynchronously to shield the system. If the queues are disabled or missing, it gracefully slips back to direct inline execution, ensuring the tracking data is processed without dropping a single byte:
public async Task ProcessPublishedItemAsync(PublishedItem publishedItem, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(publishedItem);
if (publishedItem.PublicLinks?.Count == 0)
{
_logger.LogInformation("Item {ItemId} has no public links, skipping to delta calculation", publishedItem.ItemId);
await EnqueueDeltaCalculationAsync(publishedItem, cancellationToken);
}
else
{
await EnqueuePublicLinkProcessingAsync(publishedItem, cancellationToken);
}
}
Resolving Public Links: Querying the DAM in Batches
In our previous post, we looked at how the CMS extracts asset pointers from standard image fields as well as plain URLs (Public Links) embedded inside Rich Text fields. While explicit field values give us clean, immediate numeric IDs, public link fields only provide text strings.
Before we can compute any usage changes, we have to translate those public links back into raw Content Hub Entity IDs.
Our AssetIdsByPublicLinksHandler achieves this by using the official Content Hub Web Client SDK. To avoid hitting API rate limits or creating network bottlenecks, the handler strips out query strings, maps the URLs to relative path segments, and clusters them into parallel batches of 50 paths using a PropertyQueryFilter.
The Core Engine: Executing Stateless Delta Calculations
Once we have aggregated a unified list of active asset IDs, we come to the core of our solution: calculating the delta. Because Azure Functions operate in a stateless cloud space, the middleware does not inherently know what the item looked like during its previous publication run.
To bridge this gap, we use a lightweight MongoDB state store to maintain a record of historical asset-item relationships.
The DeltaCalculationService pulls down the existing asset profile for the item, matches it against the incoming data stream, and instantly extracts our changes via a simple, clean set exclusion rule:
private static (List<int> ToAdd, List<int> ToRemove) CalculateDelta(List<int> currentAssetIds, List<int> newAssetIds)
{
var toAdd = newAssetIds.Where(id => !currentAssetIds.Contains(id)).ToList();
var toRemove = currentAssetIds.Where(id => !newAssetIds.Contains(id)).ToList();
return (toAdd, toRemove);
}
Navigating the Multi-Language Challenge
A critical design element is how we structure our database records. A single Sitecore item can hold different asset variants across multiple languages (e.g., an English version pointing to a graphic with English text, and a Dutch version pointing to a Dutch equivalent).
To ensure we never accidentally overwrite alternate language variations during a single-language publication run, our MongoDB document schema maps languages inside a nested dictionary lookup.
public class AssetItemLink
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public Guid ItemId { get; set; }
[BsonElement("languages")]
public Dictionary<string, LanguageAssetData> Languages { get; set; } = new();
}
Using atomic MongoDB update operations (Builders<AssetItemLink>.Update), our repository can isolate updates directly to languages.en or languages.nl without touching the surrounding language nodes, completely eliminating data overwrite risks.
Writing to the DAM & The Self-Healing Rollback Pattern
The final layer of the processing pipe falls to the PushToDamHandler. When assets are added or removed, this service initiates a write call via the Content Hub SDK to update a secured custom JSON metadata property called UsageTracking directly on the target asset entities.
But this setup introduces a classic architectural tension: Latency vs. Correctness. What happens if a content author references an asset ID that was completely purged or deleted from Content Hub prior to the publish?
If the Content Hub API throws an error or returns a null entity response, our local MongoDB state store would fall permanently out of sync with reality, incorrectly believing that a connection still exists.
To protect state integrity, our engine implements a strict Self-Healing Rollback Pattern. If a asset update fails on the DAM side, the engine catches the error, immediately triggers an automated rollback to strip those invalid asset references out of MongoDB, and writes a structured trace record out to Application Insights.
These failures are flagged inside our telemetry logs using a specific FAILED_OPERATION pattern, enabling infrastructure teams to quickly configure alert monitors for identifying broken asset relationships:
FAILED_OPERATION | Item 'Homepage' (ID: a1b2c3d4-...) references asset(s) [12345] that do not exist in Content Hub. Operation: Add | Path: /sitecore/content/home | Language: en | Error: AssetNotFound - Asset 12345 not found in Content Hub
Summary
By keeping our processing pipeline isolated within serverless Azure Functions, we have created an affordable, highly scalable architecture capable of tracking real-time asset footprints across our entire CMS ecosystem. Our Sitecore environment can fire off fast, non-blocking telemetry alerts, while our backend middleware elegantly manages link mapping, state isolation, and automatic error handling.
Now that our cloud layer is successfully updating asset metadata records inside Content Hub, we need a clean way to surface these insights directly to our users. In the fourth and final post of this series, we will move into front-end development: building custom React and TypeScript portal extensions inside Content Hub’s Shadow DOM to bring these real-time usage metrics right to the content author’s fingertips.

Leave a Reply