use deep cloning to not muttate cache

This commit is contained in:
sbwalker 2024-09-19 09:41:11 -04:00
parent f2c8d80ff8
commit 78177f7890
7 changed files with 275 additions and 98 deletions

View File

@ -441,7 +441,7 @@ namespace Oqtane.Repository
pageModule.Module.PermissionList = new List<Permission>(); pageModule.Module.PermissionList = new List<Permission>();
foreach (var permission in pageTemplateModule.PermissionList) foreach (var permission in pageTemplateModule.PermissionList)
{ {
pageModule.Module.PermissionList.Add(permission.Clone(permission)); pageModule.Module.PermissionList.Add(permission.Clone());
} }
pageModule.Module.AllPages = false; pageModule.Module.AllPages = false;
pageModule.Module.IsDeleted = false; pageModule.Module.IsDeleted = false;

View File

@ -71,7 +71,7 @@ namespace Oqtane.Services
}); });
// clone object so that cache is not mutated // clone object so that cache is not mutated
site = site.Clone(site); site = site.Clone();
// trim site settings based on user permissions // trim site settings based on user permissions
site.Settings = site.Settings site.Settings = site.Settings
@ -256,25 +256,28 @@ namespace Oqtane.Services
public Task<List<Module>> GetModulesAsync(int siteId, int pageId) public Task<List<Module>> GetModulesAsync(int siteId, int pageId)
{ {
var alias = _tenantManager.GetAlias(); var alias = _tenantManager.GetAlias();
var sitemodules = _cache.GetOrCreate($"modules:{alias.SiteKey}", entry => var modules = _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
{ {
entry.SlidingExpiration = TimeSpan.FromMinutes(30); entry.SlidingExpiration = TimeSpan.FromMinutes(30);
return GetPageModules(siteId); return GetPageModules(siteId);
}); });
// clone object so that cache is not mutated
modules = modules.ConvertAll(module => module.Clone());
// trim modules for current page based on user permissions // trim modules for current page based on user permissions
var modules = new List<Module>(); var pagemodules = new List<Module>();
foreach (Module module in sitemodules.Where(item => (item.PageId == pageId || pageId == -1) && !item.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, item.PermissionList))) foreach (Module module in modules.Where(item => (item.PageId == pageId || pageId == -1) && !item.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, item.PermissionList)))
{ {
if (Utilities.IsEffectiveAndNotExpired(module.EffectiveDate, module.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList)) if (Utilities.IsEffectiveAndNotExpired(module.EffectiveDate, module.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList))
{ {
module.Settings = module.Settings module.Settings = module.Settings
.Where(item => !item.Value.StartsWith(_private) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList)) .Where(item => !item.Value.StartsWith(_private) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList))
.ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, "")); .ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, ""));
modules.Add(module); pagemodules.Add(module);
} }
} }
return Task.FromResult(modules); return Task.FromResult(pagemodules);
} }
private List<Module> GetPageModules(int siteId) private List<Module> GetPageModules(int siteId)

View File

@ -1,4 +1,3 @@
using System;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace Oqtane.Models namespace Oqtane.Models
@ -40,5 +39,18 @@ namespace Oqtane.Models
/// Version of the satellite assembly /// Version of the satellite assembly
/// </summary> /// </summary>
public string Version { get; set; } public string Version { get; set; }
public Language Clone()
{
return new Language
{
LanguageId = LanguageId,
SiteId = SiteId,
Name = Name,
Code = Code,
IsDefault = IsDefault,
Version = Version
};
}
} }
} }

View File

