Browse Source

Cache dependencies.

pull/405/head
Sebastian 7 years ago
parent
commit
724cec87bd
  1. 31
      src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs
  2. 3
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  3. 3
      src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs
  4. 16
      src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs
  5. 53
      src/Squidex.Web/ETagExtensions.cs
  6. 8
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  7. 29
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherReferencesTests.cs
  8. 29
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherTests.cs
  9. 12
      tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs

31
src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs

@ -61,6 +61,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (contents.Any()) if (contents.Any())
{ {
var appVersion = context.App.Version.ToString();
var cache = new Dictionary<(Guid, Status), StatusInfo>(); var cache = new Dictionary<(Guid, Status), StatusInfo>();
foreach (var content in contents) foreach (var content in contents)
@ -75,14 +77,27 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ResolveCanUpdateAsync(content, result); await ResolveCanUpdateAsync(content, result);
} }
result.CacheDependencies.Add(appVersion);
results.Add(result); 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<ContentEntity> contents, Context context) private async Task ResolveReferencesAsync(ISchemaEntity schema, IEnumerable<ContentEntity> contents, Context context)
{ {
var schema = await ContentQuery.GetSchemaOrThrowAsync(context, schemaId.ToString());
var references = await GetReferencesAsync(schema, contents, context); var references = await GetReferencesAsync(schema, contents, context);
var formatted = new Dictionary<IContentEntity, JsonObject>(); var formatted = new Dictionary<IContentEntity, JsonObject>();
@ -116,6 +129,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
var referencedSchemaId = field.Properties.SchemaId; var referencedSchemaId = field.Properties.SchemaId;
var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, referencedSchemaId.ToString()); var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, referencedSchemaId.ToString());
var schemaIdentity = referencedSchema.Id.ToString();
var schemaVersion = referencedSchema.Version.ToString();
foreach (var content in contents) foreach (var content in contents)
{ {
var fieldReference = content.ReferenceData[field.Name]; 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) catch (DomainObjectNotFoundException)

3
src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -47,5 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public bool CanUpdate { get; set; } public bool CanUpdate { get; set; }
public bool IsPending { get; set; } public bool IsPending { get; set; }
public HashSet<string> CacheDependencies { get; } = new HashSet<string>();
} }
} }

3
src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs

@ -5,11 +5,12 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public interface IEnrichedContentEntity : IContentEntity public interface IEnrichedContentEntity : IContentEntity, IEntityWithCacheDependencies
{ {
bool CanUpdate { get; } bool CanUpdate { get; }

16
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<string> CacheDependencies { get; }
}
}

53
src/Squidex.Web/ETagExtensions.cs

@ -18,45 +18,39 @@ namespace Squidex.Web
{ {
private static readonly int GuidLength = Guid.Empty.ToString().Length; private static readonly int GuidLength = Guid.Empty.ToString().Length;
public static string ToEtag<T>(this IReadOnlyList<T> items, params IEntityWithVersion[] dependencies) where T : IEntity, IEntityWithVersion public static string ToEtag<T>(this IReadOnlyList<T> items) where T : IEntity, IEntityWithVersion
{ {
using (Profiler.Trace("CalculateEtag")) using (Profiler.Trace("CalculateEtag"))
{ {
var unhashed = Unhashed(items, 0, dependencies); var unhashed = Unhashed(items, 0);
return unhashed.Sha256Base64(); return unhashed.Sha256Base64();
} }
} }
public static string ToEtag<T>(this IResultList<T> items, params IEntityWithVersion[] dependencies) where T : IEntity, IEntityWithVersion public static string ToEtag<T>(this IResultList<T> items) where T : IEntity, IEntityWithVersion
{ {
using (Profiler.Trace("CalculateEtag")) using (Profiler.Trace("CalculateEtag"))
{ {
var unhashed = Unhashed(items, items.Total, dependencies); var unhashed = Unhashed(items, items.Total);
return unhashed.Sha256Base64(); return unhashed.Sha256Base64();
} }
} }
private static string Unhashed<T>(IReadOnlyList<T> items, long total, params IEntityWithVersion[] dependencies) where T : IEntity, IEntityWithVersion private static string Unhashed<T>(IReadOnlyList<T> 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(";");
sb.Append(items[i].ToEtag());
} }
sb.Append("_");
sb.Append(total); sb.Append(total);
foreach (var dependency in dependencies)
{
sb.Append("_");
sb.Append(dependency.Version);
}
return sb.ToString(); return sb.ToString();
} }
@ -85,17 +79,32 @@ namespace Squidex.Web
return sb.ToString(); return sb.ToString();
} }
public static string ToEtag<T>(this T item, IEntityWithVersion app = null) where T : IEntity, IEntityWithVersion public static string ToEtag<T>(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>(T item, StringBuilder sb) where T : IEntity, IEntityWithVersion
{
sb.Append(item.Id);
sb.Append(";");
sb.Append(item.Version);
if (item is IEntityWithCacheDependencies withDependencies)
{ {
result += ";"; if (withDependencies.CacheDependencies != null)
result += app.Version; {
foreach (var dependency in withDependencies.CacheDependencies)
{
sb.Append(";");
sb.Append(dependency);
}
}
} }
return result;
} }
} }
} }

