From 724cec87bd8d9c96e18f638d3c22de89df77b8a2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 31 Jul 2019 23:42:15 +0200 Subject: [PATCH] Cache dependencies. --- .../Contents/ContentEnricher.cs | 31 ++++++++--- .../Contents/ContentEntity.cs | 3 ++ .../Contents/IEnrichedContentEntity.cs | 3 +- .../IEntityWithCacheDependencies.cs | 16 ++++++ src/Squidex.Web/ETagExtensions.cs | 53 +++++++++++-------- .../Contents/ContentsController.cs | 8 +-- .../ContentEnricherReferencesTests.cs | 29 ++++++++++ .../Contents/ContentEnricherTests.cs | 29 +++++++++- .../TestHelpers/Mocks.cs | 12 ++++- 9 files changed, 149 insertions(+), 35 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs index 841b06e3c..69f044b87 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs @@ -61,6 +61,8 @@ namespace Squidex.Domain.Apps.Entities.Contents if (contents.Any()) { + var appVersion = context.App.Version.ToString(); + var cache = new Dictionary<(Guid, Status), StatusInfo>(); foreach (var content in contents) @@ -75,14 +77,27 @@ namespace Squidex.Domain.Apps.Entities.Contents await ResolveCanUpdateAsync(content, result); } + result.CacheDependencies.Add(appVersion); + results.Add(result); } - if (ShouldEnrichWithReferences(context)) + foreach (var group in results.GroupBy(x => x.SchemaId.Id)) { - foreach (var group in results.GroupBy(x => x.SchemaId.Id)) + var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); + + var schemaIdentity = schema.Id.ToString(); + var schemaVersion = schema.Version.ToString(); + + foreach (var content in group) + { + content.CacheDependencies.Add(schemaIdentity); + content.CacheDependencies.Add(schemaVersion); + } + + if (ShouldEnrichWithReferences(context)) { - await ResolveReferencesAsync(group.Key, group, context); + await ResolveReferencesAsync(schema, group, context); } } } @@ -91,10 +106,8 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - private async Task ResolveReferencesAsync(Guid schemaId, IEnumerable contents, Context context) + private async Task ResolveReferencesAsync(ISchemaEntity schema, IEnumerable contents, Context context) { - var schema = await ContentQuery.GetSchemaOrThrowAsync(context, schemaId.ToString()); - var references = await GetReferencesAsync(schema, contents, context); var formatted = new Dictionary(); @@ -116,6 +129,9 @@ namespace Squidex.Domain.Apps.Entities.Contents var referencedSchemaId = field.Properties.SchemaId; var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, referencedSchemaId.ToString()); + var schemaIdentity = referencedSchema.Id.ToString(); + var schemaVersion = referencedSchema.Version.ToString(); + foreach (var content in contents) { var fieldReference = content.ReferenceData[field.Name]; @@ -146,6 +162,9 @@ namespace Squidex.Domain.Apps.Entities.Contents } } } + + content.CacheDependencies.Add(schemaIdentity); + content.CacheDependencies.Add(schemaVersion); } } catch (DomainObjectNotFoundException) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index 31d6de3b8..40bec7049 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; @@ -47,5 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public bool CanUpdate { get; set; } public bool IsPending { get; set; } + + public HashSet CacheDependencies { get; } = new HashSet(); } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs index c4f6580e8..a9ebc61f2 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs @@ -5,11 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using Squidex.Domain.Apps.Core.Contents; namespace Squidex.Domain.Apps.Entities.Contents { - public interface IEnrichedContentEntity : IContentEntity + public interface IEnrichedContentEntity : IContentEntity, IEntityWithCacheDependencies { bool CanUpdate { get; } diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs b/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs new file mode 100644 index 000000000..512a2f4c6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IEntityWithCacheDependencies + { + HashSet CacheDependencies { get; } + } +} \ No newline at end of file diff --git a/src/Squidex.Web/ETagExtensions.cs b/src/Squidex.Web/ETagExtensions.cs index d83943222..bd6e74f58 100644 --- a/src/Squidex.Web/ETagExtensions.cs +++ b/src/Squidex.Web/ETagExtensions.cs @@ -18,45 +18,39 @@ namespace Squidex.Web { private static readonly int GuidLength = Guid.Empty.ToString().Length; - public static string ToEtag(this IReadOnlyList items, params IEntityWithVersion[] dependencies) where T : IEntity, IEntityWithVersion + public static string ToEtag(this IReadOnlyList items) where T : IEntity, IEntityWithVersion { using (Profiler.Trace("CalculateEtag")) { - var unhashed = Unhashed(items, 0, dependencies); + var unhashed = Unhashed(items, 0); return unhashed.Sha256Base64(); } } - public static string ToEtag(this IResultList items, params IEntityWithVersion[] dependencies) where T : IEntity, IEntityWithVersion + public static string ToEtag(this IResultList items) where T : IEntity, IEntityWithVersion { using (Profiler.Trace("CalculateEtag")) { - var unhashed = Unhashed(items, items.Total, dependencies); + var unhashed = Unhashed(items, items.Total); return unhashed.Sha256Base64(); } } - private static string Unhashed(IReadOnlyList items, long total, params IEntityWithVersion[] dependencies) where T : IEntity, IEntityWithVersion + private static string Unhashed(IReadOnlyList items, long total) where T : IEntity, IEntityWithVersion { - var sb = new StringBuilder((items.Count * (GuidLength + 8)) + 10); + var sb = new StringBuilder(); - for (var i = 0; i < items.Count; i++) + foreach (var item in items) { + AppendItem(item, sb); + sb.Append(";"); - sb.Append(items[i].ToEtag()); } - sb.Append("_"); sb.Append(total); - foreach (var dependency in dependencies) - { - sb.Append("_"); - sb.Append(dependency.Version); - } - return sb.ToString(); } @@ -85,17 +79,32 @@ namespace Squidex.Web return sb.ToString(); } - public static string ToEtag(this T item, IEntityWithVersion app = null) where T : IEntity, IEntityWithVersion + public static string ToEtag(this T item) where T : IEntity, IEntityWithVersion { - var result = $"{item.Id};{item.Version}"; + var sb = new StringBuilder(); + + AppendItem(item, sb); - if (app != null) + return sb.ToString(); + } + + private static void AppendItem(T item, StringBuilder sb) where T : IEntity, IEntityWithVersion + { + sb.Append(item.Id); + sb.Append(";"); + sb.Append(item.Version); + + if (item is IEntityWithCacheDependencies withDependencies) { - result += ";"; - result += app.Version; + if (withDependencies.CacheDependencies != null) + { + foreach (var dependency in withDependencies.CacheDependencies) + { + sb.Append(";"); + sb.Append(dependency); + } + } } - - return result; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 3024549c2..efbfe5454 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -137,7 +137,7 @@ namespace Squidex.Areas.Api.Controllers.Contents Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); } - Response.Headers[HeaderNames.ETag] = contents.ToEtag(App); + Response.Headers[HeaderNames.ETag] = contents.ToEtag(); return Ok(response); } @@ -176,7 +176,7 @@ namespace Squidex.Areas.Api.Controllers.Contents Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); } - Response.Headers[HeaderNames.ETag] = contents.ToEtag(App, schema); + Response.Headers[HeaderNames.ETag] = contents.ToEtag(); return Ok(response); } @@ -210,7 +210,7 @@ namespace Squidex.Areas.Api.Controllers.Contents Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); } - Response.Headers[HeaderNames.ETag] = content.ToEtag(App); + Response.Headers[HeaderNames.ETag] = content.ToEtag(); return Ok(response); } @@ -245,7 +245,7 @@ namespace Squidex.Areas.Api.Controllers.Contents Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); } - Response.Headers[HeaderNames.ETag] = content.ToEtag(App); + Response.Headers[HeaderNames.ETag] = content.ToEtag(); return Ok(response.Data); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherReferencesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherReferencesTests.cs index 22ddb944d..ca4337237 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherReferencesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherReferencesTests.cs @@ -76,6 +76,35 @@ namespace Squidex.Domain.Apps.Entities.Contents sut = new ContentEnricher(new Lazy(() => contentQuery), contentWorkflow); } + [Fact] + public async Task Should_add_referenced_id_as_dependency() + { + var ref1_1 = CreateRefContent(Guid.NewGuid(), "ref1_1", 13); + var ref1_2 = CreateRefContent(Guid.NewGuid(), "ref1_2", 17); + var ref2_1 = CreateRefContent(Guid.NewGuid(), "ref2_1", 23); + var ref2_2 = CreateRefContent(Guid.NewGuid(), "ref2_2", 29); + + var source = new IContentEntity[] + { + CreateContent(new Guid[] { ref1_1.Id }, new Guid[] { ref2_1.Id }), + CreateContent(new Guid[] { ref1_2.Id }, new Guid[] { ref2_2.Id }) + }; + + A.CallTo(() => contentQuery.QueryAsync(A.Ignored, A>.That.Matches(x => x.Count == 4))) + .Returns(ResultList.CreateFrom(4, ref1_1, ref1_2, ref2_1, ref2_2)); + + var enriched = await sut.EnrichAsync(source, requestContext); + + var enriched1 = enriched.ElementAt(0); + var enriched2 = enriched.ElementAt(1); + + Assert.Contains(refSchemaId1.Id.ToString(), enriched1.CacheDependencies); + Assert.Contains(refSchemaId2.Id.ToString(), enriched1.CacheDependencies); + + Assert.Contains(refSchemaId1.Id.ToString(), enriched2.CacheDependencies); + Assert.Contains(refSchemaId2.Id.ToString(), enriched2.CacheDependencies); + } + [Fact] public async Task Should_enrich_with_reference_data() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherTests.cs index c8227ef60..d9461caad 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherTests.cs @@ -9,6 +9,8 @@ using System; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Xunit; @@ -18,15 +20,40 @@ namespace Squidex.Domain.Apps.Entities.Contents { private readonly IContentWorkflow contentWorkflow = A.Fake(); private readonly IContentQueryService contentQuery = A.Fake(); - private readonly Context requestContext = new Context(); + private readonly ISchemaEntity schema; + private readonly Context requestContext; + private readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); private readonly ContentEnricher sut; public ContentEnricherTests() { + requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); + + schema = Mocks.Schema(appId, schemaId); + + A.CallTo(() => contentQuery.GetSchemaOrThrowAsync(requestContext, schemaId.Id.ToString())) + .Returns(schema); + sut = new ContentEnricher(new Lazy(() => contentQuery), contentWorkflow); } + [Fact] + public async Task Should_add_app_version_and_schema_as_dependency() + { + var source = new ContentEntity { Status = Status.Published, SchemaId = schemaId }; + + A.CallTo(() => contentWorkflow.GetInfoAsync(source)) + .Returns(new StatusInfo(Status.Published, StatusColors.Published)); + + var result = await sut.EnrichAsync(source, requestContext); + + Assert.Contains(requestContext.App.Version.ToString(), result.CacheDependencies); + + Assert.Contains(schema.Id.ToString(), result.CacheDependencies); + Assert.Contains(schema.Version.ToString(), result.CacheDependencies); + } + [Fact] public async Task Should_enrich_content_with_status_color() { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs index 6ead64d13..3aa99dc28 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs @@ -49,12 +49,22 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers return schema; } + public static ClaimsPrincipal ApiUser(string role = null) + { + return CreateUser(role, "api"); + } + public static ClaimsPrincipal FrontendUser(string role = null) + { + return CreateUser(role, DefaultClients.Frontend); + } + + private static ClaimsPrincipal CreateUser(string role, string client) { var claimsIdentity = new ClaimsIdentity(); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); + claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, client)); if (role != null) {