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);
}