diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ODataQuery.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ODataQuery.cs index 6b7df90d72..58f9dd2446 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ODataQuery.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ODataQuery.cs @@ -26,7 +26,7 @@ public string OrFilter public string? Expand { get; set; } public string? Search { get; set; } - public override string ToString() + public override string? ToString() { var qs = new AppQueryStringCollection(); @@ -68,5 +68,5 @@ public override string ToString() return qs.ToString(); } - public static implicit operator string(ODataQuery query) => query.ToString(); + public static implicit operator string?(ODataQuery query) => query.ToString(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs index 3d9401d9bb..b7cb47b8b5 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs @@ -45,6 +45,8 @@ public partial class ServerApiSettings : SharedSettings public CloudflareOptions? Cloudflare { get; set; } //#endif + public ResponseCachingOptions ResponseCaching { get; set; } = default!; + /// /// Defines the list of origins permitted for CORS access to the API. These origins are also valid for use as return URLs after social sign-ins and for generating URLs in emails. /// @@ -81,6 +83,7 @@ public override IEnumerable Validate(ValidationContext validat { Validator.TryValidateObject(ForwardedHeaders, new ValidationContext(ForwardedHeaders), validationResults, true); } + Validator.TryValidateObject(ResponseCaching, new ValidationContext(ResponseCaching), validationResults, true); if (AppEnvironment.IsDev() is false) { @@ -211,3 +214,16 @@ public partial class SmsOptions string.IsNullOrEmpty(TwilioAccountSid) is false && string.IsNullOrEmpty(TwilioAutoToken) is false; } + +public class ResponseCachingOptions +{ + /// + /// Enables ASP.NET Core's response output caching + /// + public bool EnableOutputCaching { get; set; } + + /// + /// Enables CDN's edge servers caching + /// + public bool EnableCdnEdgeCaching { get; set; } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/AppResponseCachePolicy.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/AppResponseCachePolicy.cs index 14f45083ef..eef6a32d2c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/AppResponseCachePolicy.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/AppResponseCachePolicy.cs @@ -7,7 +7,7 @@ namespace Boilerplate.Server.Api.Services; /// /// An implementation of this interface can update how the current request is cached. /// -public class AppResponseCachePolicy(IHostEnvironment env) : IOutputCachePolicy +public class AppResponseCachePolicy(ServerApiSettings settings) : IOutputCachePolicy { /// /// Updates the before the cache middleware is invoked. @@ -39,12 +39,13 @@ public async ValueTask CacheRequestAsync(OutputCacheContext context, Cancellatio var edgeCacheTtl = responseCacheAtt.SharedMaxAge; var outputCacheTtl = responseCacheAtt.SharedMaxAge; - if (env.IsDevelopment()) + if (settings.ResponseCaching.EnableCdnEdgeCaching is false) { - // To enhance the developer experience, return from here to make it easier for developers to debug cacheable responses. edgeCacheTtl = -1; + } + if (settings.ResponseCaching.EnableOutputCaching is false) + { outputCacheTtl = -1; - browserCacheTtl = -1; } if (context.HttpContext.User.IsAuthenticated() && responseCacheAtt.UserAgnostic is false) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ResponseCacheService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ResponseCacheService.cs index 9293c562ec..d09f61839d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ResponseCacheService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ResponseCacheService.cs @@ -28,7 +28,7 @@ public async Task PurgeCache(params string[] relativePaths) //#if (cloudflare == true) await PurgeCloudflareCache(relativePaths); //#else - // If you're using CDNs like GCore or others, make sure to purge the Edge Cache of your CDN. + // If you're using CDN like GCore or others, make sure to purge the Edge Cache of your CDN. // The Cloudflare Cache API is already integrated into the Boilerplate, but for other CDNs, // you'll need to implement the caching logic yourself. if (httpContextAccessor.HttpContext!.Request.IsFromCDN()) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json index a08606d826..3558634bda 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json @@ -148,5 +148,9 @@ "AdditionalDomains_Comment": "The ResponseCacheService clears the cache for the current domain by default. If multiple Cloudflare-hosted domains point to your backend, you will need to purge the cache for each of them individually." }, //#endif + "ResponseCaching": { + "EnableOutputCaching": false, + "EnableCdnEdgeCaching": false + }, "$schema": "https://json.schemastore.org/appsettings.json" } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs index 73c457787e..0419677c8e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs @@ -87,7 +87,7 @@ public static void ConfigureMiddlewares(this WebApplication app) { if (env.IsDevelopment() is false) { - // Caching static files on the Browser and CDNs' edge servers. + // Caching static files on the Browser and CDN's edge servers. if (context.Request.Query.Any(q => string.Equals(q.Key, "v", StringComparison.InvariantCultureIgnoreCase)) && env.WebRootFileProvider.GetFileInfo(context.Request.Path).Exists) { @@ -297,7 +297,7 @@ private static void Configure_401_403_404_Pages(WebApplication app) var qs = AppQueryStringCollection.Parse(httpContext.Request.QueryString.Value ?? string.Empty); qs.Remove("try_refreshing_token"); - var returnUrl = UriHelper.BuildRelative(httpContext.Request.PathBase, httpContext.Request.Path, new QueryString($"?{qs}")); + var returnUrl = UriHelper.BuildRelative(httpContext.Request.PathBase, httpContext.Request.Path, new QueryString(qs.ToString())); httpContext.Response.Redirect($"{Urls.NotAuthorizedPage}?return-url={returnUrl}&isForbidden={(is403 ? "true" : "false")}"); } else if (httpContext.Response.StatusCode is 404 && diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/ServerWebSettings.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/ServerWebSettings.cs index 70631e218f..e109d6f55e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/ServerWebSettings.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/ServerWebSettings.cs @@ -7,12 +7,29 @@ public partial class ServerWebSettings : ClientWebSettings { public ForwardedHeadersOptions ForwardedHeaders { get; set; } = default!; + public ResponseCachingOptions ResponseCaching { get; set; } = default!; + public override IEnumerable Validate(ValidationContext validationContext) { var validationResults = base.Validate(validationContext).ToList(); Validator.TryValidateObject(ForwardedHeaders, new ValidationContext(ForwardedHeaders), validationResults, true); + Validator.TryValidateObject(ResponseCaching, new ValidationContext(ResponseCaching), validationResults, true); + return validationResults; } } + +public class ResponseCachingOptions +{ + /// + /// Enables ASP.NET Core's response output caching + /// + public bool EnableOutputCaching { get; set; } + + /// + /// Enables CDN's edge servers caching + /// + public bool EnableCdnEdgeCaching { get; set; } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Services/AppResponseCachePolicy.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Services/AppResponseCachePolicy.cs index f336d1e501..c2920f1645 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Services/AppResponseCachePolicy.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Services/AppResponseCachePolicy.cs @@ -7,7 +7,7 @@ namespace Boilerplate.Server.Web.Services; /// /// An implementation of this interface can update how the current request is cached. /// -public class AppResponseCachePolicy(IHostEnvironment env) : IOutputCachePolicy +public class AppResponseCachePolicy(ServerWebSettings settings) : IOutputCachePolicy { /// /// Updates the before the cache middleware is invoked. @@ -39,12 +39,13 @@ public async ValueTask CacheRequestAsync(OutputCacheContext context, Cancellatio var edgeCacheTtl = responseCacheAtt.SharedMaxAge; var outputCacheTtl = responseCacheAtt.SharedMaxAge; - if (env.IsDevelopment()) + if (settings.ResponseCaching.EnableCdnEdgeCaching is false) { - // To enhance the developer experience, return from here to make it easier for developers to debug cacheable responses. edgeCacheTtl = -1; + } + if (settings.ResponseCaching.EnableOutputCaching is false) + { outputCacheTtl = -1; - browserCacheTtl = -1; } if (context.HttpContext.User.IsAuthenticated() && responseCacheAtt.UserAgnostic is false) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json index e995673c18..8ea04a0b62 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json @@ -101,6 +101,10 @@ } }, //#endif + "ResponseCaching": { + "EnableOutputCaching": false, + "EnableCdnEdgeCaching": false + }, "AllowedHosts": "*", "ForwardedHeaders": { "ForwardedHeaders": "All", diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppQueryStringCollection.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppQueryStringCollection.cs index 1f905e6244..4b354aeacd 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppQueryStringCollection.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AppQueryStringCollection.cs @@ -7,8 +7,11 @@ namespace System; /// public class AppQueryStringCollection() : Dictionary(StringComparer.OrdinalIgnoreCase) { - public override string ToString() + public override string? ToString() { + if (Count == 0) + return null; + return string.Join("&", this.Select(kv => $"{Uri.EscapeDataString(Uri.UnescapeDataString(kv.Key))}={Uri.EscapeDataString(Uri.UnescapeDataString(kv.Value?.ToString() ?? ""))}")); } @@ -29,8 +32,8 @@ public static AppQueryStringCollection Parse(string query) { // Split the pair into key and value using '='. var parts = pair.Split(['='], 2); - string key = parts[0]; - string value = parts.Length > 1 ? parts[1] : string.Empty; + string key = parts.ElementAt(0); + string value = parts.ElementAtOrDefault(1) ?? string.Empty; qsCollection.Add(key, value); }