Browse Source

Caching header improvements.

pull/492/head
Sebastian 6 years ago
parent
commit
882b0ea6fc
  1. 31
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs
  2. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  3. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs
  4. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/EnrichForCaching.cs
  5. 2
      backend/src/Squidex.Infrastructure/Caching/IRequestCache.cs
  6. 1
      backend/src/Squidex.Web/Pipeline/CachingFilter.cs
  7. 38
      backend/src/Squidex.Web/Pipeline/CachingManager.cs
  8. 14
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs
  9. 24
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichForCachingTests.cs
  10. 47
      backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs

31
backend/src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs

@ -9,21 +9,36 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public static class ContextExtensions public static class ContextExtensions
{ {
private const string HeaderUnpublished = "X-Unpublished";
private const string HeaderFlatten = "X-Flatten"; private const string HeaderFlatten = "X-Flatten";
private const string HeaderLanguages = "X-Languages"; private const string HeaderLanguages = "X-Languages";
private const string HeaderResolveFlow = "X-ResolveFlow";
private const string HeaderResolveAssetUrls = "X-Resolve-Urls";
private const string HeaderNoResolveLanguages = "X-NoResolveLanguages";
private const string HeaderNoEnrichment = "X-NoEnrichment";
private const string HeaderNoCleanup = "X-NoCleanup"; private const string HeaderNoCleanup = "X-NoCleanup";
private const string HeaderNoEnrichment = "X-NoEnrichment";
private const string HeaderNoResolveLanguages = "X-NoResolveLanguages";
private const string HeaderResolveFlow = "X-ResolveFlow";
private const string HeaderResolveUrls = "X-Resolve-Urls";
private const string HeaderUnpublished = "X-Unpublished";
private static readonly char[] Separators = { ',', ';' }; private static readonly char[] Separators = { ',', ';' };
public static void AddCacheHeaders(this Context context, IRequestCache cache)
{
cache.AddHeader(HeaderFlatten);
cache.AddHeader(HeaderLanguages);
cache.AddHeader(HeaderNoCleanup);
cache.AddHeader(HeaderNoEnrichment);
cache.AddHeader(HeaderNoResolveLanguages);
cache.AddHeader(HeaderResolveFlow);
cache.AddHeader(HeaderResolveUrls);
cache.AddHeader(HeaderUnpublished);
}
public static bool ShouldCleanup(this Context context) public static bool ShouldCleanup(this Context context)
{ {
return !context.Headers.ContainsKey(HeaderNoCleanup); return !context.Headers.ContainsKey(HeaderNoCleanup);
@ -91,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public static IEnumerable<string> AssetUrls(this Context context) public static IEnumerable<string> AssetUrls(this Context context)
{ {
if (context.Headers.TryGetValue(HeaderResolveAssetUrls, out var value)) if (context.Headers.TryGetValue(HeaderResolveUrls, out var value))
{ {
return value.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToHashSet(); return value.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToHashSet();
} }
@ -103,11 +118,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
if (fieldNames?.Any() == true) if (fieldNames?.Any() == true)
{ {
context.Headers[HeaderResolveAssetUrls] = string.Join(",", fieldNames); context.Headers[HeaderResolveUrls] = string.Join(",", fieldNames);
} }
else else
{ {
context.Headers.Remove(HeaderResolveAssetUrls); context.Headers.Remove(HeaderResolveUrls);
} }
return context; return context;

5
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs

@ -54,6 +54,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
var results = new List<ContentEntity>(); var results = new List<ContentEntity>();
foreach (var step in steps)
{
await step.EnrichAsync(context);
}
if (contents.Any()) if (contents.Any())
{ {
foreach (var content in contents) foreach (var content in contents)

6
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents.Queries namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
@ -17,5 +18,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public interface IContentEnricherStep public interface IContentEnricherStep
{ {
Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas); Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas);
Task EnrichAsync(Context context)
{
return TaskHelper.Done;
}
} }
} }

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

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{ {
@ -24,6 +25,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
this.requestCache = requestCache; this.requestCache = requestCache;
} }
public Task EnrichAsync(Context context)
{
context.AddCacheHeaders(requestCache);
return TaskHelper.Done;
}
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;

2
backend/src/Squidex.Infrastructure/Caching/IRequestCache.cs

@ -14,5 +14,7 @@ namespace Squidex.Infrastructure.Caching
void AddDependency(Guid key, long version); void AddDependency(Guid key, long version);
void AddDependency(object? value); void AddDependency(object? value);
void AddHeader(string header);
} }
} }

1
backend/src/Squidex.Web/Pipeline/CachingFilter.cs

@ -35,6 +35,7 @@ namespace Squidex.Web.Pipeline
var httpContext = context.HttpContext; var httpContext = context.HttpContext;
cachingManager.Start(httpContext); cachingManager.Start(httpContext);
cachingManager.AddHeader(HeaderNames.Authorization);
var resultContext = await next(); var resultContext = await next();

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