@ -2,6 +2,7 @@ using Oqtane.Shared;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -28,11 +29,18 @@ namespace Oqtane.Models
public string ModuleDefinitionName { get; set; } public string ModuleDefinitionName { get; set; }
/// <summary> /// <summary>
/// Determines if this Module Instance should be shown on all pages of the current <see cref="Site"/> /// Determines if this module should be shown on all pages of the current <see cref="Site"/>
/// </summary> /// </summary>
public bool AllPages { get; set; } public bool AllPages { get; set; }
#region IDeletable Properties (note that these are NotMapped and are only used for storing PageModule properties)
/// <summary>
/// Reference to the <see cref="ModuleDefinition"/> used for this module.
/// </summary>
[NotMapped]
public ModuleDefinition ModuleDefinition { get; set; }
#region IDeletable Properties
[NotMapped] [NotMapped]
public string DeletedBy { get; set; } public string DeletedBy { get; set; }
@ -40,17 +48,26 @@ namespace Oqtane.Models
public DateTime? DeletedOn { get; set; } public DateTime? DeletedOn { get; set; }
[NotMapped] [NotMapped]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }
#endregion #endregion
/// <summary>
/// list of permissions for this module
/// </summary>
[NotMapped] [NotMapped]
public List<Permission> PermissionList { get; set; } public List<Permission> PermissionList { get; set; }
/// <summary>
/// List of settings for this module
/// </summary>
[NotMapped] [NotMapped]
public Dictionary<string, string> Settings { get; set; } public Dictionary<string, string> Settings { get; set; }
#region PageModule properties #region PageModule properties
/// <summary>
/// The id of the PageModule instance
/// </summary>
[NotMapped] [NotMapped]
public int PageModuleId { get; set; } public int PageModuleId { get; set; }
@ -60,24 +77,39 @@ namespace Oqtane.Models
[NotMapped] [NotMapped]
public int PageId { get; set; } public int PageId { get; set; }
/// <summary>
/// Title of the pagemodule instance
/// </summary>
[NotMapped] [NotMapped]
public string Title { get; set; } public string Title { get; set; }
/// <summary> /// <summary>
/// The Pane this module is shown in. /// The pane where this pagemodule instance will be injected on the page
/// </summary> /// </summary>
[NotMapped] [NotMapped]
public string Pane { get; set; } public string Pane { get; set; }
/// <summary>
/// The order of the pagemodule instance within the Pane
/// </summary>
[NotMapped] [NotMapped]
public int Order { get; set; } public int Order { get; set; }
/// <summary>
/// The container for the pagemodule instance
/// </summary>
[NotMapped] [NotMapped]
public string ContainerType { get; set; } public string ContainerType { get; set; }
/// <summary>
/// Start of when this module is visible. See also <see cref="ExpiryDate"/>
/// </summary>
[NotMapped] [NotMapped]
public DateTime? EffectiveDate { get; set; } public DateTime? EffectiveDate { get; set; }
/// <summary>
/// End of when this module is visible. See also <see cref="EffectiveDate"/>
/// </summary>
[NotMapped] [NotMapped]
public DateTime? ExpiryDate { get; set; } public DateTime? ExpiryDate { get; set; }
@ -85,38 +117,67 @@ namespace Oqtane.Models
#region SiteRouter properties #region SiteRouter properties
/// <summary>
/// Stores the type name for the module component being rendered
/// </summary>
[NotMapped] [NotMapped]
public string ModuleType { get; set; } public string ModuleType { get; set; }
/// <summary>
/// The position of the module instance in a pane
/// </summary>
[NotMapped] [NotMapped]
public int PaneModuleIndex { get; set; } public int PaneModuleIndex { get; set; }
/// <summary>
/// The number of modules in a pane
/// </summary>
[NotMapped] [NotMapped]
public int PaneModuleCount { get; set; } public int PaneModuleCount { get; set; }
/// <summary>
/// A unique id to help determine if a component should be rendered
/// </summary>
[NotMapped] [NotMapped]
public Guid RenderId { get; set; } public Guid RenderId { get; set; }
#endregion #endregion
#region ModuleDefinition #region IModuleControl properties
/// <summary> /// <summary>
/// Reference to the <see cref="ModuleDefinition"/> used for this module. /// The minimum access level to view the component being rendered
/// TODO: todoc - unclear if this is always populated
/// </summary> /// </summary>
[NotMapped] [NotMapped]
public ModuleDefinition ModuleDefinition { get; set; }
#endregion
#region IModuleControl properties
[NotMapped]
public SecurityAccessLevel SecurityAccessLevel { get; set; } public SecurityAccessLevel SecurityAccessLevel { get; set; }
/// <summary>
/// An optional title for the component
/// </summary>
[NotMapped] [NotMapped]
public string ControlTitle { get; set; } public string ControlTitle { get; set; }
/// <summary>
/// Optional mapping of Url actions to a component
/// </summary>
[NotMapped] [NotMapped]
public string Actions { get; set; } public string Actions { get; set; }
/// <summary>
/// Optionally indicate if a compoent should not be rendered with the default modal admin container
/// </summary>
[NotMapped] [NotMapped]
public bool UseAdminContainer { get; set; } public bool UseAdminContainer { get; set; }
/// <summary>
/// Optionally specify the render mode for the component (overrides the Site setting)
/// </summary>
[NotMapped] [NotMapped]
public string RenderMode{ get; set; } public string RenderMode { get; set; }
/// <summary>
/// Optionally specify id the component should be prerendered (overrides the Site setting)
/// </summary>
[NotMapped] [NotMapped]
public bool? Prerender { get; set; } public bool? Prerender { get; set; }
@ -140,5 +201,34 @@ namespace Oqtane.Models
} }
#endregion #endregion
public Module Clone()
{
return new Module
{
ModuleId = ModuleId,
SiteId = SiteId,
ModuleDefinitionName = ModuleDefinitionName,
AllPages = AllPages,
PageModuleId = PageModuleId,
PageId = PageId,
Title = Title,
Pane = Pane,
Order = Order,
ContainerType = ContainerType,
EffectiveDate = EffectiveDate,
ExpiryDate = ExpiryDate,
CreatedBy = CreatedBy,
CreatedOn = CreatedOn,
ModifiedBy = ModifiedBy,
ModifiedOn = ModifiedOn,
DeletedBy = DeletedBy,
DeletedOn = DeletedOn,
IsDeleted = IsDeleted,
ModuleDefinition = ModuleDefinition,
PermissionList = PermissionList.ConvertAll(permission => permission.Clone()),
Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value)
};
}
} }
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -75,33 +76,68 @@ namespace Oqtane.Models
public string BodyContent { get; set; } public string BodyContent { get; set; }
/// <summary> /// <summary>
/// Icon file for this page. /// Icon class name for this page
/// TODO: unclear what this is for, and what icon library is used. Probably FontAwesome?
/// </summary> /// </summary>
public string Icon { get; set; } public string Icon { get; set; }
/// <summary>
/// Indicates if this page should be included in navigation menu
/// </summary>
public bool IsNavigation { get; set; } public bool IsNavigation { get; set; }
/// <summary>
/// Indicates if this page should be clickable in navigation menu
/// </summary>
public bool IsClickable { get; set; } public bool IsClickable { get; set; }
public int? UserId { get; set; }
/// <summary> /// <summary>
/// Start of when this assignment is valid. See also <see cref="ExpiryDate"/> /// Indicates if page is personalizable ie. allows users to create custom versions of the page
/// </summary> /// </summary>
public DateTime? EffectiveDate { get; set; }
/// <summary>
/// End of when this assignment is valid. See also <see cref="EffectiveDate"/>
/// </summary>
public DateTime? ExpiryDate { get; set; }
public bool IsPersonalizable { get; set; } public bool IsPersonalizable { get; set; }
#region IDeletable Properties /// <summary>
/// Reference to the user <see cref="User"/> who owns the personalized page
public string DeletedBy { get; set; } /// </summary>
public DateTime? DeletedOn { get; set; } public int? UserId { get; set; }
public bool IsDeleted { get; set; }
#endregion
/// <summary> /// <summary>
/// List of Pane-names which this Page has. /// Start of when this page is visible. See also <see cref="ExpiryDate"/>
/// </summary>
public DateTime? EffectiveDate { get; set; }
/// <summary>
/// End of when this page is visible. See also <see cref="EffectiveDate"/>
/// </summary>
public DateTime? ExpiryDate { get; set; }
/// <summary>
/// The hierarchical level of the page
/// </summary>
[NotMapped]
public int Level { get; set; }
/// <summary>
/// Determines if there are sub-pages. True if this page has sub-pages.
/// </summary>
[NotMapped]
public bool HasChildren { get; set; }
/// <summary>
/// List of permissions for this page
/// </summary>
[NotMapped]
public List<Permission> PermissionList { get; set; }
/// <summary>
/// List of settings for this page
/// </summary>
[NotMapped]
public Dictionary<string, string> Settings { get; set; }
#region SiteRouter properties
/// <summary>
/// List of Pane names for the Theme assigned to this page
/// </summary> /// </summary>
[NotMapped] [NotMapped]
public List<string> Panes { get; set; } public List<string> Panes { get; set; }
@ -112,20 +148,15 @@ namespace Oqtane.Models
[NotMapped] [NotMapped]
public List<Resource> Resources { get; set; } public List<Resource> Resources { get; set; }
[NotMapped] #endregion
public List<Permission> PermissionList { get; set; }
[NotMapped] #region IDeletable Properties
public Dictionary<string, string> Settings { get; set; }
[NotMapped] public string DeletedBy { get; set; }
public int Level { get; set; } public DateTime? DeletedOn { get; set; }
public bool IsDeleted { get; set; }
/// <summary> #endregion
/// Determines if there are sub-pages. True if this page has sub-pages.
/// </summary>
[NotMapped]
public bool HasChildren { get; set; }
#region Deprecated Properties #region Deprecated Properties
@ -152,5 +183,42 @@ namespace Oqtane.Models
} }
#endregion #endregion
public Page Clone()
{
return new Page
{
PageId = PageId,
SiteId = SiteId,
Path = Path,
ParentId = ParentId,
Name = Name,
Title = Title,
Order = Order,
Url = Url,
ThemeType = ThemeType,
DefaultContainerType = DefaultContainerType,
HeadContent = HeadContent,
BodyContent = BodyContent,
Icon = Icon,
IsNavigation = IsNavigation,
IsClickable = IsClickable,
UserId = UserId,
IsPersonalizable = IsPersonalizable,
EffectiveDate = EffectiveDate,
ExpiryDate = ExpiryDate,
Level = Level,
HasChildren = HasChildren,
CreatedBy = CreatedBy,
CreatedOn = CreatedOn,
ModifiedBy = ModifiedBy,
ModifiedOn = ModifiedOn,
DeletedBy = DeletedBy,
DeletedOn = DeletedOn,
IsDeleted = IsDeleted,
PermissionList = PermissionList.ConvertAll(permission => permission.Clone()),
Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value)
};
}
} }
} }

