diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
index 4fb62c39d..58e5ca682 100644
--- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
+++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
@@ -14,6 +14,7 @@ using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
+using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
using Squidex.Pipeline;
@@ -62,6 +63,8 @@ namespace Squidex.Areas.Api.Controllers.Apps
var response = entities.Select(a => AppDto.FromApp(a, subject, appPlansProvider)).ToList();
+ Response.Headers["Etag"] = string.Join(";", response.Select(x => $"{x.Id}{x.Version}")).Sha256Base64();
+
return Ok(response);
}
diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
index 0b3f722de..015c70773 100644
--- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
@@ -12,10 +12,11 @@ using NodaTime;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
+using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
- public sealed class AssetDto
+ public sealed class AssetDto : IGenerateEtag
{
///
/// The id of the asset.
@@ -97,6 +98,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
///
public long Version { get; set; }
+ public string GenerateETag()
+ {
+ return $"{Id}{Version}";
+ }
+
public static AssetDto FromAsset(IAssetEntity asset)
{
return SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() });
diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs
index 6e81fa113..c4192c9e6 100644
--- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs
@@ -9,10 +9,11 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
+using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
- public sealed class AssetsDto
+ public sealed class AssetsDto : IGenerateEtag
{
///
/// The assets.
@@ -25,6 +26,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
///
public long Total { get; set; }
+ public string GenerateETag()
+ {
+ return string.Join(";", Items?.Select(x => x.GenerateETag()) ?? Enumerable.Empty()).Sha256Base64();
+ }
+
public static AssetsDto FromAssets(IResultList assets)
{
return new AssetsDto { Total = assets.Total, Items = assets.Select(AssetDto.FromAsset).ToArray() };
diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
index 97093cdc2..e111a4edd 100644
--- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
@@ -16,10 +16,11 @@ using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
+using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Contents.Models
{
- public sealed class ContentDto
+ public sealed class ContentDto : IGenerateEtag
{
///
/// The if of the content item.
@@ -79,6 +80,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
///
public long Version { get; set; }
+ public string GenerateETag()
+ {
+ return $"{Id}{Version}";
+ }
+
public static ContentDto FromCommand(CreateContent command, EntityCreatedResult result)
{
var now = SystemClock.Instance.GetCurrentInstant();
diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
index 12c19cd9c..71c54dd66 100644
--- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
+++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs
@@ -5,9 +5,13 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System.Linq;
+using Squidex.Infrastructure;
+using Squidex.Pipeline;
+
namespace Squidex.Areas.Api.Controllers.Contents.Models
{
- public sealed class ContentsDto
+ public sealed class ContentsDto : IGenerateEtag
{
///
/// The total number of content items.
@@ -18,5 +22,10 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models
/// The content items.
///
public ContentDto[] Items { get; set; }
+
+ public string GenerateETag()
+ {
+ return string.Join(";", Items?.Select(x => x.GenerateETag()) ?? Enumerable.Empty()).Sha256Base64();
+ }
}
}
diff --git a/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs b/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs
index cceb3a4b7..4d145f1fa 100644
--- a/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs
+++ b/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs
@@ -27,18 +27,18 @@ namespace Squidex.Areas.Frontend.Middlewares
public async Task Invoke(HttpContext context)
{
- var buffer = new MemoryStream();
- var body = context.Response.Body;
+ var responseBuffer = new MemoryStream();
+ var responseBody = context.Response.Body;
- context.Response.Body = buffer;
+ context.Response.Body = responseBuffer;
await next(context);
- buffer.Seek(0, SeekOrigin.Begin);
+ responseBuffer.Seek(0, SeekOrigin.Begin);
if (context.Response.StatusCode == 200 && IsIndex(context) && IsHtml(context))
{
- using (var reader = new StreamReader(buffer))
+ using (var reader = new StreamReader(responseBuffer))
{
var response = await reader.ReadToEndAsync();
@@ -56,17 +56,17 @@ namespace Squidex.Areas.Frontend.Middlewares
context.Response.Headers["Content-Length"] = memoryStream.Length.ToString();
- await memoryStream.CopyToAsync(body);
+ await memoryStream.CopyToAsync(responseBody);
}
}
}
}
else if (context.Response.StatusCode != 304)
{
- await buffer.CopyToAsync(body);
+ await responseBuffer.CopyToAsync(responseBody);
}
- context.Response.Body = body;
+ context.Response.Body = responseBody;
}
private static string InjectStyles(string response)
diff --git a/src/Squidex/Config/Web/WebServices.cs b/src/Squidex/Config/Web/WebServices.cs
index fbb865fa1..2e0190fcb 100644
--- a/src/Squidex/Config/Web/WebServices.cs
+++ b/src/Squidex/Config/Web/WebServices.cs
@@ -33,7 +33,11 @@ namespace Squidex.Config.Web
services.AddSingletonAs()
.AsSelf();
- services.AddMvc().AddMySerializers();
+ services.AddMvc(options =>
+ {
+ options.Filters.Add();
+ }).AddMySerializers();
+
services.AddCors();
services.AddRouting();
}
diff --git a/src/Squidex/Pipeline/ETagFilter.cs b/src/Squidex/Pipeline/ETagFilter.cs
new file mode 100644
index 000000000..960791b9c
--- /dev/null
+++ b/src/Squidex/Pipeline/ETagFilter.cs
@@ -0,0 +1,44 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace Squidex.Pipeline
+{
+ public sealed class ETagFilter : IAsyncActionFilter
+ {
+ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+ {
+ var resultContext = await next();
+
+ var httpContext = context.HttpContext;
+
+ if (!httpContext.Response.Headers.TryGetValue("Etag", out _) && resultContext.Result is ObjectResult obj && obj.Value is IGenerateEtag g)
+ {
+ var calculatedEtag = g.GenerateETag();
+
+ if (!string.IsNullOrWhiteSpace(calculatedEtag))
+ {
+ httpContext.Response.Headers.Add("Etag", calculatedEtag);
+ }
+ }
+
+ if (httpContext.Request.Method == "GET" &&
+ httpContext.Request.Headers.TryGetValue("If-None-Match", out var noneMatch) &&
+ httpContext.Response.StatusCode == 200 &&
+ httpContext.Response.Headers.TryGetValue("Etag", out var etag) &&
+ !string.IsNullOrWhiteSpace(noneMatch) &&
+ !string.IsNullOrWhiteSpace(etag) &&
+ string.Equals(etag, noneMatch, System.StringComparison.Ordinal))
+ {
+ resultContext.Result = new StatusCodeResult(304);
+ }
+ }
+ }
+}
diff --git a/src/Squidex/Pipeline/IGenerateEtag.cs b/src/Squidex/Pipeline/IGenerateEtag.cs
new file mode 100644
index 000000000..4755fcf27
--- /dev/null
+++ b/src/Squidex/Pipeline/IGenerateEtag.cs
@@ -0,0 +1,14 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Pipeline
+{
+ public interface IGenerateEtag
+ {
+ string GenerateETag();
+ }
+}