Skip to content
Draft
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
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Kontent.Ai.Delivery.Abstractions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;

namespace Kontent.Ai.Boilerplate.CacheInvalidation;

internal class CacheInvalidationService : IHostedService
{
private readonly IDeliveryCacheManager _cacheManager;
private readonly IOptions<DeliveryOptions> _options;
private string? _continuationToken;
private readonly HttpClient _client;
private Timer? _timer;


public CacheInvalidationService(IDeliveryCacheManager cacheManager, IOptions<DeliveryOptions> options)
{
_cacheManager = cacheManager ?? throw new ArgumentNullException(nameof(cacheManager));
_options = options ?? throw new ArgumentNullException(nameof(options));
_client = new HttpClient();
_continuationToken = CacheInvalidationServiceHelper
.CheckChangeFeed(client: _client, options: options, continuationToken: _continuationToken).Result.Item2;
}


public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(TryCacheInvalidation, null, TimeSpan.Zero,
TimeSpan.FromMinutes(5));

return Task.CompletedTask;
}


public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Dispose();
return Task.CompletedTask;
}


private async void TryCacheInvalidation(object? state)
{
IEnumerable<ChangeFeedResponseItem>? changeFeed;
do
{
(changeFeed, var continuationToken) = await CacheInvalidationServiceHelper.CheckChangeFeed(client: _client,
options: _options, continuationToken: _continuationToken);
if (continuationToken != null && continuationToken != _continuationToken)
_continuationToken = continuationToken;
if (changeFeed != null)
await CacheInvalidationServiceHelper.InvalidateCache(itemsChanged: changeFeed,
cacheManager: _cacheManager);
} while (changeFeed != null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Kontent.Ai.Delivery.Abstractions;
using Kontent.Ai.Delivery.Caching;
using Microsoft.Extensions.Options;

namespace Kontent.Ai.Boilerplate.CacheInvalidation;

internal static class CacheInvalidationServiceHelper
{
internal static async Task<Tuple<IEnumerable<ChangeFeedResponseItem>?, string?>> CheckChangeFeed(
IOptions<DeliveryOptions> options, string? continuationToken, HttpClient client)
{
var changeFeedResponse = await client.SendAsync(
new HttpRequestMessage(method: HttpMethod.Get,
requestUri:
$"{options.Value.ProductionEndpoint}/{options.Value.ProjectId}/change-feed")
{ Headers = { { HeaderNames.Continuation, continuationToken } } });
var changeFeedItems = changeFeedResponse.StatusCode == HttpStatusCode.OK
? await JsonSerializer.DeserializeAsync<IEnumerable<ChangeFeedResponseItem>>(
await changeFeedResponse.Content.ReadAsStreamAsync())
: null;

return new Tuple<IEnumerable<ChangeFeedResponseItem>?, string?>(changeFeedItems,
changeFeedResponse.Headers.GetValues(HeaderNames.Continuation).FirstOrDefault());
}

internal static async Task InvalidateCache(IEnumerable<ChangeFeedResponseItem> itemsChanged,
IDeliveryCacheManager cacheManager)
{
var dependencies = new HashSet<string>();
{
foreach (var item in itemsChanged)
{
dependencies.Add(CacheHelpers.GetItemDependencyKey(item.Codename));
}

dependencies.Add(CacheHelpers.GetItemsDependencyKey());
}

foreach (var dependency in dependencies)
{
await cacheManager.InvalidateDependencyAsync(dependency);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using System.Text.Json.Serialization;

namespace Kontent.Ai.Boilerplate.CacheInvalidation;

internal record ChangeFeedResponseItem([property: JsonPropertyName("codename")]string Codename);
6 changes: 6 additions & 0 deletions src/content/Kontent.Ai.Boilerplate/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Kontent.Ai.Boilerplate;

public static class HeaderNames
{
public const string Continuation = "X-Continuation";
}
5 changes: 3 additions & 2 deletions src/content/Kontent.Ai.Boilerplate/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Kontent.Ai.Delivery.Abstractions;
using Kontent.Ai.Delivery.Extensions;
using Kontent.Ai.AspNetCore.Webhooks;
using Kontent.Ai.Boilerplate.CacheInvalidation;

namespace Kontent.Ai.Boilerplate
{
Expand All @@ -37,7 +38,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddSingleton<ITypeProvider, CustomTypeProvider>();
services.AddSingleton<IContentLinkUrlResolver, CustomContentLinkUrlResolver>();
services.AddDeliveryClient(Configuration);

services.AddHostedService<CacheInvalidationService>();
// Use cached client decorator
services.AddDeliveryClientCache(new DeliveryCacheOptions()
{
Expand Down Expand Up @@ -74,7 +75,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

// Register webhook-based cache invalidation controller
app.UseWebhookSignatureValidator(context => context.Request.Path.StartsWithSegments("/webhooks/webhooks", StringComparison.OrdinalIgnoreCase), Configuration.GetSection(nameof(WebhookOptions)));

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
Expand Down