Browse Source

Better cache dependency handling. (#483)

* Better cache dependency handling.

* Tests fixed.
pull/484/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
5becaf0360
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs
  2. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  3. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs
  4. 20
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs
  5. 11
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs
  6. 15
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs
  7. 2
      backend/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs
  8. 14
      backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs
  9. 3
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs
  10. 12
      backend/src/Squidex.Infrastructure/Caching/IRequestCache.cs
  11. 2
      backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs
  12. 85
      backend/src/Squidex.Web/ETagExtensions.cs
  13. 15
      backend/src/Squidex.Web/Extensions.cs
  14. 17
      backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs
  15. 22
      backend/src/Squidex.Web/Pipeline/CachingFilter.cs
  16. 157
      backend/src/Squidex.Web/Pipeline/CachingManager.cs
  17. 6
      backend/src/Squidex.Web/Pipeline/CachingOptions.cs
  18. 19
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  19. 1
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs
  20. 41
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  21. 14
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs
  22. 16
      backend/src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs
  23. 2
      backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  24. 7
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  25. 6
      backend/src/Squidex/Config/Web/WebServices.cs
  26. 23
      backend/src/Squidex/appsettings.json
  27. 15
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs
  28. 9
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs
  29. 10
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs
  30. 22
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs
  31. 11
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs
  32. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs
  33. 16
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs
  34. 30
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs
  35. 38
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs
  36. 220
      backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs
  37. 102
      backend/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs
  38. 6
      frontend/app/framework/angular/list-view.component.scss

11
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs

@ -11,6 +11,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -20,14 +21,17 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
private readonly ITagService tagService; private readonly ITagService tagService;
private readonly IEnumerable<IAssetMetadataSource> assetMetadataSources; private readonly IEnumerable<IAssetMetadataSource> assetMetadataSources;
private readonly IRequestCache requestCache;
public AssetEnricher(ITagService tagService, IEnumerable<IAssetMetadataSource> assetMetadataSources) public AssetEnricher(ITagService tagService, IEnumerable<IAssetMetadataSource> assetMetadataSources, IRequestCache requestCache)
{ {
Guard.NotNull(tagService); Guard.NotNull(tagService);
Guard.NotNull(assetMetadataSources); Guard.NotNull(assetMetadataSources);
Guard.NotNull(requestCache);
this.tagService = tagService; this.tagService = tagService;
this.assetMetadataSources = assetMetadataSources; this.assetMetadataSources = assetMetadataSources;
this.requestCache = requestCache;
} }
public async Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset, Context context) public async Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset, Context context)
@ -49,6 +53,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{ {
var results = assets.Select(x => SimpleMapper.Map(x, new AssetEntity())).ToList(); var results = assets.Select(x => SimpleMapper.Map(x, new AssetEntity())).ToList();
foreach (var asset in results)
{
requestCache.AddDependency(asset.Id, asset.Version);
}
if (ShouldEnrich(context)) if (ShouldEnrich(context))
{ {
await EnrichTagsAsync(results); await EnrichTagsAsync(results);

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

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
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.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
@ -55,7 +54,5 @@ 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<object?> CacheDependencies { get; set; }
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs

@ -10,7 +10,7 @@ using Squidex.Domain.Apps.Core.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public interface IEnrichedContentEntity : IContentEntity, IEntityWithCacheDependencies public interface IEnrichedContentEntity : IContentEntity
{ {
bool CanUpdate { get; } bool CanUpdate { get; }

20
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs

@ -8,11 +8,22 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{ {
public sealed class EnrichForCaching : IContentEnricherStep public sealed class EnrichForCaching : IContentEnricherStep
{ {
private readonly IRequestCache requestCache;
public EnrichForCaching(IRequestCache requestCache)
{
Guard.NotNull(requestCache);
this.requestCache = requestCache;
}
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas) public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
{ {
var app = context.App; var app = context.App;
@ -23,12 +34,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
foreach (var content in group) foreach (var content in group)
{ {
content.CacheDependencies ??= new HashSet<object?>(); requestCache.AddDependency(content.Id, content.Version);
requestCache.AddDependency(app.Id, app.Version);
content.CacheDependencies.Add(app.Id); requestCache.AddDependency(schema.Id, schema.Version);
content.CacheDependencies.Add(app.Version);
content.CacheDependencies.Add(schema.Id);
content.CacheDependencies.Add(schema.Version);
} }
} }
} }

11
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs

@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
@ -27,14 +28,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
private readonly IAssetUrlGenerator assetUrlGenerator; private readonly IAssetUrlGenerator assetUrlGenerator;
private readonly IAssetQueryService assetQuery; private readonly IAssetQueryService assetQuery;
private readonly IRequestCache requestCache;
public ResolveAssets(IAssetUrlGenerator assetUrlGenerator, IAssetQueryService assetQuery) public ResolveAssets(IAssetUrlGenerator assetUrlGenerator, IAssetQueryService assetQuery, IRequestCache requestCache)
{ {
Guard.NotNull(assetUrlGenerator); Guard.NotNull(assetUrlGenerator);
Guard.NotNull(assetQuery); Guard.NotNull(assetQuery);
Guard.NotNull(requestCache);
this.assetUrlGenerator = assetUrlGenerator; this.assetUrlGenerator = assetUrlGenerator;
this.assetQuery = assetQuery; this.assetQuery = assetQuery;
this.requestCache = requestCache;
} }
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas) public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
@ -88,10 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{ {
var url = assetUrlGenerator.GenerateUrl(referencedImage.Id.ToString()); var url = assetUrlGenerator.GenerateUrl(referencedImage.Id.ToString());
content.CacheDependencies ??= new HashSet<object?>(); requestCache.AddDependency(referencedImage.Id, referencedImage.Version);
content.CacheDependencies.Add(referencedImage.Id);
content.CacheDependencies.Add(referencedImage.Version);
fieldReference.AddJsonValue(partitionKey, JsonValue.Create(url)); fieldReference.AddJsonValue(partitionKey, JsonValue.Create(url));
} }

15
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveReferences.cs

@ -14,6 +14,7 @@ using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
@ -22,17 +23,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{ {
private static readonly ILookup<Guid, IEnrichedContentEntity> EmptyContents = Enumerable.Empty<IEnrichedContentEntity>().ToLookup(x => x.Id); private static readonly ILookup<Guid, IEnrichedContentEntity> EmptyContents = Enumerable.Empty<IEnrichedContentEntity>().ToLookup(x => x.Id);
private readonly Lazy<IContentQueryService> contentQuery; private readonly Lazy<IContentQueryService> contentQuery;
private readonly IRequestCache requestCache;
private IContentQueryService ContentQuery private IContentQueryService ContentQuery
{ {
get { return contentQuery.Value; } get { return contentQuery.Value; }
} }
public ResolveReferences(Lazy<IContentQueryService> contentQuery) public ResolveReferences(Lazy<IContentQueryService> contentQuery, IRequestCache requestCache)
{ {
Guard.NotNull(contentQuery); Guard.NotNull(contentQuery);
Guard.NotNull(requestCache);
this.contentQuery = contentQuery; this.contentQuery = contentQuery;
this.requestCache = requestCache;
} }
public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas) public async Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas)
@ -92,12 +97,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
var referencedSchema = await schemas(reference.SchemaId.Id); var referencedSchema = await schemas(reference.SchemaId.Id);
content.CacheDependencies ??= new HashSet<object?>(); requestCache.AddDependency(referencedSchema.Id, referencedSchema.Version);
requestCache.AddDependency(reference.Id, reference.Version);
content.CacheDependencies.Add(referencedSchema.Id);
content.CacheDependencies.Add(referencedSchema.Version);
content.CacheDependencies.Add(reference.Id);
content.CacheDependencies.Add(reference.Version);
var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema)); var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema));

2
backend/src/Squidex.Domain.Apps.Entities/Rules/IEnrichedRuleEntity.cs

@ -9,7 +9,7 @@ using NodaTime;
namespace Squidex.Domain.Apps.Entities.Rules namespace Squidex.Domain.Apps.Entities.Rules
{ {
public interface IEnrichedRuleEntity : IRuleEntity, IEntityWithCacheDependencies public interface IEnrichedRuleEntity : IRuleEntity
{ {
int NumSucceeded { get; } int NumSucceeded { get; }

14
backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -18,12 +19,16 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries
public sealed class RuleEnricher : IRuleEnricher public sealed class RuleEnricher : IRuleEnricher
{ {
private readonly IRuleEventRepository ruleEventRepository; private readonly IRuleEventRepository ruleEventRepository;
private readonly IRequestCache requestCache;
public RuleEnricher(IRuleEventRepository ruleEventRepository) public RuleEnricher(IRuleEventRepository ruleEventRepository, IRequestCache requestCache)
{ {
Guard.NotNull(ruleEventRepository); Guard.NotNull(ruleEventRepository);
Guard.NotNull(requestCache);
this.ruleEventRepository = ruleEventRepository; this.ruleEventRepository = ruleEventRepository;
this.requestCache = requestCache;
} }
public async Task<IEnrichedRuleEntity> EnrichAsync(IRuleEntity rule, Context context) public async Task<IEnrichedRuleEntity> EnrichAsync(IRuleEntity rule, Context context)
@ -57,6 +62,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries
foreach (var rule in group) foreach (var rule in group)
{ {
requestCache.AddDependency(rule.Id, rule.Version);
var statistic = statistics.FirstOrDefault(x => x.RuleId == rule.Id); var statistic = statistics.FirstOrDefault(x => x.RuleId == rule.Id);
if (statistic != null) if (statistic != null)
@ -65,10 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries
rule.NumFailed = statistic.NumFailed; rule.NumFailed = statistic.NumFailed;
rule.NumSucceeded = statistic.NumSucceeded; rule.NumSucceeded = statistic.NumSucceeded;
rule.CacheDependencies = new HashSet<object?> requestCache.AddDependency(rule.LastExecuted);
{
statistic.LastExecuted
};
} }
} }
} }

3
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEntity.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -40,7 +39,5 @@ namespace Squidex.Domain.Apps.Entities.Rules
public int NumFailed { get; set; } public int NumFailed { get; set; }
public Instant? LastExecuted { get; set; } public Instant? LastExecuted { get; set; }
public HashSet<object?> CacheDependencies { get; set; }
} }
} }

