Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,15 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle
services.AddScoped<HttpMessageHandlersChainFactory>(serviceProvider => transportHandler =>
{
var constructedHttpMessageHandler = ActivatorUtilities.CreateInstance<LoggingDelegatingHandler>(serviceProvider,
[ActivatorUtilities.CreateInstance<CacheDelegatingHandler>(serviceProvider,
[ActivatorUtilities.CreateInstance<RequestHeadersDelegatingHandler>(serviceProvider,
[ActivatorUtilities.CreateInstance<AuthDelegatingHandler>(serviceProvider,
[ActivatorUtilities.CreateInstance<RetryDelegatingHandler>(serviceProvider,
[ActivatorUtilities.CreateInstance<ExceptionDelegatingHandler>(serviceProvider, [transportHandler])])])])]);
[ActivatorUtilities.CreateInstance<ExceptionDelegatingHandler>(serviceProvider, [transportHandler])])])])])]);
return constructedHttpMessageHandler;
});
services.AddScoped<AuthDelegatingHandler>();
services.AddScoped<CacheDelegatingHandler>();
services.AddScoped<RetryDelegatingHandler>();
services.AddScoped<ExceptionDelegatingHandler>();
services.AddScoped<RequestHeadersDelegatingHandler>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Net;
using Microsoft.Extensions.Caching.Memory;

namespace Boilerplate.Client.Core.Services.HttpMessageHandlers;

internal class CacheDelegatingHandler(IMemoryCache memoryCache, HttpMessageHandler handler)
: DelegatingHandler(handler)
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var logScopeData = (Dictionary<string, object?>)request.Options.GetValueOrDefault(RequestOptionNames.LogScopeData)!;
var memoryCacheStatus = "DYNAMIC";
var useCache = AppEnvironment.IsDev() is false && AppPlatform.IsBlazorHybridOrBrowser;

try
{
var cacheKey = $"{request.Method}-{request.RequestUri}";

if (useCache && memoryCache.TryGetValue(cacheKey, out ResponseMemoryCacheItems? cachedResponse))
{
memoryCacheStatus = "HIT";
var cachedHttpResponse = new HttpResponseMessage(cachedResponse!.StatusCode)
{
Content = new ByteArrayContent(cachedResponse.Content)
};
foreach (var (key, values) in cachedResponse.ResponseHeaders)
{
cachedHttpResponse.Headers.TryAddWithoutValidation(key, values);
}
foreach (var (key, values) in cachedResponse.ContentHeaders)
{
cachedHttpResponse.Content.Headers.TryAddWithoutValidation(key, values);
}
foreach (var l in cachedResponse.LogScopeData)
{
logScopeData[l.Key] = l.Value;
}
request.Options.Set(new(RequestOptionNames.LogLevel), LogLevel.Information);
return cachedHttpResponse;
}

var response = await base.SendAsync(request, cancellationToken);

if (useCache && response.IsSuccessStatusCode && response.Headers.CacheControl?.MaxAge is TimeSpan maxAge && maxAge > TimeSpan.Zero)
{
memoryCacheStatus = "MISS";
var responseContent = await response.Content.ReadAsByteArrayAsync(cancellationToken);
memoryCache.Set(cacheKey, new ResponseMemoryCacheItems
{
Content = responseContent,
StatusCode = response.StatusCode,
ResponseHeaders = response.Headers.ToDictionary(h => h.Key, h => h.Value.ToArray()),
ContentHeaders = response.Content.Headers.ToDictionary(h => h.Key, h => h.Value.ToArray()),
LogScopeData = logScopeData.ToDictionary()
}, maxAge);
}

return response;
}
finally
{
logScopeData["MemoryCacheStatus"] = memoryCacheStatus;
}
}

