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 @@ -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();

Expand Down Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public partial class ServerApiSettings : SharedSettings
public CloudflareOptions? Cloudflare { get; set; }
//#endif

public ResponseCachingOptions ResponseCaching { get; set; } = default!;

/// <summary>
/// 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.
/// </summary>
Expand Down Expand Up @@ -81,6 +83,7 @@ public override IEnumerable<ValidationResult> Validate(ValidationContext validat
{
Validator.TryValidateObject(ForwardedHeaders, new ValidationContext(ForwardedHeaders), validationResults, true);
}
Validator.TryValidateObject(ResponseCaching, new ValidationContext(ResponseCaching), validationResults, true);

if (AppEnvironment.IsDev() is false)
{
Expand Down Expand Up @@ -211,3 +214,16 @@ public partial class SmsOptions
string.IsNullOrEmpty(TwilioAccountSid) is false &&
string.IsNullOrEmpty(TwilioAutoToken) is false;
}

public class ResponseCachingOptions
{
/// <summary>
/// Enables ASP.NET Core's response output caching
/// </summary>
public bool EnableOutputCaching { get; set; }

/// <summary>
/// Enables CDN's edge servers caching
/// </summary>
public bool EnableCdnEdgeCaching { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Boilerplate.Server.Api.Services;
/// <summary>
/// An implementation of this interface can update how the current request is cached.
/// </summary>
public class AppResponseCachePolicy(IHostEnvironment env) : IOutputCachePolicy
public class AppResponseCachePolicy(ServerApiSettings settings) : IOutputCachePolicy
{
/// <summary>
/// Updates the <see cref="OutputCacheContext"/> before the cache middleware is invoked.
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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 &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,29 @@ public partial class ServerWebSettings : ClientWebSettings
{
public ForwardedHeadersOptions ForwardedHeaders { get; set; } = default!;

public ResponseCachingOptions ResponseCaching { get; set; } = default!;

public override IEnumerable<ValidationResult> 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
{
/// <summary>
/// Enables ASP.NET Core's response output caching
/// </summary>
public bool EnableOutputCaching { get; set; }

/// <summary>
/// Enables CDN's edge servers caching
/// </summary>
public bool EnableCdnEdgeCaching { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Boilerplate.Server.Web.Services;
/// <summary>
/// An implementation of this interface can update how the current request is cached.
/// </summary>
public class AppResponseCachePolicy(IHostEnvironment env) : IOutputCachePolicy
public class AppResponseCachePolicy(ServerWebSettings settings) : IOutputCachePolicy
{
/// <summary>
/// Updates the <see cref="OutputCacheContext"/> before the cache middleware is invoked.
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@
}
},
//#endif
"ResponseCaching": {
"EnableOutputCaching": false,
"EnableCdnEdgeCaching": false
},
"AllowedHosts": "*",
"ForwardedHeaders": {
"ForwardedHeaders": "All",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ namespace System;
/// </summary>
public class AppQueryStringCollection() : Dictionary<string, object?>(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() ?? ""))}"));
}

Expand All @@ -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);
}

Expand Down
Loading