12
backend/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs → backend/src/Squidex.Infrastructure/Caching/IRequestCache.cs

@ -5,12 +5,14 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic; using System;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Infrastructure.Caching
{ {
public interface IEntityWithCacheDependencies public interface IRequestCache
{ {
HashSet<object?> CacheDependencies { get; } void AddDependency(Guid key, long version);
void AddDependency(object? value);
} }
} }

2
backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs

@ -71,7 +71,7 @@ namespace Squidex.Web.CommandMiddlewares
{ {
version = default; version = default;
if (httpContext.Request.Headers.TryGetHeaderString(HeaderNames.IfMatch, out var etag)) if (httpContext.Request.Headers.TryGetString(HeaderNames.IfMatch, out var etag))
{ {
if (etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) if (etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase))
{ {

85
backend/src/Squidex.Web/ETagExtensions.cs

@ -7,7 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Security.Cryptography;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
@ -16,95 +16,48 @@ namespace Squidex.Web
{ {
public static class ETagExtensions public static class ETagExtensions
{ {
private static readonly int GuidLength = Guid.Empty.ToString().Length;
public static string ToEtag<T>(this IReadOnlyList<T> items) 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); var hash = Create(items, 0);
return unhashed.Sha256Base64(); return hash;
} }
} }
public static string ToEtag<T>(this IResultList<T> items) where T : IEntity, IEntityWithVersion public static string ToEtag<T>(this IResultList<T> entities) where T : IEntity, IEntityWithVersion
{ {
using (Profiler.Trace("CalculateEtag")) using (Profiler.Trace("CalculateEtag"))
{ {
var unhashed = Unhashed(items, items.Total); var hash = Create(entities, entities.Total);
return unhashed.Sha256Base64();
}
}
private static string Unhashed<T>(IReadOnlyList<T> items, long total) where T : IEntity, IEntityWithVersion
{
var sb = new StringBuilder();
foreach (var item in items)
{
AppendItem(item, sb);
sb.Append(";"); return hash;
} }
sb.Append(total);
return sb.ToString();
} }
public static string ToSurrogateKey<T>(this T item) where T : IEntity private static string Create<T>(IReadOnlyList<T> entities, long total) where T : IEntity, IEntityWithVersion
{ {
return item.Id.ToString(); using (var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256))
}
public static string ToSurrogateKeys<T>(this IReadOnlyList<T> items) where T : IEntity
{
if (items.Count == 0)
{ {
return string.Empty; hasher.AppendData(BitConverter.GetBytes(total));
}
var sb = new StringBuilder(items.Count * (GuidLength + 1)); foreach (var item in entities)
{
hasher.AppendData(item.Id.ToByteArray());
hasher.AppendData(BitConverter.GetBytes(item.Version));
}
sb.Append(items[0].Id.ToString()); var cacheBuffer = hasher.GetHashAndReset();
var cacheString = BitConverter.ToString(cacheBuffer).Replace("-", string.Empty).ToUpperInvariant();
for (var i = 1; i < items.Count; i++) return cacheString;
{
sb.Append(" ");
sb.Append(items[i].Id.ToString());
} }
return sb.ToString();
}
public static string ToEtag<T>(this T item) where T : IEntity, IEntityWithVersion
{
var sb = new StringBuilder();
AppendItem(item, sb);
return sb.ToString();
} }
private static void AppendItem<T>(T item, StringBuilder sb) where T : IEntity, IEntityWithVersion public static string ToEtag<T>(this T entity) where T : IEntity, IEntityWithVersion
{ {
sb.Append(item.Id); return entity.Version.ToString();
sb.Append(";");
sb.Append(item.Version);
if (item is IEntityWithCacheDependencies withDependencies)
{
if (withDependencies.CacheDependencies != null)
{
foreach (var dependency in withDependencies.CacheDependencies)
{
sb.Append(";");
sb.Append(dependency);
}
}
}
} }
} }
} }

15
backend/src/Squidex.Web/Extensions.cs

