From aac4d3eefb328b9186faa9a80234f9d76e95122e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 26 Feb 2024 14:28:38 -0500 Subject: [PATCH] improve caching for static rendering --- Oqtane.Server/Extensions/CacheExtensions.cs | 79 +++++++++++++++++++ .../CacheInvalidationEventSubscriber.cs | 3 +- Oqtane.Server/Services/SiteService.cs | 12 ++- 3 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 Oqtane.Server/Extensions/CacheExtensions.cs diff --git a/Oqtane.Server/Extensions/CacheExtensions.cs b/Oqtane.Server/Extensions/CacheExtensions.cs new file mode 100644 index 000000000..1aff73f55 --- /dev/null +++ b/Oqtane.Server/Extensions/CacheExtensions.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.Caching.Memory; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Oqtane.Extensions +{ + public static class CacheExtensions + { + private static string _cachekeys = "cachekeys"; + + public static TItem GetOrCreate(this IMemoryCache cache, string key, Func factory, bool track) + { + if (!cache.TryGetValue(key, out object result)) + { + using ICacheEntry entry = cache.CreateEntry(key); + result = factory(entry); + entry.Value = result; + + if (track) + { + // track the cache key + List cachekeys; + if (!cache.TryGetValue(_cachekeys, out cachekeys)) + { + cachekeys = new List(); + } + if (!cachekeys.Contains(key)) + { + cachekeys.Add(key); + cache.Set(_cachekeys, cachekeys, new MemoryCacheEntryOptions { Priority = CacheItemPriority.NeverRemove }); + } + } + } + + return (TItem)result; + } + + public static void Remove(this IMemoryCache cache, string key, bool track) + { + List cachekeys; + + if (track && key.EndsWith("*")) + { + // wildcard cache key removal + var prefix = key.Substring(0, key.Length - 1); + if (cache.TryGetValue(_cachekeys, out cachekeys) && cachekeys.Any()) + { + for (var i = cachekeys.Count - 1; i >= 0; i--) + { + if (cachekeys[i].StartsWith(prefix)) + { + cache.Remove(cachekeys[i]); + cachekeys.RemoveAt(i); + } + } + cache.Set(_cachekeys, cachekeys, new MemoryCacheEntryOptions { Priority = CacheItemPriority.NeverRemove }); + } + } + else + { + cache.Remove(key); + } + + // reconcile all tracked cache keys + if (track && cache.TryGetValue(_cachekeys, out cachekeys) && cachekeys.Any()) + { + for (var i = cachekeys.Count - 1; i >= 0; i--) + { + if (!cache.TryGetValue(cachekeys[i], out _)) + { + cachekeys.RemoveAt(i); + } + } + cache.Set(_cachekeys, cachekeys, new MemoryCacheEntryOptions { Priority = CacheItemPriority.NeverRemove }); + } + } + } +} diff --git a/Oqtane.Server/Infrastructure/EventSubscribers/CacheInvalidationEventSubscriber.cs b/Oqtane.Server/Infrastructure/EventSubscribers/CacheInvalidationEventSubscriber.cs index 5bf1fd146..4c9ee85dc 100644 --- a/Oqtane.Server/Infrastructure/EventSubscribers/CacheInvalidationEventSubscriber.cs +++ b/Oqtane.Server/Infrastructure/EventSubscribers/CacheInvalidationEventSubscriber.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Caching.Memory; +using Oqtane.Extensions; using Oqtane.Models; using Oqtane.Shared; @@ -18,7 +19,7 @@ public void EntityChanged(SyncEvent syncEvent) // when site entities change (ie. site, pages, modules, etc...) a site refresh event is raised and the site cache item needs to be refreshed if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Refresh) { - _cache.Remove($"site:{syncEvent.TenantId}:{syncEvent.EntityId}"); + _cache.Remove($"site:{syncEvent.TenantId}:{syncEvent.EntityId}*", true); } // when a site entity is updated the hosting model may have changed, so the client assemblies cache items need to be refreshed diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index c1b1a1abe..60804ce2c 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -65,15 +65,19 @@ public async Task GetSiteAsync(int siteId) Site site = null; if (!_accessor.HttpContext.User.Identity.IsAuthenticated) { - site = _cache.GetOrCreate($"site:{_accessor.HttpContext.GetAlias().SiteKey}", entry => + site = _cache.GetOrCreate($"site:{_accessor.HttpContext.GetAlias().SiteKey}", entry => { entry.SlidingExpiration = TimeSpan.FromMinutes(30); return GetSite(siteId); - }); + }, true); } - else + else // authenticated - cached per user { - site = GetSite(siteId); + site = _cache.GetOrCreate($"site:{_accessor.HttpContext.GetAlias().SiteKey}:{_accessor.HttpContext.User.UserId}", entry => + { + entry.SlidingExpiration = TimeSpan.FromMinutes(30); + return GetSite(siteId); + }, true); } return await Task.Run(() => site); }