From d7b3b444b525a7d42f9a363873288db47e04b622 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Mon, 9 Mar 2020 15:37:49 -0400 Subject: [PATCH] infrastructure for dealing with client cache invalidation in a multi-user environment --- Oqtane.Client/Modules/ModuleBase.cs | 2 +- Oqtane.Client/Services/AliasService.cs | 4 +- .../Services/Interfaces/IAliasService.cs | 3 +- Oqtane.Client/Shared/PageState.cs | 1 + Oqtane.Client/Shared/SiteRouter.razor | 52 ++++++++++++------- Oqtane.Server/Controllers/AliasController.cs | 14 +++-- Oqtane.Server/Controllers/PageController.cs | 5 +- .../Infrastructure/Interfaces/ISyncManager.cs | 13 +++++ Oqtane.Server/Infrastructure/SyncManager.cs | 44 ++++++++++++++++ Oqtane.Server/Startup.cs | 1 + Oqtane.Shared/Models/Alias.cs | 6 +++ Oqtane.Shared/Models/SyncEvent.cs | 12 +++++ 12 files changed, 130 insertions(+), 27 deletions(-) create mode 100644 Oqtane.Server/Infrastructure/Interfaces/ISyncManager.cs create mode 100644 Oqtane.Server/Infrastructure/SyncManager.cs create mode 100644 Oqtane.Shared/Models/SyncEvent.cs diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index ecfe4f8e..690dd4a1 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -125,7 +125,7 @@ namespace Oqtane.Modules // logging methods public async Task Log(Alias alias, LogLevel level, string function, Exception exception, string message, params object[] args) { - int pageId = PageState.Page.PageId; + int pageId = ModuleState.PageId; int moduleId = ModuleState.ModuleId; int? userId = null; if (PageState.User != null) diff --git a/Oqtane.Client/Services/AliasService.cs b/Oqtane.Client/Services/AliasService.cs index 03c0b845..87ff5e43 100644 --- a/Oqtane.Client/Services/AliasService.cs +++ b/Oqtane.Client/Services/AliasService.cs @@ -39,7 +39,7 @@ namespace Oqtane.Services return await _http.GetJsonAsync(apiurl + "/" + AliasId.ToString()); } - public async Task GetAliasAsync(string Url) + public async Task GetAliasAsync(string Url, DateTime LastSyncDate) { Uri uri = new Uri(Url); string name = uri.Authority; @@ -51,7 +51,7 @@ namespace Oqtane.Services { name = name.Substring(0, name.Length - 1); } - return await _http.GetJsonAsync(apiurl + "/name/" + WebUtility.UrlEncode(name)); + return await _http.GetJsonAsync(apiurl + "/name/" + WebUtility.UrlEncode(name) + "?lastsyncdate=" + LastSyncDate.ToString("yyyyMMddHHmmssfff")); } public async Task AddAliasAsync(Alias alias) diff --git a/Oqtane.Client/Services/Interfaces/IAliasService.cs b/Oqtane.Client/Services/Interfaces/IAliasService.cs index 9ecfa221..1c94d4bd 100644 --- a/Oqtane.Client/Services/Interfaces/IAliasService.cs +++ b/Oqtane.Client/Services/Interfaces/IAliasService.cs @@ -1,4 +1,5 @@ using Oqtane.Models; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -10,7 +11,7 @@ namespace Oqtane.Services Task GetAliasAsync(int AliasId); - Task GetAliasAsync(string Url); + Task GetAliasAsync(string Url, DateTime LastSyncDate); Task AddAliasAsync(Alias Alias); diff --git a/Oqtane.Client/Shared/PageState.cs b/Oqtane.Client/Shared/PageState.cs index c8003159..fc2eec10 100644 --- a/Oqtane.Client/Shared/PageState.cs +++ b/Oqtane.Client/Shared/PageState.cs @@ -17,5 +17,6 @@ namespace Oqtane.Shared public int ModuleId { get; set; } public string Action { get; set; } public bool EditMode { get; set; } + public DateTime LastSyncDate { get; set; } } } diff --git a/Oqtane.Client/Shared/SiteRouter.razor b/Oqtane.Client/Shared/SiteRouter.razor index d6b99d02..95518631 100644 --- a/Oqtane.Client/Shared/SiteRouter.razor +++ b/Oqtane.Client/Shared/SiteRouter.razor @@ -66,16 +66,17 @@ private async Task Refresh() { - Alias alias; + Alias alias = null; Site site; List pages; Page page; - User user; + User user = null; List modules; int moduleid = -1; string action = ""; bool editmode = false; Reload reload = Reload.None; + DateTime lastsyncdate = DateTime.Now; // get Url path and querystring ( and remove anchors ) string path = new Uri(_absoluteUri).PathAndQuery.Substring(1); @@ -99,26 +100,15 @@ if (PageState != null) { editmode = PageState.EditMode; + lastsyncdate = PageState.LastSyncDate; + user = PageState.User; } - if (PageState == null || reload == Reload.Application) - { - alias = null; - } - else - { - alias = PageState.Alias; - } + alias = await AliasService.GetAliasAsync(_absoluteUri, lastsyncdate); + SiteState.Alias = alias; // set state for services + lastsyncdate = alias.SyncDate; - // check if site has changed - Alias current = await AliasService.GetAliasAsync(_absoluteUri); - if (alias == null || current.Name != alias.Name) - { - alias = current; - SiteState.Alias = alias; // set state for services - reload = Reload.Site; - } - if (PageState == null || reload <= Reload.Site) + if (PageState == null || alias.SiteId != PageState.Alias.SiteId) { site = await SiteService.GetSiteAsync(alias.SiteId, alias); } @@ -128,11 +118,34 @@ } if (site != null) { + // process sync events + if (alias.SyncEvents.Any()) + { + if (PageState != null && alias.SyncEvents.Exists(item => item.EntityName == "Page" && item.EntityId == PageState.Page.PageId)) + { + reload = Reload.Page; + } + if (alias.SyncEvents.Exists(item => item.EntityName == "Site" && item.EntityId == alias.SiteId)) + { + reload = Reload.Site; + } + if (user != null && alias.SyncEvents.Exists(item => item.EntityName == "User" && item.EntityId == user.UserId)) + { + reload = Reload.Site; + } + } + if (PageState == null || reload >= Reload.Site) { #if WASM ModuleDefinitionService.LoadModuleDefinitionsAsync(site.SiteId); // download assemblies to browser when running client-side #endif + + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + if (authState.User.Identity.IsAuthenticated) + { + user = await UserService.GetUserAsync(authState.User.Identity.Name, site.SiteId); + } pages = await PageService.GetPagesAsync(site.SiteId); } else @@ -249,6 +262,7 @@ } pagestate.Modules = modules; pagestate.EditMode = editmode; + pagestate.LastSyncDate = lastsyncdate; OnStateChange?.Invoke(pagestate); } diff --git a/Oqtane.Server/Controllers/AliasController.cs b/Oqtane.Server/Controllers/AliasController.cs index 332a8e3e..e8242bee 100644 --- a/Oqtane.Server/Controllers/AliasController.cs +++ b/Oqtane.Server/Controllers/AliasController.cs @@ -8,6 +8,7 @@ using Oqtane.Infrastructure; using System.Linq; using System; using System.Net; +using System.Globalization; namespace Oqtane.Controllers { @@ -15,11 +16,13 @@ namespace Oqtane.Controllers public class AliasController : Controller { private readonly IAliasRepository _aliases; + private readonly ISyncManager _syncManager; private readonly ILogManager _logger; - public AliasController(IAliasRepository aliases, ILogManager logger) + public AliasController(IAliasRepository aliases, ISyncManager syncManager, ILogManager logger) { _aliases = aliases; + _syncManager = syncManager; _logger = logger; } @@ -39,9 +42,9 @@ namespace Oqtane.Controllers return _aliases.GetAlias(id); } - // GET api//name/localhost:12345 + // GET api//name/localhost:12345?lastsyncdate=yyyyMMddHHmmssfff [HttpGet("name/{name}")] - public Alias Get(string name) + public Alias Get(string name, string lastsyncdate) { name = WebUtility.UrlDecode(name); List aliases = _aliases.GetAliases().ToList(); @@ -57,6 +60,11 @@ namespace Oqtane.Controllers // use first alias if name does not exist alias = aliases.FirstOrDefault(); } + + // get sync events + alias.SyncDate = DateTime.Now; + alias.SyncEvents = _syncManager.GetSyncEvents(DateTime.ParseExact(lastsyncdate, "yyyyMMddHHmmssfff", CultureInfo.InvariantCulture)); + return alias; } diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index ad404f8f..09025b22 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -17,14 +17,16 @@ namespace Oqtane.Controllers private readonly IModuleRepository _modules; private readonly IPageModuleRepository _pageModules; private readonly IUserPermissions _userPermissions; + private readonly ISyncManager _syncManager; private readonly ILogManager _logger; - public PageController(IPageRepository pages, IModuleRepository modules, IPageModuleRepository pageModules, IUserPermissions userPermissions, ILogManager logger) + public PageController(IPageRepository pages, IModuleRepository modules, IPageModuleRepository pageModules, IUserPermissions userPermissions, ISyncManager syncManager, ILogManager logger) { _pages = pages; _modules = modules; _pageModules = pageModules; _userPermissions = userPermissions; + _syncManager = syncManager; _logger = logger; } @@ -88,6 +90,7 @@ namespace Oqtane.Controllers if (_userPermissions.IsAuthorized(User, "Edit", permissions)) { Page = _pages.AddPage(Page); + _syncManager.AddSyncEvent("Site", Page.SiteId); _logger.Log(LogLevel.Information, this, LogFunction.Create, "Page Added {Page}", Page); } else diff --git a/Oqtane.Server/Infrastructure/Interfaces/ISyncManager.cs b/Oqtane.Server/Infrastructure/Interfaces/ISyncManager.cs new file mode 100644 index 00000000..f5bee6df --- /dev/null +++ b/Oqtane.Server/Infrastructure/Interfaces/ISyncManager.cs @@ -0,0 +1,13 @@ +using Oqtane.Models; +using Oqtane.Shared; +using System; +using System.Collections.Generic; + +namespace Oqtane.Infrastructure +{ + public interface ISyncManager + { + List GetSyncEvents(DateTime LastSyncDate); + void AddSyncEvent(string EntityName, int EntityId); + } +} diff --git a/Oqtane.Server/Infrastructure/SyncManager.cs b/Oqtane.Server/Infrastructure/SyncManager.cs new file mode 100644 index 00000000..5ae9f5cc --- /dev/null +++ b/Oqtane.Server/Infrastructure/SyncManager.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Models; +using Oqtane.Repository; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Oqtane.Infrastructure +{ + public class SyncManager : ISyncManager + { + private readonly IServiceScopeFactory ServiceScopeFactory; + private List SyncEvents { get; set; } + + public SyncManager(IServiceScopeFactory ServiceScopeFactory) + { + this.ServiceScopeFactory = ServiceScopeFactory; + SyncEvents = new List(); + } + + private int TenantId + { + get + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + return scope.ServiceProvider.GetRequiredService().GetTenant().TenantId; + } + } + } + + public List GetSyncEvents(DateTime LastSyncDate) + { + return SyncEvents.Where(item => item.TenantId == TenantId && item.ModifiedOn >= LastSyncDate).ToList(); + } + + public void AddSyncEvent(string EntityName, int EntityId) + { + SyncEvents.Add(new SyncEvent { TenantId = TenantId, EntityName = EntityName, EntityId = EntityId, ModifiedOn = DateTime.Now }); + // trim sync events + SyncEvents.RemoveAll(item => item.ModifiedOn < DateTime.Now.AddHours(-1)); + } + } +} diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 2ea27dd4..08a2a24b 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -162,6 +162,7 @@ namespace Oqtane.Server // register singleton scoped core services services.AddSingleton(Configuration); services.AddSingleton(); + services.AddSingleton(); // register transient scoped core services services.AddTransient(); diff --git a/Oqtane.Shared/Models/Alias.cs b/Oqtane.Shared/Models/Alias.cs index 2d49f21a..5e54e382 100644 --- a/Oqtane.Shared/Models/Alias.cs +++ b/Oqtane.Shared/Models/Alias.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; namespace Oqtane.Models @@ -15,6 +16,11 @@ namespace Oqtane.Models public string ModifiedBy { get; set; } public DateTime ModifiedOn { get; set; } + [NotMapped] + public DateTime SyncDate { get; set; } + [NotMapped] + public List SyncEvents { get; set; } + [NotMapped] public string Path { diff --git a/Oqtane.Shared/Models/SyncEvent.cs b/Oqtane.Shared/Models/SyncEvent.cs new file mode 100644 index 00000000..1d546d86 --- /dev/null +++ b/Oqtane.Shared/Models/SyncEvent.cs @@ -0,0 +1,12 @@ +using System; + +namespace Oqtane.Models +{ + public class SyncEvent + { + public int TenantId { get; set; } + public string EntityName { get; set; } + public int EntityId { get; set; } + public DateTime ModifiedOn { get; set; } + } +}