@ -9,6 +9,7 @@ using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
namespace Squidex.Web namespace Squidex.Web
@ -46,21 +47,19 @@ namespace Squidex.Web
return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase); return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase);
} }
public static bool TryGetHeaderString(this IHeaderDictionary headers, string header, [MaybeNullWhen(false)] out string result) public static bool TryGetString(this IHeaderDictionary headers, string header, [MaybeNullWhen(false)] out string result)
{ {
if (headers.TryGetValue(header, out var value)) result = null!;
{
string valueString = value;
if (!string.IsNullOrWhiteSpace(valueString)) if (headers.TryGetValue(header, out var value) && value != StringValues.Empty)
{
if (!string.IsNullOrWhiteSpace(value))
{ {
result = valueString; result = value;
return true; return true;
} }
} }
result = null!;
return false; return false;
} }
} }

17
backend/src/Squidex.Web/Pipeline/ActionContextLogAppender.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
@ -32,16 +33,7 @@ namespace Squidex.Web.Pipeline
return; return;
} }
Guid requestId; var requestId = GetRequestId(httpContext);
if (httpContext.Items.TryGetValue(nameof(requestId), out var requestIdvalue) && requestIdvalue is Guid requestIdValue)
{
requestId = requestIdValue;
}
else
{
httpContext.Items[nameof(requestId)] = requestId = Guid.NewGuid();
}
var logContext = (requestId, context: httpContext, actionContextAccessor); var logContext = (requestId, context: httpContext, actionContextAccessor);
@ -65,5 +57,10 @@ namespace Squidex.Web.Pipeline
} }
}); });
} }
private static string GetRequestId(HttpContext httpContext)
{
return Activity.Current?.Id ?? httpContext.TraceIdentifier ?? Guid.NewGuid().ToString();
}
} }
} }

22
backend/src/Squidex.Web/Pipeline/ETagFilter.cs → backend/src/Squidex.Web/Pipeline/CachingFilter.cs

@ -15,24 +15,30 @@ using Microsoft.Net.Http.Headers;
namespace Squidex.Web.Pipeline namespace Squidex.Web.Pipeline
{ {
public sealed class ETagFilter : IAsyncActionFilter public sealed class CachingFilter : IAsyncActionFilter
{ {
private readonly ETagOptions options; private readonly CachingOptions cachingOptions;
private readonly CachingManager cachingManager;
public ETagFilter(IOptions<ETagOptions> options) public CachingFilter(CachingManager cachingManager, IOptions<CachingOptions> cachingOptions)
{ {
this.options = options.Value; this.cachingOptions = cachingOptions.Value;
this.cachingManager = cachingManager;
} }
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{ {
var httpContext = context.HttpContext;
cachingManager.Start(httpContext);
var resultContext = await next(); var resultContext = await next();
var httpContext = context.HttpContext; cachingManager.Finish(httpContext, cachingOptions.MaxSurrogateKeys);
if (httpContext.Response.Headers.TryGetHeaderString(HeaderNames.ETag, out var etag)) if (httpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag))
{ {
if (!options.Strong && !etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) if (!cachingOptions.StrongETag && !etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase))
{ {
etag = $"W/{etag}"; etag = $"W/{etag}";
@ -41,7 +47,7 @@ namespace Squidex.Web.Pipeline
if (HttpMethods.IsGet(httpContext.Request.Method) && if (HttpMethods.IsGet(httpContext.Request.Method) &&
httpContext.Response.StatusCode == 200 && httpContext.Response.StatusCode == 200 &&
httpContext.Request.Headers.TryGetHeaderString(HeaderNames.IfNoneMatch, out var noneMatch) && httpContext.Request.Headers.TryGetString(HeaderNames.IfNoneMatch, out var noneMatch) &&
string.Equals(etag, noneMatch, StringComparison.Ordinal)) string.Equals(etag, noneMatch, StringComparison.Ordinal))
{ {
resultContext.Result = new StatusCodeResult(304); resultContext.Result = new StatusCodeResult(304);

157
backend/src/Squidex.Web/Pipeline/CachingManager.cs

@ -0,0 +1,157 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log;
namespace Squidex.Web.Pipeline
{
public sealed class CachingManager : IRequestCache
{
private readonly IHttpContextAccessor httpContextAccessor;
internal sealed class CacheContext : IRequestCache, IDisposable
{
private readonly IncrementalHash hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
private readonly HashSet<string> keys = new HashSet<string>();
private readonly ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim();
private bool hasDependency;
public void Dispose()
{
hasher.Dispose();
slimLock.Dispose();
}
public void AddDependency(Guid key, long version)
{
if (key != default)
{
try
{
slimLock.EnterWriteLock();
keys.Add(key.ToString());
hasher.AppendData(key.ToByteArray());
hasher.AppendData(BitConverter.GetBytes(version));
hasDependency = true;
}
finally
{
slimLock.ExitWriteLock();
}
}
}
public void AddDependency(object? value)
{
if (value != null)
{
try
{
slimLock.EnterWriteLock();
var formatted = value.ToString();
if (formatted != null)
{
hasher.AppendData(Encoding.Default.GetBytes(formatted));
}
}
finally
{
slimLock.ExitWriteLock();
}
}
}
public void Finish(HttpResponse response, int maxSurrogateKeys)
{
if (hasDependency && !response.Headers.ContainsKey(HeaderNames.ETag))
{
using (Profiler.Trace("CalculateEtag"))
{
var cacheBuffer = hasher.GetHashAndReset();
var cacheString = BitConverter.ToString(cacheBuffer).Replace("-", string.Empty).ToUpperInvariant();
response.Headers.Add(HeaderNames.ETag, cacheString);
}
}
if (keys.Count <= maxSurrogateKeys)
{
var value = string.Join(" ", keys);
response.Headers.Add("Surrogate-Key", value);
}
}
}
public CachingManager(IHttpContextAccessor httpContextAccessor)
{
Guard.NotNull(httpContextAccessor);
this.httpContextAccessor = httpContextAccessor;
}
public void Start(HttpContext httpContext)
{
Guard.NotNull(httpContext);
httpContext.Features.Set(new CacheContext());
}
public void AddDependency(Guid key, long version)
{
if (httpContextAccessor.HttpContext != null)
{
var cacheContext = httpContextAccessor.HttpContext.Features.Get<CacheContext>();
if (cacheContext != null)
{
cacheContext.AddDependency(key, version);
}
}
}
public void AddDependency(object? value)
{
if (httpContextAccessor.HttpContext != null)
{
var cacheContext = httpContextAccessor.HttpContext.Features.Get<CacheContext>();
if (cacheContext != null)
{
cacheContext.AddDependency(value);
}
}
}
public void Finish(HttpContext httpContext, int maxSurrogateKeys)
{
Guard.NotNull(httpContext);
var cacheContext = httpContextAccessor.HttpContext.Features.Get<CacheContext>();
if (cacheContext != null)
{
cacheContext.Finish(httpContext.Response, maxSurrogateKeys);
}
}
}
}

6
backend/src/Squidex.Web/Pipeline/ETagOptions.cs → backend/src/Squidex.Web/Pipeline/CachingOptions.cs

@ -7,8 +7,10 @@
namespace Squidex.Web.Pipeline namespace Squidex.Web.Pipeline
{ {
public sealed class ETagOptions public sealed class CachingOptions
{ {
public bool Strong { get; set; } public bool StrongETag { get; set; }
public int MaxSurrogateKeys { get; set; } = 200;
} }
} }

19
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -10,10 +10,8 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Assets.Models; using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Areas.Api.Controllers.Contents;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
@ -36,7 +34,6 @@ namespace Squidex.Areas.Api.Controllers.Assets
private readonly IAssetQueryService assetQuery; private readonly IAssetQueryService assetQuery;
private readonly IAssetUsageTracker assetStatsRepository; private readonly IAssetUsageTracker assetStatsRepository;
private readonly IAppPlansProvider appPlansProvider; private readonly IAppPlansProvider appPlansProvider;
private readonly MyContentsControllerOptions controllerOptions;
private readonly ITagService tagService; private readonly ITagService tagService;
public AssetsController( public AssetsController(
@ -44,14 +41,12 @@ namespace Squidex.Areas.Api.Controllers.Assets
IAssetQueryService assetQuery, IAssetQueryService assetQuery,
IAssetUsageTracker assetStatsRepository, IAssetUsageTracker assetStatsRepository,
IAppPlansProvider appPlansProvider, IAppPlansProvider appPlansProvider,
IOptions<MyContentsControllerOptions> controllerOptions,
ITagService tagService) ITagService tagService)
: base(commandBus) : base(commandBus)
{ {
this.assetQuery = assetQuery; this.assetQuery = assetQuery;
this.assetStatsRepository = assetStatsRepository; this.assetStatsRepository = assetStatsRepository;
this.appPlansProvider = appPlansProvider; this.appPlansProvider = appPlansProvider;
this.controllerOptions = controllerOptions.Value;
this.tagService = tagService; this.tagService = tagService;
} }
@ -112,13 +107,6 @@ namespace Squidex.Areas.Api.Controllers.Assets
return AssetsDto.FromAssets(assets, this, app); return AssetsDto.FromAssets(assets, this, app);
}); });
if (controllerOptions.EnableSurrogateKeys && assets.Count <= controllerOptions.MaxItemsForSurrogateKeys)
{
Response.Headers["Surrogate-Key"] = assets.ToSurrogateKeys();
}
Response.Headers[HeaderNames.ETag] = assets.ToEtag();
return Ok(response); return Ok(response);
} }
@ -150,13 +138,6 @@ namespace Squidex.Areas.Api.Controllers.Assets
return AssetDto.FromAsset(asset, this, app); return AssetDto.FromAsset(asset, this, app);
}); });
if (controllerOptions.EnableSurrogateKeys)
{
Response.Headers["Surrogate-Key"] = asset.ToSurrogateKey();
}
Response.Headers[HeaderNames.ETag] = asset.ToEtag();
return Ok(response); return Ok(response);
} }