@ -7,10 +7,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
@ -26,6 +28,7 @@ namespace Squidex.Web.Pipeline
{ {
private readonly IncrementalHash hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); private readonly IncrementalHash hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
private readonly HashSet<string> keys = new HashSet<string>(); private readonly HashSet<string> keys = new HashSet<string>();
private readonly HashSet<string> headers = new HashSet<string>();
private readonly ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim(); private readonly ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim();
private bool hasDependency; private bool hasDependency;
@ -99,6 +102,28 @@ namespace Squidex.Web.Pipeline
response.Headers.Add("Surrogate-Key", value); response.Headers.Add("Surrogate-Key", value);
} }
if (headers.Count > 0)
{
response.Headers.Add(HeaderNames.Vary, new StringValues(headers.ToArray()));
}
}
public void AddHeader(string header)
{
if (!string.IsNullOrWhiteSpace(header))
{
try
{
slimLock.EnterWriteLock();
headers.Add(header);
}
finally
{
slimLock.ExitWriteLock();
}
}
} }
} }
@ -142,6 +167,19 @@ namespace Squidex.Web.Pipeline
} }
} }
public void AddHeader(string header)
{
if (httpContextAccessor.HttpContext != null)
{
var cacheContext = httpContextAccessor.HttpContext.Features.Get<CacheContext>();
if (cacheContext != null)
{
cacheContext.AddHeader(header);
}
}
}
public void Finish(HttpContext httpContext, int maxSurrogateKeys) public void Finish(HttpContext httpContext, int maxSurrogateKeys)
{ {
Guard.NotNull(httpContext); Guard.NotNull(httpContext);

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

@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
[Fact] [Fact]
public async Task Should_not_invoke_steps() public async Task Should_only_invoke_pre_enrich_for_empty_results()
{ {
var source = new IContentEntity[0]; var source = new IContentEntity[0];
@ -60,6 +60,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await sut.EnrichAsync(source, requestContext); await sut.EnrichAsync(source, requestContext);
A.CallTo(() => step1.EnrichAsync(requestContext))
.MustHaveHappened();
A.CallTo(() => step2.EnrichAsync(requestContext))
.MustHaveHappened();
A.CallTo(() => step1.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._)) A.CallTo(() => step1.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -79,6 +85,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await sut.EnrichAsync(source, requestContext); await sut.EnrichAsync(source, requestContext);
A.CallTo(() => step1.EnrichAsync(requestContext))
.MustHaveHappened();
A.CallTo(() => step2.EnrichAsync(requestContext))
.MustHaveHappened();
A.CallTo(() => step1.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._)) A.CallTo(() => step1.EnrichAsync(requestContext, A<IEnumerable<ContentEntity>>._, A<ProvideSchema>._))
.MustHaveHappened(); .MustHaveHappened();

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
@ -38,6 +39,29 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
sut = new EnrichForCaching(requestCache); sut = new EnrichForCaching(requestCache);
} }
[Fact]
public async Task Should_add_cache_headers()
{
var headers = new List<string>();
A.CallTo(() => requestCache.AddHeader(A<string>._))
.Invokes(new Action<string>(header => headers.Add(header)));
await sut.EnrichAsync(requestContext);
Assert.Equal(new List<string>
{
"X-Flatten",
"X-Languages",
"X-NoCleanup",
"X-NoEnrichment",
"X-NoResolveLanguages",
"X-ResolveFlow",
"X-Resolve-Urls",
"X-Unpublished"
}, headers);
}
[Fact] [Fact]
public async Task Should_add_app_version_and_schema_as_dependency() public async Task Should_add_app_version_and_schema_as_dependency()
{ {

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

@ -58,6 +58,53 @@ namespace Squidex.Web.Pipeline
Assert.Equal(StringValues.Empty, httpContext.Response.Headers[HeaderNames.ETag]); Assert.Equal(StringValues.Empty, httpContext.Response.Headers[HeaderNames.ETag]);
} }
[Fact]
public async Task Should_append_authorization_header_as_vary()
{
await sut.OnActionExecutionAsync(executingContext, Next());
Assert.Equal("Authorization", httpContext.Response.Headers[HeaderNames.Vary]);
}
[Fact]
public async Task Should_not_append_null_header_as_vary()
{
await sut.OnActionExecutionAsync(executingContext, () =>
{
cachingManager.AddHeader(null!);
return Task.FromResult(executedContext);
});
Assert.Equal("Authorization", httpContext.Response.Headers[HeaderNames.Vary]);
}
[Fact]
public async Task Should_not_append_empty_header_as_vary()
{
await sut.OnActionExecutionAsync(executingContext, () =>
{
cachingManager.AddHeader(string.Empty);
return Task.FromResult(executedContext);
});
Assert.Equal("Authorization", httpContext.Response.Headers[HeaderNames.Vary]);
}
[Fact]
public async Task Should_append_custom_header_as_vary()
{
await sut.OnActionExecutionAsync(executingContext, () =>
{
cachingManager.AddHeader("X-Header");
return Task.FromResult(executedContext);
});
Assert.Equal("Authorization,X-Header", httpContext.Response.Headers[HeaderNames.Vary]);
}
[Fact] [Fact]
public async Task Should_not_append_etag_if_empty() public async Task Should_not_append_etag_if_empty()
{ {

Loading…
Cancel
Save