Skip to content

Commit 92db967

Browse files
Update Cache.cs
1 parent f139216 commit 92db967

File tree

1 file changed

+296
-13
lines changed

1 file changed

+296
-13
lines changed

class/Cache.cs

Lines changed: 296 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.AspNetCore.Http;
22
using Microsoft.Extensions.Caching.Memory;
33
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Primitives;
45
using System.Xml;
56

67
namespace CodeBehind
@@ -116,9 +117,7 @@ public bool HasMatchingController(HttpRequest request, string ControllerName)
116117
{
117118
FormData = request.Form.ToString();
118119
}
119-
catch (Exception)
120-
{
121-
}
120+
catch (Exception) {}
122121

123122
foreach (CacheProperties cache in CachePropertiesList.Caches)
124123
{
@@ -179,9 +178,7 @@ public bool HasMatchingView(HttpRequest request, string ViewPath)
179178
{
180179
FormData = request.Form.ToString();
181180
}
182-
catch (Exception)
183-
{
184-
}
181+
catch (Exception) { }
185182

186183
foreach (CacheProperties cache in CachePropertiesList.Caches)
187184
{
@@ -296,18 +293,304 @@ public class CacheProperties
296293

297294
public class ClientCache
298295
{
299-
private readonly IHeaderDictionary _Headers;
296+
private readonly IHeaderDictionary _headers;
297+
private readonly HttpContext _httpContext;
298+
299+
public ClientCache(IHeaderDictionary headers)
300+
{
301+
_headers = headers ?? throw new ArgumentNullException(nameof(headers));
302+
}
303+
304+
public ClientCache(HttpContext httpContext)
305+
{
306+
_httpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
307+
_headers = httpContext.Response.Headers;
308+
}
309+
310+
/// <summary>
311+
/// Sets basic caching with private cache control
312+
/// </summary>
313+
/// <param name="duration">Duration in seconds</param>
314+
public void Set(int duration)
315+
{
316+
SetPrivate(duration);
317+
}
318+
319+
/// <summary>
320+
/// Sets private caching (browser-only)
321+
/// </summary>
322+
/// <param name="duration">Duration in seconds</param>
323+
public void SetPrivate(int duration)
324+
{
325+
_headers["Cache-Control"] = $"private, max-age={duration}";
326+
_headers["Expires"] = DateTime.UtcNow.AddSeconds(duration).ToString("R");
327+
SetDefaultVaryHeaders();
328+
}
329+
330+
/// <summary>
331+
/// Sets public caching (shared caches)
332+
/// </summary>
333+
/// <param name="duration">Duration in seconds</param>
334+
public void SetPublic(int duration)
335+
{
336+
_headers["Cache-Control"] = $"public, max-age={duration}";
337+
_headers["Expires"] = DateTime.UtcNow.AddSeconds(duration).ToString("R");
338+
SetDefaultVaryHeaders();
339+
}
340+
341+
/// <summary>
342+
/// Sets no-cache headers (revalidate with server)
343+
/// </summary>
344+
public void SetNoCache()
345+
{
346+
_headers["Cache-Control"] = "no-cache, must-revalidate";
347+
_headers["Pragma"] = "no-cache";
348+
_headers["Expires"] = "0";
349+
}
350+
351+
/// <summary>
352+
/// Sets no-store headers (don't cache at all)
353+
/// </summary>
354+
public void SetNoStore()
355+
{
356+
_headers["Cache-Control"] = "no-store, no-cache, must-revalidate";
357+
_headers["Pragma"] = "no-cache";
358+
_headers["Expires"] = "0";
359+
}
360+
361+
/// <summary>
362+
/// Sets immutable content that never changes
363+
/// </summary>
364+
/// <param name="duration">Duration in seconds</param>
365+
public void SetImmutable(int duration)
366+
{
367+
_headers["Cache-Control"] = $"public, max-age={duration}, immutable";
368+
_headers["Expires"] = DateTime.UtcNow.AddSeconds(duration).ToString("R");
369+
}
370+
371+
/// <summary>
372+
/// Sets stale-while-revalidate pattern for background revalidation
373+
/// </summary>
374+
/// <param name="maxAge">Maximum age in seconds</param>
375+
/// <param name="staleWhileRevalidate">Stale while revalidate period in seconds</param>
376+
public void SetStaleWhileRevalidate(int maxAge, int staleWhileRevalidate)
377+
{
378+
_headers["Cache-Control"] = $"public, max-age={maxAge}, stale-while-revalidate={staleWhileRevalidate}";
379+
_headers["Expires"] = DateTime.UtcNow.AddSeconds(maxAge).ToString("R");
380+
}
381+
382+
/// <summary>
383+
/// Sets stale-if-error pattern for serving stale content on errors
384+
/// </summary>
385+
/// <param name="maxAge">Maximum age in seconds</param>
386+
/// <param name="staleIfError">Stale if error period in seconds</param>
387+
public void SetStaleIfError(int maxAge, int staleIfError)
388+
{
389+
_headers["Cache-Control"] = $"public, max-age={maxAge}, stale-if-error={staleIfError}";
390+
_headers["Expires"] = DateTime.UtcNow.AddSeconds(maxAge).ToString("R");
391+
}
392+
393+
/// <summary>
394+
/// Sets ETag for conditional requests
395+
/// </summary>
396+
/// <param name="etag">ETag value (without quotes)</param>
397+
public void SetETag(string etag)
398+
{
399+
if (!string.IsNullOrEmpty(etag))
400+
{
401+
// Ensure ETag is properly formatted with quotes
402+
var formattedEtag = etag.StartsWith("\"") ? etag : $"\"{etag}\"";
403+
_headers["ETag"] = formattedEtag;
404+
}
405+
}
406+
407+
/// <summary>
408+
/// Generates and sets ETag based on content hash
409+
/// </summary>
410+
/// <param name="content">Content bytes to hash</param>
411+
public void SetETagFromContent(byte[] content)
412+
{
413+
if (content != null && content.Length > 0)
414+
{
415+
using var sha256 = System.Security.Cryptography.SHA256.Create();
416+
var hash = sha256.ComputeHash(content);
417+
var etag = Convert.ToBase64String(hash)
418+
.Replace("+", "-")
419+
.Replace("/", "_")
420+
.Replace("=", "");
421+
SetETag(etag);
422+
}
423+
}
424+
425+
/// <summary>
426+
/// Generates and sets ETag based on string content
427+
/// </summary>
428+
/// <param name="content">String content to hash</param>
429+
public void SetETagFromString(string content)
430+
{
431+
if (!string.IsNullOrEmpty(content))
432+
{
433+
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
434+
SetETagFromContent(bytes);
435+
}
436+
}
437+
438+
/// <summary>
439+
/// Generates ETag hash from content (without setting the header)
440+
/// </summary>
441+
/// <param name="content">Content to hash</param>
442+
/// <returns>ETag value without quotes</returns>
443+
public string GenerateETag(byte[] content)
444+
{
445+
if (content == null || content.Length == 0)
446+
return string.Empty;
447+
448+
using var sha256 = System.Security.Cryptography.SHA256.Create();
449+
var hash = sha256.ComputeHash(content);
450+
return Convert.ToBase64String(hash)
451+
.Replace("+", "-")
452+
.Replace("/", "_")
453+
.Replace("=", "");
454+
}
455+
456+
/// <summary>
457+
/// Generates ETag hash from string content (without setting the header)
458+
/// </summary>
459+
/// <param name="content">String content to hash</param>
460+
/// <returns>ETag value without quotes</returns>
461+
public string GenerateETag(string content)
462+
{
463+
if (string.IsNullOrEmpty(content))
464+
return string.Empty;
465+
466+
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
467+
return GenerateETag(bytes);
468+
}
469+
470+
/// <summary>
471+
/// Sets Last-Modified header
472+
/// </summary>
473+
/// <param name="lastModified">Last modified date</param>
474+
public void SetLastModified(DateTime lastModified)
475+
{
476+
_headers["Last-Modified"] = lastModified.ToUniversalTime().ToString("R");
477+
}
478+
479+
/// <summary>
480+
/// Sets custom vary headers
481+
/// </summary>
482+
/// <param name="headers">Headers to vary by</param>
483+
public void SetVaryHeaders(params string[] headers)
484+
{
485+
if (headers != null && headers.Length > 0)
486+
{
487+
_headers["Vary"] = new StringValues(headers);
488+
}
489+
}
490+
491+
/// <summary>
492+
/// Sets default vary headers for conditional requests
493+
/// </summary>
494+
public void SetDefaultVaryHeaders()
495+
{
496+
_headers["Vary"] = "Accept-Encoding, Accept";
497+
}
498+
499+
/// <summary>
500+
/// Checks if client has fresh cached version using If-None-Match
501+
/// </summary>
502+
/// <param name="currentEtag">Current ETag of the resource (without quotes)</param>
503+
/// <returns>True if client has fresh version</returns>
504+
public bool IsFresh(string currentEtag)
505+
{
506+
if (_httpContext == null) return false;
507+
508+
var ifNoneMatch = _httpContext.Request.Headers["If-None-Match"];
509+
if (string.IsNullOrEmpty(ifNoneMatch) || string.IsNullOrEmpty(currentEtag))
510+
return false;
511+
512+
var formattedEtag = currentEtag.StartsWith("\"") ? currentEtag : $"\"{currentEtag}\"";
513+
return ifNoneMatch == formattedEtag;
514+
}
515+
516+
/// <summary>
517+
/// Checks if client has fresh cached version using If-Modified-Since
518+
/// </summary>
519+
/// <param name="lastModified">Last modified date of the resource</param>
520+
/// <returns>True if client has fresh version</returns>
521+
public bool IsFresh(DateTime lastModified)
522+
{
523+
if (_httpContext == null) return false;
524+
525+
var ifModifiedSince = _httpContext.Request.Headers["If-Modified-Since"];
526+
if (DateTime.TryParse(ifModifiedSince, out var clientModifiedSince))
527+
{
528+
return lastModified <= clientModifiedSince.ToUniversalTime();
529+
}
530+
return false;
531+
}
532+
533+
/// <summary>
534+
/// Sends 304 Not Modified response if client has fresh cache
535+
/// </summary>
536+
/// <param name="currentEtag">Current ETag</param>
537+
/// <returns>True if 304 was sent</returns>
538+
public bool TrySendNotModified(string currentEtag)
539+
{
540+
if (_httpContext != null && IsFresh(currentEtag))
541+
{
542+
_httpContext.Response.StatusCode = 304;
543+
return true;
544+
}
545+
return false;
546+
}
547+
548+
/// <summary>
549+
/// Sends 304 Not Modified response if client has fresh cache
550+
/// </summary>
551+
/// <param name="lastModified">Last modified date</param>
552+
/// <returns>True if 304 was sent</returns>
553+
public bool TrySendNotModified(DateTime lastModified)
554+
{
555+
if (_httpContext != null && IsFresh(lastModified))
556+
{
557+
_httpContext.Response.StatusCode = 304;
558+
return true;
559+
}
560+
return false;
561+
}
562+
563+
/// <summary>
564+
/// Sets cache headers for API responses with recommended defaults
565+
/// </summary>
566+
/// <param name="duration">Duration in seconds</param>
567+
public void SetApiCache(int duration)
568+
{
569+
SetPrivate(duration);
570+
_headers["Vary"] = "Accept, Accept-Encoding, Authorization";
571+
}
300572

301-
public ClientCache(IHeaderDictionary Headers)
573+
/// <summary>
574+
/// Sets cache headers for static assets with long expiration
575+
/// </summary>
576+
/// <param name="duration">Duration in seconds (default: 1 year)</param>
577+
public void SetStaticCache(int duration = 31536000) // 1 year
302578
{
303-
_Headers = Headers;
579+
SetPublic(duration);
580+
SetImmutable(duration);
304581
}
305582

306-
public void Set(int Duration)
583+
/// <summary>
584+
/// Clears all cache headers
585+
/// </summary>
586+
public void Clear()
307587
{
308-
_Headers["Cache-Control"] = "private";
309-
_Headers["Expires"] = DateTime.UtcNow.AddSeconds(Duration).ToString("R");
310-
_Headers["Vary"] = "If-Modified-Since, If-None-Match";
588+
_headers.Remove("Cache-Control");
589+
_headers.Remove("Expires");
590+
_headers.Remove("Pragma");
591+
_headers.Remove("ETag");
592+
_headers.Remove("Last-Modified");
593+
_headers.Remove("Vary");
311594
}
312595
}
313596
}

0 commit comments

Comments
 (0)