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 6a7d0ca..c61c677 100644 Binary files a/client/src/assets/images/logo.png and b/client/src/assets/images/logo.png differ