Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"Kontent.Ai.ModelGenerator": {
"version": "8.0.0",
"version": "9.0.0",
"commands": [
"KontentModelGenerator"
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,64 +1,151 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Kontent.Ai.Delivery.Abstractions;
using Kontent.Ai.Delivery.Caching;
using Kontent.Ai.AspNetCore.Webhooks.Models;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace Kontent.Ai.Boilerplate.Areas.WebHooks.Controllers
{
[Area("WebHooks")]
public class WebhooksController : Controller
{
private readonly IDeliveryCacheManager _cacheManager;

public WebhooksController(IDeliveryCacheManager cacheManager)
public WebhooksController(IDeliveryCacheManager cacheManager, IDeliveryClient deliveryClient)
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep the original constructor, rather then using this hybrid solution for field validation. but it is up to your decision.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might be right, especially since rest of the project doesn't use primary constructors either. reverting.

_cacheManager = cacheManager ?? throw new ArgumentNullException(nameof(cacheManager));
_deliveryClient = deliveryClient ?? throw new ArgumentNullException(nameof(deliveryClient));
}

private readonly IDeliveryCacheManager _cacheManager;
private readonly IDeliveryClient _deliveryClient;

[HttpPost]
public async Task<IActionResult> Index([FromBody] DeliveryWebhookModel model)
public async Task<IActionResult> Index([FromBody] WebhookNotification? webhook)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we support both until the 1.11.? or is it ok to cause this breaking now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the underlying package is https://github.com/kontent-ai/aspnetcore-extensions and it still supports legacy webhooks as well. the boilerplate should ideally use the most up-to-date approach and since we'll be removing legacy webhooks soon, I don't see a problem with adopting the new webhooks (wouldn't want users to scaffold a new project with webhooks that won't be a thing in a month).

{
if (model != null)
if (webhook is null)
{
var dependencies = new HashSet<string>();
if (model.Data.Items?.Any() == true)
{
foreach (var item in model.Data.Items ?? Enumerable.Empty<DeliveryWebhookItem>())
{
dependencies.Add(CacheHelpers.GetItemDependencyKey(item.Codename));
}
return Ok();
}

dependencies.Add(CacheHelpers.GetItemsDependencyKey());
Copy link
Contributor

@sevcik-martin sevcik-martin Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about items dependencyKey?
also when there is taxonomy update, there were other dependecies as well.
Current webhooks do not include info about touched dependencies though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this was way too simple. I added more complex logic with pattern matching to wipe the keys of (potentially) dependent entities

}
var dependencies = new HashSet<string>(StringComparer.Ordinal);
var usedInFetchTasks = new List<Task<IEnumerable<string>>>();

if (model.Data.Taxonomies?.Any() == true)
{
foreach (var taxonomy in model.Data.Taxonomies ?? Enumerable.Empty<Taxonomy>())
{
dependencies.Add(CacheHelpers.GetTaxonomyDependencyKey(taxonomy.Codename));
}

dependencies.Add(CacheHelpers.GetTaxonomiesDependencyKey());
dependencies.Add(CacheHelpers.GetItemsDependencyKey());
dependencies.Add(CacheHelpers.GetTypesDependencyKey());
}
foreach (var notification in webhook.Notifications ?? Enumerable.Empty<WebhookModel>())
{
ProcessNotification(notification, dependencies, usedInFetchTasks);
}

if (model.Message.Type == "content_type")
{
dependencies.Add(CacheHelpers.GetTypesDependencyKey());
}
await CollectUsedInDependencies(usedInFetchTasks, dependencies);
await InvalidateCacheDependencies(dependencies);

return Ok();
}

private void ProcessNotification(WebhookModel notification, HashSet<string> dependencies, List<Task<IEnumerable<string>>> usedInFetchTasks)
{
switch (notification)
{
// Content item variant: invalidate the item and listing; on publish/unpublish also invalidate items that reference it
case { Message.ObjectType: "content_item", Data.System.Codename: var codename, Message.Action: var action }:
ProcessContentItemNotification(codename, action, dependencies, usedInFetchTasks);
break;

// Taxonomy change: for created, invalidate the specific taxonomy and listing, otherwise also invalidate items and type listings
case { Message.ObjectType: "taxonomy", Data.System.Codename: var codename, Message.Action: var action }:
ProcessTaxonomyNotification(codename, action, dependencies);
break;

// Content type change: invalidate types listing and items listing
case { Message.ObjectType: "content_type" }:
ProcessContentTypeNotification(dependencies);
break;

// Language change: invalidate languages listing
case { Message.ObjectType: "language" }:
ProcessLanguageNotification(dependencies);
break;

// Asset and unknown types: no Delivery SDK dependency helpers; skip
}
}

private void ProcessContentItemNotification(string codename, string action, HashSet<string> dependencies, List<Task<IEnumerable<string>>> usedInFetchTasks)
{
dependencies.Add(CacheHelpers.GetItemDependencyKey(codename));
dependencies.Add(CacheHelpers.GetItemsDependencyKey());

if (IsPublishOrUnpublishAction(action))
{
usedInFetchTasks.Add(GetItemUsedInDependencyKeys(codename));
}
}

private static void ProcessTaxonomyNotification(string codename, string action, HashSet<string> dependencies)
{
dependencies.Add(CacheHelpers.GetTaxonomyDependencyKey(codename));
dependencies.Add(CacheHelpers.GetTaxonomiesDependencyKey());

if (!IsCreatedAction(action))
{
dependencies.Add(CacheHelpers.GetItemsDependencyKey());
dependencies.Add(CacheHelpers.GetTypesDependencyKey());
}
}

private static void ProcessContentTypeNotification(HashSet<string> dependencies)
{
dependencies.Add(CacheHelpers.GetTypesDependencyKey());
dependencies.Add(CacheHelpers.GetItemsDependencyKey());
}

private static void ProcessLanguageNotification(HashSet<string> dependencies)
{
dependencies.Add(CacheHelpers.GetLanguagesDependencyKey());
}

private static bool IsPublishOrUnpublishAction(string action) =>
string.Equals(action, "published", StringComparison.OrdinalIgnoreCase) ||
string.Equals(action, "unpublished", StringComparison.OrdinalIgnoreCase);

private static bool IsCreatedAction(string action) =>
string.Equals(action, "created", StringComparison.OrdinalIgnoreCase);

foreach (var dependency in dependencies)
private static async Task CollectUsedInDependencies(List<Task<IEnumerable<string>>> usedInFetchTasks, HashSet<string> dependencies)
{
if (usedInFetchTasks.Count == 0) return;

var usedInResults = await Task.WhenAll(usedInFetchTasks);
foreach (var key in usedInResults.SelectMany(result => result))
{
dependencies.Add(key);
}
}

private async Task InvalidateCacheDependencies(HashSet<string> dependencies)
{
if (dependencies.Count == 0) return;

var invalidations = dependencies.Select(key => _cacheManager.InvalidateDependencyAsync(key));
await Task.WhenAll(invalidations);
}

private async Task<IEnumerable<string>> GetItemUsedInDependencyKeys(string codename)
{
List<string> dependencyKeys = [];
var feed = _deliveryClient.GetItemUsedIn(codename);

while (feed.HasMoreResults)
{
IDeliveryItemsFeedResponse<IUsedInItem> response = await feed.FetchNextBatchAsync();

foreach (IUsedInItem item in response.Items)
{
await _cacheManager.InvalidateDependencyAsync(dependency);
dependencyKeys.Add(CacheHelpers.GetItemDependencyKey(item.System.Codename));
}
}

return Ok();
return dependencyKeys;
}
}
}
11 changes: 6 additions & 5 deletions src/content/Kontent.Ai.Boilerplate/Kontent.Ai.Boilerplate.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<UserSecretsId>Kontent.Ai.Boilerplate</UserSecretsId>
<PackageId>Kontent.Ai.Boilerplate</PackageId>
<Authors>Kontent s.r.o.</Authors>
Expand All @@ -22,17 +22,18 @@
<PackageTags>kontent-ai;mvc;aspnet;aspnetmvc;dotnetcore;dotnet;aspnetcore</PackageTags>
<NuspecFile>$(MSBuildThisFileDirectory)..\..\Template.nuspec</NuspecFile>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<Content Include="IISUrlRewrite.xml" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Kontent.Ai.AspNetCore" Version="0.13.1" />
<PackageReference Include="Kontent.Ai.Delivery" Version="17.0.0" />
<PackageReference Include="Kontent.Ai.Delivery.Caching" Version="17.0.0" />
<PackageReference Include="SimpleMvcSitemap" Version="4.0.0" />
<PackageReference Include="Kontent.Ai.AspNetCore" Version="0.14.1" />
<PackageReference Include="Kontent.Ai.Delivery" Version="18.3.0" />
<PackageReference Include="Kontent.Ai.Delivery.Caching" Version="18.3.0" />
<PackageReference Include="SimpleMvcSitemap" Version="4.0.1" />
<None Include="../../../README.md" Pack="true" PackagePath=""/>
<None Include="../../../img/kai-logo-symbol-color-rgb.png" Pack="true" PackagePath=""/>
</ItemGroup>
Expand Down
Loading