diff --git a/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs index d03d914f5e..c71243a199 100644 --- a/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs @@ -22,22 +22,18 @@ public static class DistributedCacheExtensions #region UserCacheRefresher - public static void RemoveUserCache(this DistributedCache dc, int userId) - => dc.Remove(UserCacheRefresher.UniqueId, userId); - public static void RemoveUserCache(this DistributedCache dc, IEnumerable users) => dc.Remove(UserCacheRefresher.UniqueId, users.Select(x => x.Id).Distinct().ToArray()); - public static void RefreshUserCache(this DistributedCache dc, int userId) - => dc.Refresh(UserCacheRefresher.UniqueId, userId); - public static void RefreshUserCache(this DistributedCache dc, IEnumerable users) { - foreach (IUser user in users) + IEnumerable payloads = users.Select(x => new UserCacheRefresher.JsonPayload() { - dc.Refresh(UserCacheRefresher.UniqueId, user.Key); - dc.Refresh(UserCacheRefresher.UniqueId, user.Id); - } + Id = x.Id, + Key = x.Key, + }); + + dc.RefreshByPayload(UserCacheRefresher.UniqueId, payloads); } public static void RefreshAllUserCache(this DistributedCache dc) diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/UserCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/UserCacheRefresher.cs index 094bb11a4a..5e9ce4916c 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/UserCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/UserCacheRefresher.cs @@ -2,27 +2,29 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Cache; -public sealed class UserCacheRefresher : CacheRefresherBase +public sealed class UserCacheRefresher : PayloadCacheRefresherBase { - #region Define - - public static readonly Guid UniqueId = Guid.Parse("E057AF6D-2EE6-41F4-8045-3694010F0AA6"); - - public UserCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) - : base(appCaches, eventAggregator, factory) + public UserCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory) + : base(appCaches, serializer, eventAggregator, factory) { } + public static readonly Guid UniqueId = Guid.Parse("E057AF6D-2EE6-41F4-8045-3694010F0AA6"); + public override Guid RefresherUniqueId => UniqueId; public override string Name => "User Cache Refresher"; - #endregion + public record JsonPayload + { + public int Id { get; init; } - #region Refresher + public Guid Key { get; init; } + } public override void RefreshAll() { @@ -30,20 +32,28 @@ public sealed class UserCacheRefresher : CacheRefresherBase userCache = AppCaches.IsolatedCaches.Get(); - if (userCache.Success) - { - userCache.Result?.Clear(RepositoryCacheKeys.GetKey(id)); - userCache.Result?.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + id); - userCache.Result?.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + id); - userCache.Result?.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id); - userCache.Result?.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + id); - } - - base.Refresh(id); + ClearCache(payloads); + base.Refresh(payloads); } - #endregion + private void ClearCache(params JsonPayload[] payloads) + { + foreach (JsonPayload p in payloads) + { + Attempt userCache = AppCaches.IsolatedCaches.Get(); + if (!userCache.Success) + { + continue; + } + + userCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Key)); + userCache.Result?.Clear(RepositoryCacheKeys.GetKey(p.Id)); + userCache.Result?.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + p.Key); + userCache.Result?.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + p.Key); + userCache.Result?.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + p.Key); + userCache.Result?.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + p.Key); + } + } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs index 0f0bdc9626..f0713f00be 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs @@ -20,6 +20,15 @@ public interface IUserRepository : IReadWriteQueryRepository /// bool ExistsByUserName(string username); + /// + /// Returns a user by id + /// + /// + /// + /// A cached instance + /// + IUser? Get(int id); + /// /// Checks if a user with the login exists /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 44750f3293..332339e49c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -20,6 +21,7 @@ using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; /// @@ -36,6 +38,8 @@ internal class UserRepository : EntityRepositoryBase, IUserReposito private bool _passwordConfigInitialized; private readonly object _sqliteValidateSessionLock = new(); private readonly IDictionary _permissionMappers; + private readonly IAppPolicyCache _globalCache; + private readonly IScopeAccessor _scopeAccessor; /// /// Initializes a new instance of the class. @@ -52,6 +56,7 @@ internal class UserRepository : EntityRepositoryBase, IUserReposito /// The JSON serializer. /// State of the runtime. /// The permission mappers. + /// The app policy cache. /// /// mapperCollection /// or @@ -68,15 +73,18 @@ internal class UserRepository : EntityRepositoryBase, IUserReposito IOptions passwordConfiguration, IJsonSerializer jsonSerializer, IRuntimeState runtimeState, - IEnumerable permissionMappers) + IEnumerable permissionMappers, + IAppPolicyCache globalCache) : base(scopeAccessor, appCaches, logger) { + _scopeAccessor = scopeAccessor; _mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection)); _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); _passwordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); _jsonSerializer = jsonSerializer; _runtimeState = runtimeState; + _globalCache = globalCache; _permissionMappers = permissionMappers.ToDictionary(x => x.Context); } @@ -917,6 +925,39 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 return Database.ExecuteScalar(sql) > 0; } + // This is a bit hacky, as we're stealing some of the cache implementation, so we also can cache user by id + // We do however need this, as all content have creatorId (as int) and thus when we index content + // this gets called for each content item, and we need to cache the user to avoid a lot of db calls + // TODO: Remove this once CreatorId gets migrated to a key. + public IUser? Get(int id) + { + string cacheKey = RepositoryCacheKeys.GetKey(id); + IUser? cachedUser = IsolatedCache.GetCacheItem(cacheKey); + if (cachedUser is not null) + { + return cachedUser; + } + + Sql sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.Id == id); + + List? dtos = Database.Fetch(sql); + + if (dtos.Count == 0) + { + return null; + } + + PerformGetReferencedDtos(dtos); + + IUser user = UserFactory.BuildEntity(_globalSettings, dtos[0], _permissionMappers); + IsolatedCache.Insert(cacheKey, () => user, TimeSpan.FromMinutes(5), true); + + return user; + } + public bool ExistsByLogin(string login) { Sql sql = SqlContext.Sql() diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index abda8d139b..23ae35cb40 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -249,8 +249,7 @@ public class BackOfficeUserStore : try { - IQuery query = _scopeProvider.CreateQuery().Where(x => x.Id == id); - return Task.FromResult(_userRepository.Get(query).FirstOrDefault()); + return Task.FromResult(_userRepository.Get(id)); } catch (DbException) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs index a5ed0b40ef..8cd5b9ae71 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs @@ -40,6 +40,8 @@ public class UserRepositoryTest : UmbracoIntegrationTest private IMediaRepository MediaRepository => GetRequiredService(); private IEnumerable PermissionMappers => GetRequiredService>(); + private IAppPolicyCache AppPolicyCache => GetRequiredService(); + private UserRepository CreateRepository(ICoreScopeProvider provider) { var accessor = (IScopeAccessor)provider; @@ -54,7 +56,8 @@ public class UserRepositoryTest : UmbracoIntegrationTest Options.Create(new UserPasswordConfigurationSettings()), new SystemTextJsonSerializer(), mockRuntimeState.Object, - PermissionMappers); + PermissionMappers, + AppPolicyCache); return repository; } @@ -161,7 +164,8 @@ public class UserRepositoryTest : UmbracoIntegrationTest Options.Create(new UserPasswordConfigurationSettings()), new SystemTextJsonSerializer(), mockRuntimeState.Object, - PermissionMappers); + PermissionMappers, + AppPolicyCache); repository2.Delete(user);