8
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["Surrogate-Key"] = contents.ToSurrogateKeys();
} }
Response.Headers[HeaderNames.ETag] = contents.ToEtag(App); Response.Headers[HeaderNames.ETag] = contents.ToEtag();
return Ok(response); return Ok(response);
} }
@ -176,7 +176,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys(); Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys();
} }
Response.Headers[HeaderNames.ETag] = contents.ToEtag(App, schema); Response.Headers[HeaderNames.ETag] = contents.ToEtag();
return Ok(response); return Ok(response);
} }
@ -210,7 +210,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); Response.Headers["Surrogate-Key"] = content.ToSurrogateKey();
} }
Response.Headers[HeaderNames.ETag] = content.ToEtag(App); Response.Headers[HeaderNames.ETag] = content.ToEtag();
return Ok(response); return Ok(response);
} }
@ -245,7 +245,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
Response.Headers["Surrogate-Key"] = content.ToSurrogateKey(); Response.Headers["Surrogate-Key"] = content.ToSurrogateKey();
} }
Response.Headers[HeaderNames.ETag] = content.ToEtag(App); Response.Headers[HeaderNames.ETag] = content.ToEtag();
return Ok(response.Data); return Ok(response.Data);
} }

29
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherReferencesTests.cs

@ -76,6 +76,35 @@ namespace Squidex.Domain.Apps.Entities.Contents
sut = new ContentEnricher(new Lazy<IContentQueryService>(() => contentQuery), contentWorkflow); sut = new ContentEnricher(new Lazy<IContentQueryService>(() => 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<Context>.Ignored, A<IReadOnlyList<Guid>>.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] [Fact]
public async Task Should_enrich_with_reference_data() public async Task Should_enrich_with_reference_data()
{ {

29
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentEnricherTests.cs

@ -9,6 +9,8 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Xunit; using Xunit;
@ -18,15 +20,40 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(); private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>();
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly Context requestContext = new Context(); private readonly ISchemaEntity schema;
private readonly Context requestContext;
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly ContentEnricher sut; private readonly ContentEnricher sut;
public ContentEnricherTests() 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<IContentQueryService>(() => contentQuery), contentWorkflow); sut = new ContentEnricher(new Lazy<IContentQueryService>(() => 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] [Fact]
public async Task Should_enrich_content_with_status_color() public async Task Should_enrich_content_with_status_color()
{ {

12
tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs

@ -49,12 +49,22 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
return schema; return schema;
} }
public static ClaimsPrincipal ApiUser(string role = null)
{
return CreateUser(role, "api");
}
public static ClaimsPrincipal FrontendUser(string role = null) 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 claimsIdentity = new ClaimsIdentity();
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, client));
if (role != null) if (role != null)
{ {

Loading…
Cancel
Save