Skip to content

Commit

Permalink
Merge pull request #86 from Kentico/feature/KCL-2975_Best_practices_f…
Browse files Browse the repository at this point in the history
…or_caching

Minor caching changes and readme update
  • Loading branch information
petrsvihlik authored Nov 28, 2019
2 parents 9d67380 + 1bd8682 commit cb1494a
Show file tree
Hide file tree
Showing 9 changed files with 38 additions and 44 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,15 @@ Rich text elements in Kentico Kontent can contain links to other content items.

### How to set up webhook-enabled caching

All content retrieved from Kentico Kontent is by default [cached](https://github.com/Kentico/kontent-boilerplate-net/blob/master/src/content/Kentico.Kontent.Boilerplate/Services/CachedDeliveryClient.cs) for 24 hours in a [MemoryCache](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.caching.memorycache) singleton object. You can either change the time by overriding the value of the `CacheTimeoutSeconds` environment variable (e.g. in appsettings.json). Or, if you want your app to immediately clear cache entries of changed content, you can create a webhook in Kentico Kontent for that.
All content retrieved from Kentico Kontent is by default [cached](https://github.com/Kentico/kontent-boilerplate-net/blob/master/src/content/Kentico.Kontent.Boilerplate/Caching/Default/CachingDeliveryClient.cs) for 10 minutes in a [MemoryCache](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycache) singleton object. When content is stale (newer version exists) it is cached for 2 seconds. You can change the expiration times in [Startup](https://github.com/Kentico/kontent-boilerplate-net/blob/6fb2b26deecb858f3853d84e29121e3f17b3a291/src/content/Kentico.Kontent.Boilerplate/Startup.cs#L47).

To create the web hook, go to Project settings --> Webhooks --> Create new Webhook. Give it a name (like "Cache invalidation webhook") and the publicly routable URL of your app with `/WebHooks/KenticoKontent` appended (like "https://myapp.azurewebsites.net/WebHooks/KenticoKontent"). Then, copy the API secret and paste it as the `KenticoKontentWebhookSecret` environment variable (secret) into your app's settings.
If displaying outdated content for a limited time is not an option, you need to use another caching strategy and invalidate cached content when a webhook notification about content change is received. To enable this caching strategy just switch to another [caching client](https://github.com/Kentico/kontent-boilerplate-net/blob/master/src/content/Kentico.Kontent.Boilerplate/Caching/Webhooks/CachingDeliveryClient.cs) by using `IServiceCollection.AddWebhookInvalidatedCachingClient` extension in [Startup](https://github.com/Kentico/kontent-boilerplate-net/blob/6fb2b26deecb858f3853d84e29121e3f17b3a291/src/content/Kentico.Kontent.Boilerplate/Startup.cs#L53).

![New webhook configuration](https://i.imgur.com/ootVcPZ.png)
Also, you need to [create a webhook](https://docs.kontent.ai/tutorials/develop-apps/integrate/using-webhooks-for-automatic-updates#a-creating-a-webhook). When entering a webhook URL, append a `/Webhooks/Webhooks` path to a publicly available URL address of the application, e.g. `https://myapp.azurewebsites.net/Webhooks/Webhooks`. Finally, copy the API secret and put it into the app settings (usually the appsettings.json) as the `KenticoKontentWebhookSecret` environment variable.

**Note**: During local development, you can use the [ngrok](https://ngrok.com/) service to route to your workstation.
![New webhook configuration](https://i.imgur.com/TjJ7n5H.png)

**Note**: During local development, you can use the [ngrok](https://ngrok.com/) service to route to your workstation. Simply start your application locally and run command `ngrok http [port] localhost:[port]` and set the webhook URL to the displayed HTTPS address.

**Note**: Speed of the Delivery/Preview API service is already tuned up because the service uses a geo-distributed CDN network for most of the types of requests. Therefore, the main advantage of caching in Kentico Kontent applications is not speed but lowering the amount of requests needed (See [pricing](https://kontent.ai/pricing) for details).

Expand Down
22 changes: 6 additions & 16 deletions src/content/Kentico.Kontent.Boilerplate/Caching/CacheHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace Kentico.Kontent.Boilerplate.Caching
{
public class CacheHelper
public static class CacheHelper
{
#region Constants

Expand Down Expand Up @@ -174,7 +174,7 @@ public static IEnumerable<string> GetItemJsonDependencies(JObject response)

return dependencies.Count > MAX_DEPENDENCY_ITEMS
? new[] { GetItemsDependencyKey() }
: dependencies.Distinct();
: dependencies.AsEnumerable();

bool TryExtractCodename(JObject item, out string codename)
{
Expand Down Expand Up @@ -215,7 +215,7 @@ public static IEnumerable<string> GetItemDependencies(dynamic response)

return dependencies.Count > MAX_DEPENDENCY_ITEMS
? new[] { GetItemsDependencyKey() }
: dependencies.Distinct();
: dependencies.AsEnumerable();
}

public static IEnumerable<string> GetItemsJsonDependencies(JObject response)
Expand All @@ -232,16 +232,6 @@ public static IEnumerable<string> GetItemsDependencies(dynamic response)
: Enumerable.Empty<string>();
}

public static IEnumerable<string> GetItemsFeedDependencies(DeliveryItemsFeedResponse _)
{
return new[] { GetItemsDependencyKey() };
}

public static IEnumerable<string> GetItemsFeedDependencies<T>(DeliveryItemsFeedResponse<T> _)
{
return new[] { GetItemsDependencyKey() };
}

public static IEnumerable<string> GetTypeJsonDependencies(JObject response)
{
return response?["system"]?["codename"] != null
Expand Down Expand Up @@ -334,10 +324,10 @@ private static bool IsItemListingResponse(dynamic response)

private static bool IsComponent(JProperty property)
{
// Components have 0 on index 14 in its id.
// xxxxxxxx-xxxx-0xxx-xxxx-xxxxxxxxxxxx
// Components have substring 01 in its id starting at position 14.
// xxxxxxxx-xxxx-01xx-xxxx-xxxxxxxxxxxx
var id = property?.Value?["system"]?["id"]?.Value<string>();
return Guid.TryParse(id, out _) && id?[14] == '0';
return Guid.TryParse(id, out _) && id.Substring(14, 2).Equals("01", StringComparison.Ordinal);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Kentico.Kontent.Boilerplate.Caching
{
public class CacheOptions
{
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromMinutes(10);
public TimeSpan StaleContentTimeout { get; set; } = TimeSpan.FromSeconds(10);
public TimeSpan DefaultExpiration { get; set; } = TimeSpan.FromMinutes(10);
public TimeSpan StaleContentExpiration { get; set; } = TimeSpan.FromSeconds(10);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ public async Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> valueFactory, Fu
var valueCacheOptions = new MemoryCacheEntryOptions();
if (value is AbstractResponse ar && ar.HasStaleContent)
{
valueCacheOptions.SetAbsoluteExpiration(_cacheOptions.StaleContentTimeout);
valueCacheOptions.SetAbsoluteExpiration(_cacheOptions.StaleContentExpiration);
}
else
{
valueCacheOptions.SetAbsoluteExpiration(_cacheOptions.DefaultTimeout);
valueCacheOptions.SetAbsoluteExpiration(_cacheOptions.DefaultExpiration);
}

return _memoryCache.Set(key, value, valueCacheOptions);
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ public async Task<T> GetOrAddAsync<T>(string key, Func<Task<T>> valueFactory, Fu
var valueCacheOptions = new MemoryCacheEntryOptions();
if (value is AbstractResponse ar && ar.HasStaleContent)
{
valueCacheOptions.SetAbsoluteExpiration(_cacheOptions.StaleContentTimeout);
valueCacheOptions.SetAbsoluteExpiration(_cacheOptions.StaleContentExpiration);
}
else
{
valueCacheOptions.SetSlidingExpiration(_cacheOptions.DefaultTimeout);
valueCacheOptions.SetSlidingExpiration(_cacheOptions.DefaultExpiration);
}

var dependencies = dependenciesFactory?.Invoke(value) ?? new List<string>();
Expand Down
16 changes: 8 additions & 8 deletions src/content/Kentico.Kontent.Boilerplate/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,16 @@ IDeliveryClient BuildBaseClient(IServiceProvider sp) => DeliveryClientBuilder
.Build();

// Use cached client version based on the use case
//services.AddCachingClient(BuildBaseClient, options =>
//{
// options.StaleContentTimeout = TimeSpan.FromSeconds(2);
// options.DefaultTimeout = TimeSpan.FromSeconds(20);
//});
services.AddWebhookInvalidatedCachingClient(BuildBaseClient, options =>
services.AddCachingClient(BuildBaseClient, options =>
{
options.StaleContentTimeout = TimeSpan.FromSeconds(2);
options.DefaultTimeout = TimeSpan.FromHours(24);
options.StaleContentExpiration = TimeSpan.FromSeconds(2);
options.DefaultExpiration = TimeSpan.FromMinutes(10);
});
//services.AddWebhookInvalidatedCachingClient(BuildBaseClient, options =>
//{
// options.StaleContentExpiration = TimeSpan.FromSeconds(2);
// options.DefaultExpiration = TimeSpan.FromHours(24);
//});

HtmlHelperExtensions.ProjectOptions = Configuration.Get<ProjectOptions>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ public async Task GetOrAddAsync_IsNotStaleContent_CachedValueExpiresAfterDefault
{
const string key = "key";
var value = await GetAbstractResponseInstance(false);
_cacheOptions.DefaultTimeout = TimeSpan.FromMilliseconds(500);
_cacheOptions.DefaultExpiration = TimeSpan.FromMilliseconds(500);

await _cacheManager.GetOrAddAsync(key, () => Task.FromResult(value));
await Task.Delay(_cacheOptions.DefaultTimeout);
await Task.Delay(_cacheOptions.DefaultExpiration);

Assert.False(_memoryCache.TryGetValue(key, out _));
}
Expand All @@ -94,10 +94,10 @@ public async Task GetOrAddAsync_IsStaleContent_CachedValueExpiresAfterStaleConte
{
const string key = "key";
var value = await GetAbstractResponseInstance(true);
_cacheOptions.StaleContentTimeout = TimeSpan.FromMilliseconds(500);
_cacheOptions.StaleContentExpiration = TimeSpan.FromMilliseconds(500);

await _cacheManager.GetOrAddAsync(key, () => Task.FromResult(value));
await Task.Delay(_cacheOptions.StaleContentTimeout);
await Task.Delay(_cacheOptions.StaleContentExpiration);

Assert.False(_memoryCache.TryGetValue(key, out _));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ public static class ResponseHelper

internal static (string codename, object item) CreateComponent()
{
// Components have substring 01 in its id starting at position 14.
// xxxxxxxx-xxxx-01xx-xxxx-xxxxxxxxxxxx
var id = Guid.NewGuid().ToString();
id = $"{id.Substring(0, 14)}0{id.Substring(15)}";
id = $"{id.Substring(0, 14)}01{id.Substring(16)}";
var codename = $"n{id}";
return (
codename,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,10 @@ public async Task GetOrAddAsync_IsNotStaleContent_CachedValueExpiresAfterDefault
{
const string key = "key";
var value = await GetAbstractResponseInstance(false);
_cacheOptions.DefaultTimeout = TimeSpan.FromMilliseconds(500);
_cacheOptions.DefaultExpiration = TimeSpan.FromMilliseconds(500);

await _cacheManager.GetOrAddAsync(key, () => Task.FromResult(value), null);
await Task.Delay(_cacheOptions.DefaultTimeout);
await Task.Delay(_cacheOptions.DefaultExpiration);

Assert.False(_memoryCache.TryGetValue(key, out _));
}
Expand All @@ -164,10 +164,10 @@ public async Task GetOrAddAsync_IsStaleContent_CachedValueExpiresAfterStaleConte
{
const string key = "key";
var value = await GetAbstractResponseInstance(true);
_cacheOptions.StaleContentTimeout = TimeSpan.FromMilliseconds(500);
_cacheOptions.StaleContentExpiration = TimeSpan.FromMilliseconds(500);

await _cacheManager.GetOrAddAsync(key, () => Task.FromResult(value), null);
await Task.Delay(_cacheOptions.StaleContentTimeout);
await Task.Delay(_cacheOptions.StaleContentExpiration);

Assert.False(_memoryCache.TryGetValue(key, out _));
}
Expand Down

0 comments on commit cb1494a

Please sign in to comment.