View File

@ -101,17 +101,21 @@ namespace Oqtane.Models
IsAuthorized = isAuthorized; IsAuthorized = isAuthorized;
} }
public Permission Clone(Permission permission) public Permission Clone()
{ {
return new Permission return new Permission
{ {
SiteId = permission.SiteId, SiteId = SiteId,
EntityName = permission.EntityName, EntityName = EntityName,
EntityId = permission.EntityId, EntityId = EntityId,
PermissionName = permission.PermissionName, PermissionName = PermissionName,
RoleName = permission.RoleName, RoleName = RoleName,
UserId = permission.UserId, UserId = UserId,
IsAuthorized = permission.IsAuthorized IsAuthorized = IsAuthorized,
CreatedBy = CreatedBy,
CreatedOn = CreatedOn,
ModifiedBy = ModifiedBy,
ModifiedOn = ModifiedOn
}; };
} }

View File

@ -187,47 +187,47 @@ namespace Oqtane.Models
[NotMapped] [NotMapped]
public List<Theme> Themes { get; set; } public List<Theme> Themes { get; set; }
public Site Clone(Site site) public Site Clone()
{ {
return new Site return new Site
{ {
SiteId = site.SiteId, SiteId = SiteId,
TenantId = site.TenantId, TenantId = TenantId,
Name = site.Name, Name = Name,
LogoFileId = site.LogoFileId, LogoFileId = LogoFileId,
FaviconFileId = site.FaviconFileId, FaviconFileId = FaviconFileId,
DefaultThemeType = site.DefaultThemeType, DefaultThemeType = DefaultThemeType,
DefaultContainerType = site.DefaultContainerType, DefaultContainerType = DefaultContainerType,
AdminContainerType = site.AdminContainerType, AdminContainerType = AdminContainerType,
PwaIsEnabled = site.PwaIsEnabled, PwaIsEnabled = PwaIsEnabled,
PwaAppIconFileId = site.PwaAppIconFileId, PwaAppIconFileId = PwaAppIconFileId,
PwaSplashIconFileId = site.PwaSplashIconFileId, PwaSplashIconFileId = PwaSplashIconFileId,
AllowRegistration = site.AllowRegistration, AllowRegistration = AllowRegistration,
VisitorTracking = site.VisitorTracking, VisitorTracking = VisitorTracking,
CaptureBrokenUrls = site.CaptureBrokenUrls, CaptureBrokenUrls = CaptureBrokenUrls,
SiteGuid = site.SiteGuid, SiteGuid = SiteGuid,
RenderMode = site.RenderMode, RenderMode = RenderMode,
Runtime = site.Runtime, Runtime = Runtime,
Prerender = site.Prerender, Prerender = Prerender,
Hybrid = site.Hybrid, Hybrid = Hybrid,
Version = site.Version, Version = Version,
HomePageId = site.HomePageId, HomePageId = HomePageId,
HeadContent = site.HeadContent, HeadContent = HeadContent,
BodyContent = site.BodyContent, BodyContent = BodyContent,
IsDeleted = site.IsDeleted, IsDeleted = IsDeleted,
DeletedBy = site.DeletedBy, DeletedBy = DeletedBy,
DeletedOn = site.DeletedOn, DeletedOn = DeletedOn,
ImageFiles = site.ImageFiles, ImageFiles = ImageFiles,
UploadableFiles = site.UploadableFiles, UploadableFiles = UploadableFiles,
SiteTemplateType = site.SiteTemplateType, SiteTemplateType = SiteTemplateType,
CreatedBy = site.CreatedBy, CreatedBy = CreatedBy,
CreatedOn = site.CreatedOn, CreatedOn = CreatedOn,
ModifiedBy = site.ModifiedBy, ModifiedBy = ModifiedBy,
ModifiedOn = site.ModifiedOn, ModifiedOn = ModifiedOn,
Settings = site.Settings.ToDictionary(), Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value),
Pages = site.Pages.ToList(), Pages = Pages.ConvertAll(page => page.Clone()),
Languages = site.Languages.ToList(), Languages = Languages.ConvertAll(language => language.Clone()),
Themes = site.Themes.ToList() Themes = Themes
}; };
} }