From f6735a38feab54d51cbf9184cb7210c43fca9c10 Mon Sep 17 00:00:00 2001 From: Charles Showalter Date: Tue, 31 May 2022 13:54:41 -0700 Subject: [PATCH] Server Side Caching --- API/Controllers/ProductsController.cs | 4 ++ .../ApplicationServicesExtensions.cs | 1 + API/Helpers/CachedAttributes.cs | 53 ++++++++++++++++++ Core/Interfaces/IResponseCacheService.cs | 8 +++ .../Services/ResponseCacheService.cs | 45 +++++++++++++++ client/src/assets/images/logo.png | Bin 11707 -> 11707 bytes 6 files changed, 111 insertions(+) create mode 100644 API/Helpers/CachedAttributes.cs create mode 100644 Core/Interfaces/IResponseCacheService.cs create mode 100644 Infrastructure/Services/ResponseCacheService.cs diff --git a/API/Controllers/ProductsController.cs b/API/Controllers/ProductsController.cs index 19a6d50..710469b 100644 --- a/API/Controllers/ProductsController.cs +++ b/API/Controllers/ProductsController.cs @@ -24,6 +24,7 @@ namespace API.Controllers } + [CachedAttributes(600)] [HttpGet] public async Task>>> GetProducts([FromQuery]ProductSpecParams productParams) { @@ -35,6 +36,7 @@ namespace API.Controllers return Ok(new Pagination(productParams.PageIndex, productParams.PageSize, totalItems, data)); } + [CachedAttributes(600)] [HttpGet("{id}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] @@ -46,12 +48,14 @@ namespace API.Controllers return _mapper.Map(product); } + [CachedAttributes(600)] [HttpGet("brands")] public async Task>> GetProductBrands() { return Ok(await _productBrandRepo.ListAllAsync()); } + [CachedAttributes(600)] [HttpGet("types")] public async Task>> GetProductTypes() { diff --git a/API/Extensions/ApplicationServicesExtensions.cs b/API/Extensions/ApplicationServicesExtensions.cs index c3ea36d..e364988 100644 --- a/API/Extensions/ApplicationServicesExtensions.cs +++ b/API/Extensions/ApplicationServicesExtensions.cs @@ -10,6 +10,7 @@ namespace API.Extensions { public static IServiceCollection AddApplicationServices(this IServiceCollection services) { + services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Helpers/CachedAttributes.cs b/API/Helpers/CachedAttributes.cs new file mode 100644 index 0000000..e5a383e --- /dev/null +++ b/API/Helpers/CachedAttributes.cs @@ -0,0 +1,53 @@ +using System.Text; +using Core.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace API.Helpers +{ + public class CachedAttributes : Attribute, IAsyncActionFilter + { + private readonly int _timeToLiveSeconds; + public CachedAttributes(int timeToLiveSeconds) + { + _timeToLiveSeconds = timeToLiveSeconds; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var cacheService = context.HttpContext.RequestServices.GetRequiredService(); + var cacheKey = GenerateCacheKeyFromRequest(context.HttpContext.Request); + var cachedResponse = await cacheService.GetCachedResponseAsync(cacheKey); + + if(!string.IsNullOrEmpty(cachedResponse)) + { + var contentResult = new ContentResult + { + Content = cachedResponse, + ContentType = "application/json", + StatusCode = 200 + }; + context.Result = contentResult; + return; + } + + var executedContext = await next(); + if(executedContext.Result is OkObjectResult okObjectResult) + { + await cacheService.CacheResponseAsync(cacheKey, okObjectResult.Value, TimeSpan.FromSeconds(_timeToLiveSeconds)); + } + } + + private string GenerateCacheKeyFromRequest(HttpRequest request) + { + var keyBuilder = new StringBuilder(); + keyBuilder.Append($"{request.Path}"); + foreach (var (key, value) in request.Query.OrderBy(x => x.Key)) + { + keyBuilder.Append($"|{key}-{value}"); + } + + return keyBuilder.ToString(); + } + } +} \ No newline at end of file diff --git a/Core/Interfaces/IResponseCacheService.cs b/Core/Interfaces/IResponseCacheService.cs new file mode 100644 index 0000000..7940c86 --- /dev/null +++ b/Core/Interfaces/IResponseCacheService.cs @@ -0,0 +1,8 @@ +namespace Core.Interfaces +{ + public interface IResponseCacheService + { + Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive); + Task GetCachedResponseAsync(string cacheKey); + } +} \ No newline at end of file diff --git a/Infrastructure/Services/ResponseCacheService.cs b/Infrastructure/Services/ResponseCacheService.cs new file mode 100644 index 0000000..05ddd05 --- /dev/null +++ b/Infrastructure/Services/ResponseCacheService.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Core.Interfaces; +using StackExchange.Redis; + +namespace Infrastructure.Services +{ + public class ResponseCacheService : IResponseCacheService + { + private readonly IDatabase _database; + public ResponseCacheService(IConnectionMultiplexer redis) + { + _database = redis.GetDatabase(); + } + + public async Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive) + { + if(response == null) + { + return; + } + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var serializedResponse = JsonSerializer.Serialize(response, options); + await _database.StringSetAsync(cacheKey, serializedResponse, timeToLive); + } + + public async Task GetCachedResponseAsync(string cacheKey) + { + var cachedResponse = await _database.StringGetAsync(cacheKey); + if(cachedResponse.IsNullOrEmpty){ + return null; + } + + return cachedResponse; + } + } +} \ No newline at end of file diff --git a/client/src/assets/images/logo.png b/client/src/assets/images/logo.png index 6a7d0ca9fbdcaede5a6a635807ba0ecffbcec8a3..c61c677e89f3185a83f00747fcb8eaa801ae3bcf 100644 GIT binary patch delta 2187 zcmcJQ`#aMOAIIlVO{BFINe;_lq~=r<8HOH*PzsxwL+Z(CTA9e9Z&>7zGUY6X9MVv% zVTZI9Q`n7axMfVFEzate>Wqkt#0^2*;+g^w%o+}Y%4?FI?(;H5Zroly!d`NO~yGlLaha^PMsUr1MwU z?8DR>b1$Cj-1fdqi^9^W1`B^l1srBCEI(Kfe(I3smzNP~qdaOEZKW9*QMtCUlvRBHUZsOkxvKfv98cII6|aGt zol2?lag#siG$84Z&ytt_8s)Fv&y6`Y=_YrG)QuU))t>|~hTDguZ|>;()>Qh0(cgd` zY4`SRNOj=+ePFyM0q<72ZI{xj14(0TcNjukO{W)US_se8WvQ9P>4 zhbNB#Y+gUeg+TqJUNAx!>b(QAExp%!1HWB&L0ijqQ4F^Vi(B0rFS?+smgIHIvi#9L z!kP@ozil&&Rhxb_qaiKc+iULQ*ZUq`b8U6Q${?RtH})79Ux5Y{3Vd0glB76y?V(F& zeq~MZ^q6zV&Pk3|uwa3bkG2LuRLwK@eV_n7!~AyTQ2s&AS%V6OpfLZU?l=;gh2I9B zkAX2aek5q8zKo@bXb%J9|3Y3yqJ@ADZ1Q2ffBPIj<+ z@Id?mBa`OvIzZQ3DJM80t|!tgWcuRxke$tGZjpnzms?Z26Dzh^?nG&6NcKY(X-p@q zO|86*-+_!yx;ltYufI~ui@E~1gy@7T)hpOKKTarqzDIbYeoBKn;uOJY={pXOw^T)C zLp^E^hzzBocj@a#w^g_@ol?$l)ig7#I>vH*ML4m~u7e=IBIf$2H!7Wv{Z-#CDrBU% z){J!gXArE0aPiww|5HswAAD6eyjG1I=*#gj1z8s|hsrXJb{6}6i39q(AeEIPBgHlS zWIwODhT4o0qJq3aQPxDpU?Nr3yjt?}erMcG{-r8Hx4S$kS?5;PE9RfxJ}yKHQGe^a4bEwDYVq+cT%x=)DNx1%h`=}%$b`9o*1~M3!d=h zH6!f5N0}G!5ehi$BQDWVGs`oNay>7zUoJY-eV#V$%LNMc-e`!3T;KLyo(WPkfI3N- z{^A*P0YYto-`g}}jfh5D0DC>8E74dEG?=xRR{&o(#E_&n~4c^DjvbA;$-#Y&gony#yRiIf}p)Y5=W;7Tr<-)^M0TKzQ$QT$6a3s>dnd|ZtDI@c#_VJb^+zrJPkha>}-gj zBmg_3c=r&g6&Dx+H2$n5T$IcnEl`2;7}wG!9K_#@BX&1jqnpj?oOn6yc2=A=VXA^i z6AG4`ye$9C!_uaUuPRdEd6zE5@5Fa;+`11DL3+IFHFhV)rv>Y^aXE?aK8Bk}y?lm&Ebp`A+ftIJ(wQ-&gnQQGXDf5g@_OdcY zE2hRLw%K%~c%toALj-gCP5N_XwO5Gmjxygs?dBJ8Ab0(H;3oPsW4Gf=4A=S4@|J6_ zj=fLDU*=+1AD@|eJKW=f9ra2}l3{ delta 2187 zcmV;62z2+mTf1A3BYz+ONkldMW~@J5N3HDSDpSQKO_12faX5==P0xkvicnYvReueG=U4II=6d)Jq?jdj~&;cmm z0=)xlvb#i=0xrx`Cc8^?Dc}O#Jp(QUIsgS+pm%^xc9-Z< zzy-Q{23!hs0DlU&K<@yX>@LxzfD3f@47e2N02J60fo@)t)&cg=U7|~YtQ6RjfG+ut zeE!&?Z^Ks+8F}{F#uH<=)2uPy5RnV|tjEFau%}XBPXT&fS@@^#SfEF~*eI5mH%ks{ zzfkp>#+jFF##}ce$GPp4?qc6ib(FC{Ak*74 zwV`2kT~UaLawV{nMI22;^NA?a#zu&!i{-LePek1YBsyG2f`|qa(f2m}&my8%9gKxNKG?2by9auC zIHnRYN9(A#tm;~NsPr)hM9Zbi?}=!Z!v|t%Ab;q$FAjvq@@)S!sLVf4C88@V{r*iv zmlM%y&b!U@*g!XK&91dM;u+h^Be8`0BI1 zJC>#n`83CkY|&l1sPdTbE6;osiQkqS)+7?^Fbd@{1Uukgm zJ3;BBlbpU+6V>?YBc2*b?_T|d(`PvSSa~?ER>db$Rh(

PbW!O&7qQh=1sQ(>S_L z2HInT01k8G0sR9aI@WTF&mp2)hzN(k+U$UiXU;UKE>L3tN-KE(NkpFz5&qpZeUa&0 z1mgS}8~T|=c8sZW9oPtX916x!r~+she_OpGlZog`%iJ(0aC%567$3BOoo1X3-7pU; zE+ERoR-w-iDhmvzX0;2$eSe!aCh9Z03JXHaQ30%<3iSgV3B3fY?13*9C z1PB0V8WF+jZfA_+Z9GKsEtauyIIWyO2fEeT+g2SKG{>RmT}U?Pwtr1O^ML+eA_8d4 zttbtA-h6mc7Odik#W1&LiRfa>Z;*m;k@7e?kE!x5dL8Jen#RUnendpawTUWi6gk`a zt{w3l3!Gc~K(}{%5Mi^nO(#bFQvYhVhql45%1RH6ej5DQdt_bWW!l^byH$&}oFig^ zv+;CYY&^IbxC1s^w|~B865Y*eLN=?r8R+_RF!n&qH@94XZfBv+M*Cd}^skBNFv|oX zMSf`JUiMsqugh(5FY_Aes(AIonLpO4uAK*wuv*`JV&&~|^{-9>k+p4B=a zq`lPd>FYZ`PaUmsZqvDNF