From 7e3ea07718c6ef9355cfec292a4b101ee63dcffc Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 24 Apr 2018 15:56:08 +0200 Subject: [PATCH] 1. Caching for app provider 2. Paging fix 3. Removing content in references-editor fix. --- .../AppProvider.cs | 168 ++++++++++-------- .../Caching/HttpRequestCache.cs | 68 +++++++ .../Caching/IRequestCache.cs | 18 ++ .../Caching/RequestCacheExtensions.cs | 43 +++++ .../Squidex.Infrastructure.csproj | 1 + .../Config/Domain/InfrastructureServices.cs | 4 + .../shared/contents-selector.component.html | 2 +- .../shared/references-editor.component.html | 2 +- .../Caching/HttpRequestCacheTests.cs | 133 ++++++++++++++ 9 files changed, 366 insertions(+), 73 deletions(-) create mode 100644 src/Squidex.Infrastructure/Caching/HttpRequestCache.cs create mode 100644 src/Squidex.Infrastructure/Caching/IRequestCache.cs create mode 100644 src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Caching/HttpRequestCacheTests.cs diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index 739b82e64..ef9f2c419 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Orleans; @@ -27,139 +28,164 @@ namespace Squidex.Domain.Apps.Entities private readonly IGrainFactory grainFactory; private readonly IAppRepository appRepository; private readonly IRuleRepository ruleRepository; + private readonly IRequestCache requestCache; private readonly ISchemaRepository schemaRepository; public AppProvider( IGrainFactory grainFactory, IAppRepository appRepository, ISchemaRepository schemaRepository, - IRuleRepository ruleRepository) + IRuleRepository ruleRepository, + IRequestCache requestCache) { Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(appRepository, nameof(appRepository)); Guard.NotNull(schemaRepository, nameof(schemaRepository)); + Guard.NotNull(requestCache, nameof(requestCache)); Guard.NotNull(ruleRepository, nameof(ruleRepository)); this.grainFactory = grainFactory; this.appRepository = appRepository; this.schemaRepository = schemaRepository; + this.requestCache = requestCache; this.ruleRepository = ruleRepository; } - public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) + public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) { - using (Profile.Method()) + return requestCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () => { - var app = await grainFactory.GetGrain(appId).GetStateAsync(); - - if (!IsExisting(app)) + using (Profile.Method()) { - return (null, null); - } + var app = await grainFactory.GetGrain(appId).GetStateAsync(); - var schema = await grainFactory.GetGrain(id).GetStateAsync(); + if (!IsExisting(app)) + { + return (null, null); + } - if (!IsExisting(schema, false)) - { - return (null, null); - } + var schema = await grainFactory.GetGrain(id).GetStateAsync(); - return (app.Value, schema.Value); - } + if (!IsExisting(schema, false)) + { + return (null, null); + } + + return (app.Value, schema.Value); + } + }); } - public async Task GetAppAsync(string appName) + public Task GetAppAsync(string appName) { - using (Profile.Method()) + return requestCache.GetOrCreateAsync($"GetAppAsync({appName})", async () => { - var appId = await GetAppIdAsync(appName); - - if (appId == Guid.Empty) + using (Profile.Method()) { - return null; - } + var appId = await GetAppIdAsync(appName); - var app = await grainFactory.GetGrain(appId).GetStateAsync(); + if (appId == Guid.Empty) + { + return null; + } - if (!IsExisting(app)) - { - return null; - } + var app = await grainFactory.GetGrain(appId).GetStateAsync(); - return app.Value; - } + if (!IsExisting(app)) + { + return null; + } + + return app.Value; + } + }); } - public async Task GetSchemaAsync(Guid appId, string name) + public Task GetSchemaAsync(Guid appId, string name) { - using (Profile.Method()) + return requestCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () => { - var schemaId = await GetSchemaIdAsync(appId, name); - - if (schemaId == Guid.Empty) + using (Profile.Method()) { - return null; - } + var schemaId = await GetSchemaIdAsync(appId, name); - return await GetSchemaAsync(appId, schemaId, false); - } + if (schemaId == Guid.Empty) + { + return null; + } + + return await GetSchemaAsync(appId, schemaId, false); + } + }); } - public async Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) + public Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) { - using (Profile.Method()) + return requestCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () => { - var schema = await grainFactory.GetGrain(id).GetStateAsync(); - - if (!IsExisting(schema, allowDeleted) || schema.Value.AppId.Id != appId) + using (Profile.Method()) { - return null; - } + var schema = await grainFactory.GetGrain(id).GetStateAsync(); - return schema.Value; - } + if (!IsExisting(schema, allowDeleted) || schema.Value.AppId.Id != appId) + { + return null; + } + + return schema.Value; + } + }); } - public async Task> GetSchemasAsync(Guid appId) + public Task> GetSchemasAsync(Guid appId) { - using (Profile.Method()) + return requestCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () => { - var ids = await schemaRepository.QuerySchemaIdsAsync(appId); + using (Profile.Method()) + { + var ids = await schemaRepository.QuerySchemaIdsAsync(appId); - var schemas = - await Task.WhenAll( - ids.Select(id => grainFactory.GetGrain(id).GetStateAsync())); + var schemas = + await Task.WhenAll( + ids.Select(id => grainFactory.GetGrain(id).GetStateAsync())); - return schemas.Where(s => IsFound(s.Value)).Select(s => s.Value).ToList(); - } + return schemas.Where(s => IsFound(s.Value)).Select(s => s.Value).ToList(); + } + }); } - public async Task> GetRulesAsync(Guid appId) + public Task> GetRulesAsync(Guid appId) { - using (Profile.Method()) + return requestCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () => { - var ids = await ruleRepository.QueryRuleIdsAsync(appId); + using (Profile.Method()) + { + var ids = await ruleRepository.QueryRuleIdsAsync(appId); - var rules = - await Task.WhenAll( - ids.Select(id => grainFactory.GetGrain(id).GetStateAsync())); + var rules = + await Task.WhenAll( + ids.Select(id => grainFactory.GetGrain(id).GetStateAsync())); - return rules.Where(r => IsFound(r.Value)).Select(r => r.Value).ToList(); - } + return rules.Where(r => IsFound(r.Value)).Select(r => r.Value).ToList(); + } + }); } - public async Task> GetUserApps(string userId) + public Task> GetUserApps(string userId) { - using (Profile.Method()) + return requestCache.GetOrCreateAsync($"GetUserApps({userId})", async () => { - var ids = await appRepository.QueryUserAppIdsAsync(userId); + using (Profile.Method()) + { + var ids = await appRepository.QueryUserAppIdsAsync(userId); - var apps = - await Task.WhenAll( - ids.Select(id => grainFactory.GetGrain(id).GetStateAsync())); + var apps = + await Task.WhenAll( + ids.Select(id => grainFactory.GetGrain(id).GetStateAsync())); - return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList(); - } + return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList(); + } + }); } private async Task GetAppIdAsync(string name) diff --git a/src/Squidex.Infrastructure/Caching/HttpRequestCache.cs b/src/Squidex.Infrastructure/Caching/HttpRequestCache.cs new file mode 100644 index 000000000..3c21de6bb --- /dev/null +++ b/src/Squidex.Infrastructure/Caching/HttpRequestCache.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Http; + +namespace Squidex.Infrastructure.Caching +{ + public sealed class HttpRequestCache : IRequestCache + { + private readonly IHttpContextAccessor httpContextAccessor; + + public HttpRequestCache(IHttpContextAccessor httpContextAccessor) + { + Guard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); + + this.httpContextAccessor = httpContextAccessor; + } + + public void Add(object key, object value) + { + var cacheKey = GetCacheKey(key); + + var items = httpContextAccessor.HttpContext?.Items; + + if (items != null) + { + items[cacheKey] = value; + } + } + + public void Remove(object key) + { + var cacheKey = GetCacheKey(key); + + var items = httpContextAccessor.HttpContext?.Items; + + if (items != null) + { + items?.Remove(cacheKey); + } + } + + public bool TryGetValue(object key, out object value) + { + var cacheKey = GetCacheKey(key); + + var items = httpContextAccessor.HttpContext?.Items; + + if (items != null) + { + return items.TryGetValue(cacheKey, out value); + } + + value = null; + + return false; + } + + private static string GetCacheKey(object key) + { + return $"CACHE_{key}"; + } + } +} diff --git a/src/Squidex.Infrastructure/Caching/IRequestCache.cs b/src/Squidex.Infrastructure/Caching/IRequestCache.cs new file mode 100644 index 000000000..b6bb6aaf1 --- /dev/null +++ b/src/Squidex.Infrastructure/Caching/IRequestCache.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Caching +{ + public interface IRequestCache + { + void Add(object key, object value); + + void Remove(object key); + + bool TryGetValue(object key, out object value); + } +} diff --git a/src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs b/src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs new file mode 100644 index 000000000..0523b9c54 --- /dev/null +++ b/src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Caching +{ + public static class RequestCacheExtensions + { + public static async Task GetOrCreateAsync(this IRequestCache cache, object key, Func> task) + { + if (cache.TryGetValue(key, out var value) && value is T typedValue) + { + return typedValue; + } + + typedValue = await task(); + + cache.Add(key, typedValue); + + return typedValue; + } + + public static T GetOrCreate(this IRequestCache cache, object key, Func task) + { + if (cache.TryGetValue(key, out var value) && value is T typedValue) + { + return typedValue; + } + + typedValue = task(); + + cache.Add(key, typedValue); + + return typedValue; + } + } +} diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 96dcbebf2..5da83c8bf 100644 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -8,6 +8,7 @@ True + diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index b1fbb1c7d..deccd4b66 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using NodaTime; using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.UsageTracking; #pragma warning disable RECS0092 // Convert field to readonly @@ -31,6 +32,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs(c => new CachingUsageTracker(c.GetRequiredService(), c.GetRequiredService())) .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.html b/src/Squidex/app/features/content/shared/contents-selector.component.html index 2c13045ee..9199687fc 100644 --- a/src/Squidex/app/features/content/shared/contents-selector.component.html +++ b/src/Squidex/app/features/content/shared/contents-selector.component.html @@ -62,7 +62,7 @@ diff --git a/src/Squidex/app/features/content/shared/references-editor.component.html b/src/Squidex/app/features/content/shared/references-editor.component.html index 8f35f5256..caa531af3 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.html +++ b/src/Squidex/app/features/content/shared/references-editor.component.html @@ -12,7 +12,7 @@ diff --git a/tests/Squidex.Infrastructure.Tests/Caching/HttpRequestCacheTests.cs b/tests/Squidex.Infrastructure.Tests/Caching/HttpRequestCacheTests.cs new file mode 100644 index 000000000..271c7b76a --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Caching/HttpRequestCacheTests.cs @@ -0,0 +1,133 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Squidex.Infrastructure.Caching +{ + public sealed class HttpRequestCacheTests + { + private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); + private readonly IRequestCache sut; + private int called; + + public HttpRequestCacheTests() + { + sut = new HttpRequestCache(httpContextAccessor); + } + + [Fact] + public void Should_add_item_to_cache_when_context_exists() + { + SetupContext(); + + sut.Add("Key", 1); + + var found = sut.TryGetValue("Key", out var value); + + Assert.True(found); + Assert.Equal(1, value); + + sut.Remove("Key"); + + var foundAfterRemove = sut.TryGetValue("Key", out value); + + Assert.False(foundAfterRemove); + Assert.Null(value); + } + + [Fact] + public void Should_not_add_item_to_cache_when_context_not_exists() + { + SetupNoContext(); + + sut.Add("Key", 1); + + var found = sut.TryGetValue("Key", out var value); + + Assert.False(found); + Assert.Null(value); + + sut.Remove("Key"); + + var foundAfterRemove = sut.TryGetValue("Key", out value); + + Assert.False(foundAfterRemove); + Assert.Null(value); + } + + [Fact] + public void Should_call_creator_once_when_context_exists() + { + SetupContext(); + + var value1 = sut.GetOrCreate("Key", () => ++called); + var value2 = sut.GetOrCreate("Key", () => ++called); + + Assert.Equal(1, called); + Assert.Equal(1, value1); + Assert.Equal(1, value2); + } + + [Fact] + public void Should_call_creator_twice_when_context_not_exists() + { + SetupNoContext(); + + var value1 = sut.GetOrCreate("Key", () => ++called); + var value2 = sut.GetOrCreate("Key", () => ++called); + + Assert.Equal(2, called); + Assert.Equal(1, value1); + Assert.Equal(2, value2); + } + + [Fact] + public async Task Should_call_async_creator_once_when_context_exists() + { + SetupContext(); + + var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called)); + var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called)); + + Assert.Equal(1, called); + Assert.Equal(1, value1); + Assert.Equal(1, value2); + } + + [Fact] + public async Task Should_call_async_creator_twice_when_context_not_exists() + { + SetupNoContext(); + + var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called)); + var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called)); + + Assert.Equal(2, called); + Assert.Equal(1, value1); + Assert.Equal(2, value2); + } + + private void SetupNoContext() + { + A.CallTo(() => httpContextAccessor.HttpContext).Returns(null); + } + + private void SetupContext() + { + var httpItems = new Dictionary(); + var httpContext = A.Fake(); + + A.CallTo(() => httpContext.Items).Returns(httpItems); + A.CallTo(() => httpContextAccessor.HttpContext).Returns(httpContext); + } + } +}