1
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetQuery.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Text;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;

41
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -6,12 +6,9 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
@ -26,7 +23,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
public sealed class ContentsController : ApiController public sealed class ContentsController : ApiController
{ {
private readonly MyContentsControllerOptions controllerOptions;
private readonly IContentQueryService contentQuery; private readonly IContentQueryService contentQuery;
private readonly IContentWorkflow contentWorkflow; private readonly IContentWorkflow contentWorkflow;
private readonly IGraphQLService graphQl; private readonly IGraphQLService graphQl;
@ -34,13 +30,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
public ContentsController(ICommandBus commandBus, public ContentsController(ICommandBus commandBus,
IContentQueryService contentQuery, IContentQueryService contentQuery,
IContentWorkflow contentWorkflow, IContentWorkflow contentWorkflow,
IGraphQLService graphQl, IGraphQLService graphQl)
IOptions<MyContentsControllerOptions> controllerOptions)
: base(commandBus) : base(commandBus)
{ {
this.contentQuery = contentQuery; this.contentQuery = contentQuery;
this.contentWorkflow = contentWorkflow; this.contentWorkflow = contentWorkflow;
this.controllerOptions = controllerOptions.Value;
this.graphQl = graphQl; this.graphQl = graphQl;
} }
@ -133,13 +127,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow); return ContentsDto.FromContentsAsync(contents, Context, this, null, contentWorkflow);
}); });
if (ShouldProvideSurrogateKeys(contents))
{
Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys();
}
Response.Headers[HeaderNames.ETag] = contents.ToEtag();
return Ok(response); return Ok(response);
} }
@ -177,13 +164,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
return await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow); return await ContentsDto.FromContentsAsync(contents, Context, this, schema, contentWorkflow);
}); });
if (ShouldProvideSurrogateKeys(contents))
{
Response.Headers["Surrogate-Key"] = contents.ToSurrogateKeys();
}
Response.Headers[HeaderNames.ETag] = contents.ToEtag();
return Ok(response); return Ok(response);
} }
@ -211,13 +191,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
var response = ContentDto.FromContent(Context, content, this); var response = ContentDto.FromContent(Context, content, this);
if (controllerOptions.EnableSurrogateKeys)
{
Response.Headers["Surrogate-Key"] = content.ToSurrogateKey();
}
Response.Headers[HeaderNames.ETag] = content.ToEtag();
return Ok(response); return Ok(response);
} }
@ -246,13 +219,6 @@ namespace Squidex.Areas.Api.Controllers.Contents
var response = ContentDto.FromContent(Context, content, this); var response = ContentDto.FromContent(Context, content, this);
if (controllerOptions.EnableSurrogateKeys)
{
Response.Headers["Surrogate-Key"] = content.ToSurrogateKey();
}
Response.Headers[HeaderNames.ETag] = content.ToEtag();
return Ok(response.Data); return Ok(response.Data);
} }
@ -482,10 +448,5 @@ namespace Squidex.Areas.Api.Controllers.Contents
return response; return response;
} }
private bool ShouldProvideSurrogateKeys(IReadOnlyList<IContentEntity> response)
{
return controllerOptions.EnableSurrogateKeys && response.Count <= controllerOptions.MaxItemsForSurrogateKeys;
}
} }
} }

14
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs

@ -21,6 +21,7 @@ using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Pipeline.OpenApi; using Squidex.Pipeline.OpenApi;
namespace Squidex.Areas.Api.Controllers.Contents.Generator namespace Squidex.Areas.Api.Controllers.Contents.Generator
@ -28,12 +29,13 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
public sealed class SchemasOpenApiGenerator public sealed class SchemasOpenApiGenerator
{ {
private readonly OpenApiDocumentGeneratorSettings settings = new OpenApiDocumentGeneratorSettings(); private readonly OpenApiDocumentGeneratorSettings settings = new OpenApiDocumentGeneratorSettings();
private readonly IRequestCache requestCache;
private OpenApiSchemaGenerator schemaGenerator; private OpenApiSchemaGenerator schemaGenerator;
private OpenApiDocument document; private OpenApiDocument document;
private JsonSchema statusSchema; private JsonSchema statusSchema;
private JsonSchemaResolver schemaResolver; private JsonSchemaResolver schemaResolver;
public SchemasOpenApiGenerator(IEnumerable<IDocumentProcessor> documentProcessors) public SchemasOpenApiGenerator(IEnumerable<IDocumentProcessor> documentProcessors, IRequestCache requestCache)
{ {
settings.ConfigureSchemaSettings(); settings.ConfigureSchemaSettings();
@ -41,6 +43,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
settings.DocumentProcessors.Add(processor); settings.DocumentProcessors.Add(processor);
} }
this.requestCache = requestCache;
} }
public OpenApiDocument Generate(HttpContext httpContext, IAppEntity app, IEnumerable<ISchemaEntity> schemas) public OpenApiDocument Generate(HttpContext httpContext, IAppEntity app, IEnumerable<ISchemaEntity> schemas)
@ -79,13 +83,17 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private void GenerateSchemasOperations(IEnumerable<ISchemaEntity> schemas, IAppEntity app) private void GenerateSchemasOperations(IEnumerable<ISchemaEntity> schemas, IAppEntity app)
{ {
requestCache.AddDependency(app.Id, app.Version);
var appBasePath = $"/content/{app.Name}"; var appBasePath = $"/content/{app.Name}";
foreach (var schema in schemas.Select(x => x.SchemaDef).Where(x => x.IsPublished)) foreach (var schema in schemas.Where(x => x.SchemaDef.IsPublished))
{ {
requestCache.AddDependency(schema.Id, schema.Version);
var partition = app.PartitionResolver(); var partition = app.PartitionResolver();
new SchemaOpenApiGenerator(document, app.Name, appBasePath, schema, AppendSchema, statusSchema, partition) new SchemaOpenApiGenerator(document, app.Name, appBasePath, schema.SchemaDef, AppendSchema, statusSchema, partition)
.GenerateSchemaOperations(); .GenerateSchemaOperations();
} }
} }

16
backend/src/Squidex/Areas/Api/Controllers/Contents/MyContentsControllerOptions.cs

@ -1,16 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Areas.Api.Controllers.Contents
{
public sealed class MyContentsControllerOptions
{
public bool EnableSurrogateKeys { get; set; }
public int MaxItemsForSurrogateKeys { get; set; }
}
}

2
backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -92,8 +92,6 @@ namespace Squidex.Areas.Api.Controllers.Rules
return RulesDto.FromRules(rules, this, app); return RulesDto.FromRules(rules, this, app);
}); });
Response.Headers[HeaderNames.ETag] = rules.ToEtag();
return Ok(response); return Ok(response);
} }