public class ResponseMemoryCacheItems
{
public required byte[] Content { get; set; }

public required HttpStatusCode StatusCode { get; set; }

public required Dictionary<string, string[]> ResponseHeaders { get; set; }
public required Dictionary<string, string[]> ContentHeaders { get; set; }

public required Dictionary<string, object?> LogScopeData { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
/// The returned chain will include the following handlers, in order:
///
/// 1. <see cref="LoggingDelegatingHandler"/>
/// 2. <see cref="RequestHeadersDelegatingHandler" />
/// 3. <see cref="AuthDelegatingHandler" />
/// 4. <see cref="RetryDelegatingHandler" />
/// 5. <see cref="ExceptionDelegatingHandler" />
/// 2. <see cref="CacheDelegatingHandler"/>
/// 3. <see cref="RequestHeadersDelegatingHandler" />
/// 4. <see cref="AuthDelegatingHandler" />
/// 5. <see cref="RetryDelegatingHandler" />
/// 6. <see cref="ExceptionDelegatingHandler" />
///
/// The chain is constructed in reverse order, with the provided `transportHandler` as the final
/// link. Each subsequent handler in the chain receives the output of the previous one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public async Task<ProductDto> Get(Guid id, CancellationToken cancellationToken)


// This method needs to be implemented based on the logic required in each business.
[EnableQuery, HttpGet("{id}")]
[EnableQuery, HttpGet("{id}"), AppResponseCache(MaxAge = 60 * 5, SharedMaxAge = 0, UserAgnostic = true)]
public IQueryable<ProductDto> GetSimilar(Guid id)
{
var similarProducts = Get()
Expand All @@ -35,7 +35,7 @@ public IQueryable<ProductDto> GetSimilar(Guid id)
return similarProducts;
}

[EnableQuery, HttpGet("{id}")]
[EnableQuery, HttpGet("{id}"), AppResponseCache(MaxAge = 60 * 5, SharedMaxAge = 0, UserAgnostic = true)]
public IQueryable<ProductDto> GetSiblings(Guid id)
{
var siblings = Get().Where(t => t.CategoryId == id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ public static void AddServerApiProjectServices(this WebApplicationBuilder builde
var builder = policy.AddPolicy<AppResponseCachePolicy>();
}, excludeDefaultPolicy: true);
});
services.AddMemoryCache();

services.AddHttpContextAccessor();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public async ValueTask CacheRequestAsync(OutputCacheContext context, Cancellatio
responseCacheAtt.SharedMaxAge = responseCacheAtt.MaxAge;
}

var browserCacheTtl = responseCacheAtt.MaxAge;
var clientCacheTtl = responseCacheAtt.MaxAge;
var edgeCacheTtl = responseCacheAtt.SharedMaxAge;
var outputCacheTtl = responseCacheAtt.SharedMaxAge;

Expand All @@ -59,10 +59,10 @@ public async ValueTask CacheRequestAsync(OutputCacheContext context, Cancellatio
if (context.HttpContext.IsBlazorPageContext() && CultureInfoManager.MultilingualEnabled)
{
// Note: Currently, we are not keeping the current culture in the URL.
// The edge and browser caches do not support such variations, although the output cache does.
// As a temporary solution, browser and edge caching are disabled for pre-rendered pages.
// The edge and client caches do not support such variations, although the output cache does.
// As a temporary solution, client and edge caching are disabled for pre-rendered pages.
edgeCacheTtl = -1;
browserCacheTtl = -1;
clientCacheTtl = -1;
}
//#endif

Expand All @@ -72,13 +72,13 @@ public async ValueTask CacheRequestAsync(OutputCacheContext context, Cancellatio
outputCacheTtl = -1;
}

// Edge - Browser Cache
if (browserCacheTtl != -1 || edgeCacheTtl != -1)
// Edge - Client Cache
if (clientCacheTtl != -1 || edgeCacheTtl != -1)
{
context.HttpContext.Response.GetTypedHeaders().CacheControl = new()
{
Public = edgeCacheTtl > 0,
MaxAge = browserCacheTtl == -1 ? null : TimeSpan.FromSeconds(browserCacheTtl),
MaxAge = clientCacheTtl == -1 ? null : TimeSpan.FromSeconds(clientCacheTtl),
SharedMaxAge = edgeCacheTtl == -1 ? null : TimeSpan.FromSeconds(edgeCacheTtl)
};
context.HttpContext.Response.Headers.Remove("Pragma");
Expand All @@ -96,7 +96,7 @@ public async ValueTask CacheRequestAsync(OutputCacheContext context, Cancellatio
//#if (api == "Integrated")
context.HttpContext.Items["AppResponseCachePolicy__DisableStreamPrerendering"] = outputCacheTtl > 0 || edgeCacheTtl > 0;
//#endif
context.HttpContext.Response.Headers.TryAdd("App-Cache-Response", FormattableString.Invariant($"Output:{outputCacheTtl},Edge:{edgeCacheTtl},Browser:{browserCacheTtl}"));
context.HttpContext.Response.Headers.TryAdd("App-Cache-Response", FormattableString.Invariant($"Output:{outputCacheTtl},Edge:{edgeCacheTtl},Client:{clientCacheTtl}"));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ public static void AddServerWebProjectServices(this WebApplicationBuilder builde
var builder = policy.AddPolicy<AppResponseCachePolicy>();
}, excludeDefaultPolicy: true);
});
services.AddMemoryCache();

services.AddHttpContextAccessor();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public async ValueTask CacheRequestAsync(OutputCacheContext context, Cancellatio
responseCacheAtt.SharedMaxAge = responseCacheAtt.MaxAge;
}

var browserCacheTtl = responseCacheAtt.MaxAge;
var clientCacheTtl = responseCacheAtt.MaxAge;
var edgeCacheTtl = responseCacheAtt.SharedMaxAge;
var outputCacheTtl = responseCacheAtt.SharedMaxAge;

Expand All @@ -58,10 +58,10 @@ public async ValueTask CacheRequestAsync(OutputCacheContext context, Cancellatio
if (context.HttpContext.IsBlazorPageContext() && CultureInfoManager.MultilingualEnabled)
{
// Note: Currently, we are not keeping the current culture in the URL.
// The edge and browser caches do not support such variations, although the output cache does.
// As a temporary solution, browser and edge caching are disabled for pre-rendered pages.
// The edge and client caches do not support such variations, although the output cache does.
// As a temporary solution, client and edge caching are disabled for pre-rendered pages.
edgeCacheTtl = -1;
browserCacheTtl = -1;
clientCacheTtl = -1;
}

if (context.HttpContext.Request.IsFromCDN() && edgeCacheTtl > 0)
Expand All @@ -70,13 +70,13 @@ public async ValueTask CacheRequestAsync(OutputCacheContext context, Cancellatio
outputCacheTtl = -1;
}

// Edge - Browser Cache
if (browserCacheTtl != -1 || edgeCacheTtl != -1)
// Edge - Client Cache
if (clientCacheTtl != -1 || edgeCacheTtl != -1)
{
context.HttpContext.Response.GetTypedHeaders().CacheControl = new()
{
Public = edgeCacheTtl > 0,
MaxAge = browserCacheTtl == -1 ? null : TimeSpan.FromSeconds(browserCacheTtl),
MaxAge = clientCacheTtl == -1 ? null : TimeSpan.FromSeconds(clientCacheTtl),
SharedMaxAge = edgeCacheTtl == -1 ? null : TimeSpan.FromSeconds(edgeCacheTtl)
};
context.HttpContext.Response.Headers.Remove("Pragma");
Expand All @@ -92,7 +92,7 @@ public async ValueTask CacheRequestAsync(OutputCacheContext context, Cancellatio
}

context.HttpContext.Items["AppResponseCachePolicy__DisableStreamPrerendering"] = outputCacheTtl > 0 || edgeCacheTtl > 0;
context.HttpContext.Response.Headers.TryAdd("App-Cache-Response", FormattableString.Invariant($"Output:{outputCacheTtl},Edge:{edgeCacheTtl},Browser:{browserCacheTtl}"));
context.HttpContext.Response.Headers.TryAdd("App-Cache-Response", FormattableString.Invariant($"Output:{outputCacheTtl},Edge:{edgeCacheTtl},Client:{clientCacheTtl}"));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Localization" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public static IServiceCollection AddSharedProjectServices(this IServiceCollectio

services.AddLocalization();

services.AddMemoryCache();

return services;
}

Expand Down
Loading