7
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -11,7 +11,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NodaTime; using NodaTime;
using Orleans; using Orleans;
using Squidex.Areas.Api.Controllers.Contents;
using Squidex.Areas.Api.Controllers.Contents.Generator; using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Areas.Api.Controllers.News; using Squidex.Areas.Api.Controllers.News;
using Squidex.Areas.Api.Controllers.News.Service; using Squidex.Areas.Api.Controllers.News.Service;
@ -97,10 +96,8 @@ namespace Squidex.Config.Domain
{ {
services.Configure<RobotsTxtOptions>( services.Configure<RobotsTxtOptions>(
config.GetSection("robots")); config.GetSection("robots"));
services.Configure<ETagOptions>( services.Configure<CachingOptions>(
config.GetSection("etags")); config.GetSection("caching"));
services.Configure<MyContentsControllerOptions>(
config.GetSection("contentsController"));
services.Configure<MyUIOptions>( services.Configure<MyUIOptions>(
config.GetSection("ui")); config.GetSection("ui"));
services.Configure<MyNewsOptions>( services.Configure<MyNewsOptions>(

6
backend/src/Squidex/Config/Web/WebServices.cs

@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Config.Domain; using Squidex.Config.Domain;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure.Caching;
using Squidex.Pipeline.Plugins; using Squidex.Pipeline.Plugins;
using Squidex.Pipeline.Robots; using Squidex.Pipeline.Robots;
using Squidex.Web; using Squidex.Web;
@ -48,6 +49,9 @@ namespace Squidex.Config.Web
services.AddSingletonAs<RequestLogPerformanceMiddleware>() services.AddSingletonAs<RequestLogPerformanceMiddleware>()
.AsSelf(); .AsSelf();
services.AddSingletonAs<CachingManager>()
.As<IRequestCache>();
services.AddSingletonAs<ContextProvider>() services.AddSingletonAs<ContextProvider>()
.As<IContextProvider>(); .As<IContextProvider>();
@ -64,7 +68,7 @@ namespace Squidex.Config.Web
services.AddMvc(options => services.AddMvc(options =>
{ {
options.Filters.Add<ETagFilter>(); options.Filters.Add<CachingFilter>();
options.Filters.Add<DeferredActionFilter>(); options.Filters.Add<DeferredActionFilter>();
options.Filters.Add<AppResolver>(); options.Filters.Add<AppResolver>();
options.Filters.Add<MeasureResultFilter>(); options.Filters.Add<MeasureResultFilter>();

23
backend/src/Squidex/appsettings.json

@ -30,11 +30,16 @@
"Squidex.Extensions.dll" "Squidex.Extensions.dll"
], ],
"etags": { "caching": {
/* /*
* Set to true, to use strong etags. * Set to true, to use strong etags.
*/ */
"strong": false "strongETag": false,
/*
* Restrict the surrogate keys to results that have less than 200 items.
*/
"maxItemsForSurrogateKeys": 200
}, },
"languages": { "languages": {
@ -176,20 +181,6 @@
} }
}, },
"contentsController": {
/*
* Enable surrogate keys as headers.
*
* Nginx Has problems with long headers. It might make sense to disable this feature if you do not use a CDN.
*/
"enableSurrogateKeys": true,
/*
* Restrict the surrogate keys to results that have less than 200 items.
*/
"maxItemsForSurrogateKeys": 200
},
"contents": { "contents": {
/* /*
* The default page size if not specified by a query. * The default page size if not specified by a query.

15
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs

@ -12,6 +12,7 @@ using FakeItEasy;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets.Queries namespace Squidex.Domain.Apps.Entities.Assets.Queries
@ -19,6 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
public class AssetEnricherTests public class AssetEnricherTests
{ {
private readonly ITagService tagService = A.Fake<ITagService>(); private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IRequestCache requestCache = A.Fake<IRequestCache>();
private readonly IAssetMetadataSource assetMetadataSource1 = A.Fake<IAssetMetadataSource>(); private readonly IAssetMetadataSource assetMetadataSource1 = A.Fake<IAssetMetadataSource>();
private readonly IAssetMetadataSource assetMetadataSource2 = A.Fake<IAssetMetadataSource>(); private readonly IAssetMetadataSource assetMetadataSource2 = A.Fake<IAssetMetadataSource>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
@ -33,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
assetMetadataSource2 assetMetadataSource2
}; };
sut = new AssetEnricher(tagService, assetMetadataSources); sut = new AssetEnricher(tagService, assetMetadataSources, requestCache);
} }
[Fact] [Fact]
@ -46,6 +48,17 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
Assert.Empty(result.TagNames); Assert.Empty(result.TagNames);
} }
[Fact]
public async Task Should_enrich_with_cache_dependencies()
{
var source = new AssetEntity { AppId = appId, Id = Guid.NewGuid(), Version = 13 };
var result = await sut.EnrichAsync(source, requestContext);
A.CallTo(() => requestCache.AddDependency(result.Id, result.Version))
.MustHaveHappened();
}
[Fact] [Fact]
public async Task Should_enrich_asset_with_tag_names() public async Task Should_enrich_asset_with_tag_names()
{ {

9
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -71,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_invoke_steps() public async Task Should_invoke_steps()
{ {
var source = PublishedContent(); var source = CreateContent();
var step1 = A.Fake<IContentEnricherStep>(); var step1 = A.Fake<IContentEnricherStep>();
var step2 = A.Fake<IContentEnricherStep>(); var step2 = A.Fake<IContentEnricherStep>();
@ -90,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_provide_and_cache_schema() public async Task Should_provide_and_cache_schema()
{ {
var source = PublishedContent(); var source = CreateContent();
var step1 = new ResolveSchema(); var step1 = new ResolveSchema();
var step2 = new ResolveSchema(); var step2 = new ResolveSchema();
@ -106,9 +105,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
.MustHaveHappenedOnceExactly(); .MustHaveHappenedOnceExactly();
} }
private ContentEntity PublishedContent() private ContentEntity CreateContent()
{ {
return new ContentEntity { Status = Status.Published, SchemaId = schemaId }; return new ContentEntity { SchemaId = schemaId };
} }
} }
} }

10
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs

@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_convert_data_only() public async Task Should_convert_data_only()
{ {
var content = PublishedContent(new NamedContentData()); var content = CreateContent(new NamedContentData());
await sut.EnrichAsync(requestContext, Enumerable.Repeat(content, 1), schemaProvider); await sut.EnrichAsync(requestContext, Enumerable.Repeat(content, 1), schemaProvider);
@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_convert_data_and_data_draft_when_frontend_user() public async Task Should_convert_data_and_data_draft_when_frontend_user()
{ {
var content = PublishedContent(new NamedContentData()); var content = CreateContent(new NamedContentData());
var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId)); var ctx = new Context(Mocks.FrontendUser(), Mocks.App(appId));
@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var source = BuildTestData(id1, id2); var source = BuildTestData(id1, id2);
var content = PublishedContent(source); var content = CreateContent(source);
var expected = var expected =
new NamedContentData() new NamedContentData()
@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var source = BuildTestData(id1, id2); var source = BuildTestData(id1, id2);
var content = PublishedContent(source); var content = CreateContent(source);
var expected = var expected =
new NamedContentData() new NamedContentData()
@ -173,7 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
.Add("nested", JsonValue.Array(id1, id2))))); .Add("nested", JsonValue.Array(id1, id2)))));
} }
private ContentEntity PublishedContent(NamedContentData data) private ContentEntity CreateContent(NamedContentData data)
{ {
return new ContentEntity return new ContentEntity
{ {

22
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs

@ -8,11 +8,12 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents; using FakeItEasy;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.Queries namespace Squidex.Domain.Apps.Entities.Contents.Queries
@ -20,6 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public class EnrichForCachingTests public class EnrichForCachingTests
{ {
private readonly ISchemaEntity schema; private readonly ISchemaEntity schema;
private readonly IRequestCache requestCache = A.Fake<IRequestCache>();
private readonly Context requestContext; private readonly Context requestContext;
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); 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");
@ -33,25 +35,29 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
schema = Mocks.Schema(appId, schemaId); schema = Mocks.Schema(appId, schemaId);
schemaProvider = x => Task.FromResult(schema); schemaProvider = x => Task.FromResult(schema);
sut = new EnrichForCaching(); sut = new EnrichForCaching(requestCache);
} }
[Fact] [Fact]
public async Task Should_add_app_version_and_schema_as_dependency() public async Task Should_add_app_version_and_schema_as_dependency()
{ {
var content = PublishedContent(); var content = CreateContent();
await sut.EnrichAsync(requestContext, Enumerable.Repeat(content, 1), schemaProvider); await sut.EnrichAsync(requestContext, Enumerable.Repeat(content, 1), schemaProvider);
Assert.Contains(requestContext.App.Version, content.CacheDependencies); A.CallTo(() => requestCache.AddDependency(content.Id, content.Version))
.MustHaveHappened();
Assert.Contains(schema.Id, content.CacheDependencies); A.CallTo(() => requestCache.AddDependency(schema.Id, schema.Version))
Assert.Contains(schema.Version, content.CacheDependencies); .MustHaveHappened();
A.CallTo(() => requestCache.AddDependency(requestContext.App.Id, requestContext.App.Version))
.MustHaveHappened();
} }
private ContentEntity PublishedContent() private ContentEntity CreateContent()
{ {
return new ContentEntity { Status = Status.Published, SchemaId = schemaId }; return new ContentEntity { Id = Guid.NewGuid(), SchemaId = schemaId, Version = 13 };
} }
} }
} }

11
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithSchemaTests.cs

@ -8,7 +8,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
@ -39,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_enrich_with_reference_fields() public async Task Should_enrich_with_reference_fields()
{ {
var content = PublishedContent(); var content = CreateContent();
var ctx = new Context(Mocks.FrontendUser(), requestContext.App); var ctx = new Context(Mocks.FrontendUser(), requestContext.App);
@ -51,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_not_enrich_with_reference_fields_when_not_frontend() public async Task Should_not_enrich_with_reference_fields_when_not_frontend()
{ {
var source = PublishedContent(); var source = CreateContent();
await sut.EnrichAsync(requestContext, Enumerable.Repeat(source, 1), schemaProvider); await sut.EnrichAsync(requestContext, Enumerable.Repeat(source, 1), schemaProvider);
@ -61,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_enrich_with_schema_names() public async Task Should_enrich_with_schema_names()
{ {
var content = PublishedContent(); var content = CreateContent();
await sut.EnrichAsync(requestContext, Enumerable.Repeat(content, 1), schemaProvider); await sut.EnrichAsync(requestContext, Enumerable.Repeat(content, 1), schemaProvider);
@ -69,9 +68,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.Equal("my-schema", content.SchemaDisplayName); Assert.Equal("my-schema", content.SchemaDisplayName);
} }
private ContentEntity PublishedContent() private ContentEntity CreateContent()
{ {
return new ContentEntity { Status = Status.Published, SchemaId = schemaId }; return new ContentEntity { SchemaId = schemaId };
} }
} }
} }

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs

@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_enrich_content_with_status_color() public async Task Should_enrich_content_with_status_color()
{ {
var content = PublishedContent(); var content = CreateContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(content)) A.CallTo(() => contentWorkflow.GetInfoAsync(content))
.Returns(new StatusInfo(Status.Published, StatusColors.Published)); .Returns(new StatusInfo(Status.Published, StatusColors.Published));
@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
[Fact] [Fact]
public async Task Should_enrich_content_with_default_color_if_not_found() public async Task Should_enrich_content_with_default_color_if_not_found()
{ {
var content = PublishedContent(); var content = CreateContent();
A.CallTo(() => contentWorkflow.GetInfoAsync(content)) A.CallTo(() => contentWorkflow.GetInfoAsync(content))
.Returns(Task.FromResult<StatusInfo>(null!)); .Returns(Task.FromResult<StatusInfo>(null!));
@ -101,9 +101,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
private ContentEntity PublishedContent() private ContentEntity CreateContent()
{ {
return new ContentEntity { Status = Status.Published, SchemaId = schemaId }; return new ContentEntity { SchemaId = schemaId };
} }
} }
} }

16
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs

@ -19,6 +19,7 @@ using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Xunit; using Xunit;
@ -28,6 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>(); private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly IRequestCache requestCache = A.Fake<IRequestCache>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); 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 ProvideSchema schemaProvider; private readonly ProvideSchema schemaProvider;
@ -69,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
}; };
sut = new ResolveAssets(assetUrlGenerator, assetQuery); sut = new ResolveAssets(assetUrlGenerator, assetQuery, requestCache);
} }
[Fact] [Fact]
@ -96,15 +98,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await sut.EnrichAsync(requestContext, contents, schemaProvider); await sut.EnrichAsync(requestContext, contents, schemaProvider);
var enriched1 = contents[0]; A.CallTo(() => requestCache.AddDependency(image1.Id, image1.Version))
.MustHaveHappened();
Assert.Contains(image1.Id, enriched1.CacheDependencies); A.CallTo(() => requestCache.AddDependency(image2.Id, image2.Version))
Assert.Contains(image1.Version, enriched1.CacheDependencies); .MustHaveHappened();
var enriched2 = contents[1];
Assert.Contains(image2.Id, enriched2.CacheDependencies);
Assert.Contains(image2.Version, enriched2.CacheDependencies);
} }
[Fact] [Fact]

30
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveReferencesTests.cs

@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Xunit; using Xunit;
@ -25,6 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public class ResolveReferencesTests public class ResolveReferencesTests
{ {
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly IRequestCache requestCache = A.Fake<IRequestCache>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> refSchemaId1 = NamedId.Of(Guid.NewGuid(), "my-ref1"); private readonly NamedId<Guid> refSchemaId1 = NamedId.Of(Guid.NewGuid(), "my-ref1");
private readonly NamedId<Guid> refSchemaId2 = NamedId.Of(Guid.NewGuid(), "my-ref2"); private readonly NamedId<Guid> refSchemaId2 = NamedId.Of(Guid.NewGuid(), "my-ref2");
@ -83,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
}; };
sut = new ResolveReferences(new Lazy<IContentQueryService>(() => contentQuery)); sut = new ResolveReferences(new Lazy<IContentQueryService>(() => contentQuery), requestCache);
} }
[Fact] [Fact]
@ -107,25 +109,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var enriched1 = contents[0]; var enriched1 = contents[0];
Assert.Contains(refSchemaId1.Id, enriched1.CacheDependencies); A.CallTo(() => requestCache.AddDependency(refSchemaId1.Id, 0))
Assert.Contains(refSchemaId2.Id, enriched1.CacheDependencies); .MustHaveHappened();
Assert.Contains(ref1_1.Id, enriched1.CacheDependencies); A.CallTo(() => requestCache.AddDependency(refSchemaId2.Id, 0))
Assert.Contains(ref1_1.Version, enriched1.CacheDependencies); .MustHaveHappened();
Assert.Contains(ref2_1.Id, enriched1.CacheDependencies); A.CallTo(() => requestCache.AddDependency(ref1_1.Id, ref1_1.Version))
Assert.Contains(ref2_1.Version, enriched1.CacheDependencies); .MustHaveHappened();
var enriched2 = contents[1]; A.CallTo(() => requestCache.AddDependency(ref2_1.Id, ref2_1.Version))
.MustHaveHappened();
Assert.Contains(refSchemaId1.Id, enriched2.CacheDependencies); A.CallTo(() => requestCache.AddDependency(ref1_2.Id, ref1_2.Version))
Assert.Contains(refSchemaId2.Id, enriched2.CacheDependencies); .MustHaveHappened();
Assert.Contains(ref1_2.Id, enriched2.CacheDependencies); A.CallTo(() => requestCache.AddDependency(ref2_2.Id, ref2_2.Version))
Assert.Contains(ref1_2.Version, enriched2.CacheDependencies); .MustHaveHappened();
Assert.Contains(ref2_2.Id, enriched2.CacheDependencies);
Assert.Contains(ref2_2.Version, enriched2.CacheDependencies);
} }
[Fact] [Fact]

38
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs

@ -7,12 +7,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Rules.Queries namespace Squidex.Domain.Apps.Entities.Rules.Queries
@ -20,36 +20,43 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries
public class RuleEnricherTests public class RuleEnricherTests
{ {
private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>();
private readonly IRequestCache requestCache = A.Fake<IRequestCache>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly Context requestContext = Context.Anonymous(); private readonly Context requestContext = Context.Anonymous();
private readonly RuleEnricher sut; private readonly RuleEnricher sut;
public RuleEnricherTests() public RuleEnricherTests()
{ {
sut = new RuleEnricher(ruleEventRepository); sut = new RuleEnricher(ruleEventRepository, requestCache);
} }
[Fact] [Fact]
public async Task Should_not_enrich_if_statistics_not_found() public async Task Should_not_enrich_if_statistics_not_found()
{ {
var source = new RuleEntity { AppId = appId }; var source = CreateRule();
var result = await sut.EnrichAsync(source, requestContext); var result = await sut.EnrichAsync(source, requestContext);
Assert.Equal(0, result.NumFailed); Assert.Equal(0, result.NumFailed);
Assert.Equal(0, result.NumSucceeded); Assert.Equal(0, result.NumSucceeded);
Assert.Null(result.LastExecuted); Assert.Null(result.LastExecuted);
A.CallTo(() => requestCache.AddDependency(source.Id, source.Version))
.MustHaveHappened();
A.CallTo(() => requestCache.AddDependency(null))
.MustNotHaveHappened();
} }
[Fact] [Fact]
public async Task Should_enrich_rules_with_found_statistics() public async Task Should_enrich_rules_with_found_statistics()
{ {
var source1 = new RuleEntity { AppId = appId, Id = Guid.NewGuid() }; var source = CreateRule();
var source2 = new RuleEntity { AppId = appId, Id = Guid.NewGuid() };
var stats = new RuleStatistics var stats = new RuleStatistics
{ {
RuleId = source1.Id, RuleId = source.Id,
NumFailed = 12, NumFailed = 12,
NumSucceeded = 17, NumSucceeded = 17,
LastExecuted = SystemClock.Instance.GetCurrentInstant() LastExecuted = SystemClock.Instance.GetCurrentInstant()
@ -58,19 +65,18 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries
A.CallTo(() => ruleEventRepository.QueryStatisticsByAppAsync(appId.Id)) A.CallTo(() => ruleEventRepository.QueryStatisticsByAppAsync(appId.Id))
.Returns(new List<RuleStatistics> { stats }); .Returns(new List<RuleStatistics> { stats });
var result = await sut.EnrichAsync(new[] { source1, source2 }, requestContext); var result = await sut.EnrichAsync(source, requestContext);
var enriched1 = result.ElementAt(0);
Assert.Equal(12, enriched1.NumFailed); A.CallTo(() => requestCache.AddDependency(source.Id, source.Version))
Assert.Equal(17, enriched1.NumSucceeded); .MustHaveHappened();
Assert.Equal(stats.LastExecuted, enriched1.LastExecuted);
var enriched2 = result.ElementAt(1); A.CallTo(() => requestCache.AddDependency(stats.LastExecuted))
.MustHaveHappened();
}
Assert.Equal(0, enriched2.NumFailed); private RuleEntity CreateRule()
Assert.Equal(0, enriched2.NumSucceeded); {
Assert.Null(enriched2.LastExecuted); return new RuleEntity { AppId = appId, Id = Guid.NewGuid(), Version = 13 };
} }
} }
} }

220
backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs

@ -0,0 +1,220 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Squidex.Web.Pipeline
{
public class CachingFilterTests
{
private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>();
private readonly HttpContext httpContext = new DefaultHttpContext();
private readonly ActionExecutingContext executingContext;
private readonly ActionExecutedContext executedContext;
private readonly CachingOptions cachingOptions = new CachingOptions();
private readonly CachingManager cachingManager;
private readonly CachingFilter sut;
public CachingFilterTests()
{
A.CallTo(() => httpContextAccessor.HttpContext)
.Returns(httpContext);
cachingManager = new CachingManager(httpContextAccessor);
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var actionFilters = new List<IFilterMetadata>();
executingContext = new ActionExecutingContext(actionContext, actionFilters, new Dictionary<string, object>(), this);
executedContext = new ActionExecutedContext(actionContext, actionFilters, this)
{
Result = new OkResult()
};
sut = new CachingFilter(cachingManager, Options.Create(cachingOptions));
}
[Fact]
public async Task Should_not_append_etag_if_not_found()
{
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal(StringValues.Empty, httpContext.Response.Headers[HeaderNames.ETag]);
}
[Fact]
public async Task Should_not_append_etag_if_empty()
{
httpContext.Response.Headers[HeaderNames.ETag] = string.Empty;
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]);
}
[Fact]
public async Task Should_not_convert_strong_etag_if_disabled()
{
cachingOptions.StrongETag = true;
httpContext.Response.Headers[HeaderNames.ETag] = "13";
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal("13", httpContext.Response.Headers[HeaderNames.ETag]);
}
[Fact]
public async Task Should_not_convert_already_weak_tag()
{
httpContext.Response.Headers[HeaderNames.ETag] = "W/13";
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]);
}
[Fact]
public async Task Should_convert_strong_to_weak_tag()
{
httpContext.Response.Headers[HeaderNames.ETag] = "13";
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]);
}
[Fact]
public async Task Should_not_convert_empty_string_to_weak_tag()
{
httpContext.Response.Headers[HeaderNames.ETag] = string.Empty;
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]);
}
[Fact]
public async Task Should_return_304_for_same_etags()
{
httpContext.Request.Method = HttpMethods.Get;
httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13";
httpContext.Response.Headers[HeaderNames.ETag] = "13";
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal(304, ((StatusCodeResult)executedContext.Result).StatusCode);
}
[Fact]
public async Task Should_not_return_304_for_different_etags()
{
httpContext.Request.Method = HttpMethods.Get;
httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/11";
httpContext.Response.Headers[HeaderNames.ETag] = "13";
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal(200, ((StatusCodeResult)executedContext.Result).StatusCode);
}
[Fact]
public async Task Should_append_surrogate_keys()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
cachingOptions.MaxSurrogateKeys = 2;
await sut.OnActionExecutionAsync(executingContext, () =>
{
cachingManager.AddDependency(id1, 12);
cachingManager.AddDependency(id2, 12);
return Task.FromResult(executedContext);
});
Assert.Equal($"{id1} {id2}", httpContext.Response.Headers["Surrogate-Key"]);
}
[Fact]
public async Task Should_not_append_surrogate_keys_if_maximum_is_exceeded()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
cachingOptions.MaxSurrogateKeys = 1;
await sut.OnActionExecutionAsync(executingContext, () =>
{
cachingManager.AddDependency(id1, 12);
cachingManager.AddDependency(id2, 12);
return Task.FromResult(executedContext);
});
Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]);
}
[Fact]
public async Task Should_generate_etag_from_ids_and_versions()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
await sut.OnActionExecutionAsync(executingContext, () =>
{
cachingManager.AddDependency(id1, 12);
cachingManager.AddDependency(id2, 12);
cachingManager.AddDependency(12);
return Task.FromResult(executedContext);
});
Assert.True(httpContext.Response.Headers[HeaderNames.ETag].ToString().Length > 20);
}
[Fact]
public async Task Should_not_generate_etag_when_already_added()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
await sut.OnActionExecutionAsync(executingContext, () =>
{
cachingManager.AddDependency(id1, 12);
cachingManager.AddDependency(id2, 12);
cachingManager.AddDependency(12);
executedContext.HttpContext.Response.Headers[HeaderNames.ETag] = "W/20";
return Task.FromResult(executedContext);
});
Assert.Equal("W/20", httpContext.Response.Headers[HeaderNames.ETag]);
}
private ActionExecutionDelegate Next()
{
return () => Task.FromResult(executedContext);
}
}
}

102
backend/tests/Squidex.Web.Tests/Pipeline/ETagFilterTests.cs

@ -1,102 +0,0 @@
// ==========================================================================
// 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Squidex.Web.Pipeline
{
public class ETagFilterTests
{
private readonly HttpContext httpContext = new DefaultHttpContext();
private readonly ActionExecutingContext executingContext;
private readonly ActionExecutedContext executedContext;
private readonly ETagFilter sut = new ETagFilter(Options.Create(new ETagOptions()));
public ETagFilterTests()
{
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var filters = new List<IFilterMetadata>();
executingContext = new ActionExecutingContext(actionContext, filters, new Dictionary<string, object>(), this);
executedContext = new ActionExecutedContext(actionContext, filters, this)
{
Result = new OkResult()
};
}
[Fact]
public async Task Should_not_convert_already_weak_tag()
{
httpContext.Response.Headers[HeaderNames.ETag] = "W/13";
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]);
}
[Fact]
public async Task Should_convert_strong_to_weak_tag()
{
httpContext.Response.Headers[HeaderNames.ETag] = "13";
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]);
}
[Fact]
public async Task Should_not_convert_empty_string_to_weak_tag()
{
httpContext.Response.Headers[HeaderNames.ETag] = string.Empty;
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]);
}
[Fact]
public async Task Should_return_304_for_same_etags()
{
httpContext.Request.Method = HttpMethods.Get;
httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13";
httpContext.Response.Headers[HeaderNames.ETag] = "13";
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal(304, ((StatusCodeResult)executedContext.Result).StatusCode);
}
[Fact]
public async Task Should_not_return_304_for_different_etags()
{
httpContext.Request.Method = HttpMethods.Get;
httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/11";
httpContext.Response.Headers[HeaderNames.ETag] = "13";
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal(200, ((StatusCodeResult)executedContext.Result).StatusCode);
}
private ActionExecutionDelegate Next()
{
return () => Task.FromResult(executedContext);
}
}
}

6
frontend/app/framework/angular/list-view.component.scss

@ -75,8 +75,10 @@
flex-shrink: 0; flex-shrink: 0;
} }
::ng-deep .pagination { ::ng-deep {
margin: .25rem 0; .pagination {
margin: .25rem 0;
}
} }
} }
} }

Loading…
Cancel
Save