diff --git a/E-Commerce.Domain/Contracts/ICacheRepository.cs b/E-Commerce.Domain/Contracts/ICacheRepository.cs new file mode 100644 index 0000000..c1c1a12 --- /dev/null +++ b/E-Commerce.Domain/Contracts/ICacheRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace E_Commerce.Domain.Contracts +{ + public interface ICacheRepository + { + Task GetAsync(string Cachekey); + Task SetAsync(string Cachekey, string Cachevalue, TimeSpan TimeToLive); + + } +} diff --git a/E-Commerce.Persistance/Repositories/CacheRepository.cs b/E-Commerce.Persistance/Repositories/CacheRepository.cs new file mode 100644 index 0000000..971ef53 --- /dev/null +++ b/E-Commerce.Persistance/Repositories/CacheRepository.cs @@ -0,0 +1,30 @@ +using E_Commerce.Domain.Contracts; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace E_Commerce.Persistance.Repositories +{ + public class CacheRepository : ICacheRepository + { + private readonly IDatabase _database; + public CacheRepository(IConnectionMultiplexer connection) + { + _database = connection.GetDatabase(); + } + + public async Task GetAsync(string Cachekey) + { + var CacheValue = await _database.StringGetAsync(Cachekey); + return CacheValue.IsNullOrEmpty ? null : CacheValue.ToString(); + } + + public async Task SetAsync(string Cachekey, string Cachevalue, TimeSpan TimeToLive) + { + await _database.StringSetAsync(Cachekey, Cachevalue, TimeToLive); + } + } +} diff --git a/E-Commerce.Presentaion/Attributes/RedisCacheAttribute.cs b/E-Commerce.Presentaion/Attributes/RedisCacheAttribute.cs new file mode 100644 index 0000000..90e15bf --- /dev/null +++ b/E-Commerce.Presentaion/Attributes/RedisCacheAttribute.cs @@ -0,0 +1,72 @@ +using E_Commerce.Services.Abstraction; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace E_Commerce.Presentaion.Attributes +{ + internal class RedisCacheAttribute : ActionFilterAttribute + { + private readonly int _durationInMinutes; + + public RedisCacheAttribute(int DurationInMinutes = 5) + { + _durationInMinutes = DurationInMinutes; + } + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // 1.Get Cache Service From Dependancy Ingection Container + var CacheService = context.HttpContext.RequestServices.GetRequiredService(); + + // 2.Create Cache Key Based On Request Path And Query String + var CacheKey = CreateCacheKey(context.HttpContext.Request); + + // 3.Check If Cached Data Exists + var CahceValue = await CacheService.GetAsync(CacheKey); + + // 4.If Exists , Return Cached Data and Skip Executing Of Endpoint + if (CahceValue is not null) + { + context.Result = new ContentResult() + { + Content = CahceValue, + ContentType = "application/Json", + StatusCode = StatusCodes.Status200OK + }; + return; + } + + // 5.If Not Exists , Excute Endpoint And Store The Data In Cache if 200 OK Response + var ExcutedContext = await next.Invoke(); + if(ExcutedContext.Result is OkObjectResult result) + { + await CacheService.SetAsync(CacheKey, result.Value!,TimeSpan.FromMinutes(_durationInMinutes)); + } + + } + + // /api/Products + // /api/Products?brandId=2&typeId=1 + // /api/Products?typeId=1 + // /api/Products?typeId=1&brandId=2 + + + + private string CreateCacheKey(HttpRequest request) + { + StringBuilder key = new StringBuilder(); + key.Append(request.Path); // /api/Products + foreach (var item in request.Query.OrderBy(x => x.Key)) + key.Append($"| {item.Key}-{item.Value}"); + return key.ToString(); + } + + + } +} diff --git a/E-Commerce.Presentaion/Controllers/ProductController.cs b/E-Commerce.Presentaion/Controllers/ProductController.cs index 219af7a..9e215e6 100644 --- a/E-Commerce.Presentaion/Controllers/ProductController.cs +++ b/E-Commerce.Presentaion/Controllers/ProductController.cs @@ -1,6 +1,8 @@ -using E_Commerce.Services.Abstraction; +using E_Commerce.Presentaion.Attributes; +using E_Commerce.Services.Abstraction; using E_Commerce.Shared; using E_Commerce.Shared.DTOs.ProductDTOs; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; @@ -24,6 +26,7 @@ public ProductController(IProductService productService) //GetAllProducts //Get : BaseUrl/api/Products [HttpGet] + [RedisCache] public async Task>> GetAllProducts([FromQuery] ProductQueryParams queryParams) { var Products = await _productService.GetAllProductAsync(queryParams); @@ -35,7 +38,9 @@ public async Task>> GetAllProducts([Fro [HttpGet("{id}")] public async Task> GetProduct(int id) { + //throw new Exception(); var Product = await _productService.GetProductByIdAsync(id); + return Ok(Product); } diff --git a/E-Commerce.Presentaion/E-Commerce.Presentaion.csproj b/E-Commerce.Presentaion/E-Commerce.Presentaion.csproj index 535f0fe..0d1399b 100644 --- a/E-Commerce.Presentaion/E-Commerce.Presentaion.csproj +++ b/E-Commerce.Presentaion/E-Commerce.Presentaion.csproj @@ -16,5 +16,9 @@ + + + + diff --git a/E-Commerce.Services.Abstraction/ICacheService.cs b/E-Commerce.Services.Abstraction/ICacheService.cs new file mode 100644 index 0000000..4457298 --- /dev/null +++ b/E-Commerce.Services.Abstraction/ICacheService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace E_Commerce.Services.Abstraction +{ + public interface ICacheService + { + Task GetAsync(string Cachekey); + Task SetAsync(string Cachekey, object Cachevalue, TimeSpan TimeToLive); + + } +} diff --git a/E-Commerce.Services/BasketService.cs b/E-Commerce.Services/BasketService.cs index a742a22..4f5a46e 100644 --- a/E-Commerce.Services/BasketService.cs +++ b/E-Commerce.Services/BasketService.cs @@ -2,6 +2,7 @@ using E_Commerce.Domain.Contracts; using E_Commerce.Domain.Entities.BasketModule; using E_Commerce.Services.Abstraction; +using E_Commerce.Services.Exceptions; using E_Commerce.Shared.DTOs.BasketDTOs; using System; using System.Collections.Generic; @@ -35,7 +36,10 @@ public async Task CreateOrUpdateBasketAsync(BasketDTO basketDTO) public async Task GetBasketAsync(string id) { var Basket = await _basketRepository.GetBasketAsync(id); - return _mapper.Map(Basket!); + + if (Basket == null) + throw new BasketNotFoundException(id); + return _mapper.Map(Basket); } } } diff --git a/E-Commerce.Services/CacheService.cs b/E-Commerce.Services/CacheService.cs new file mode 100644 index 0000000..3f14c57 --- /dev/null +++ b/E-Commerce.Services/CacheService.cs @@ -0,0 +1,36 @@ +using E_Commerce.Domain.Contracts; +using E_Commerce.Services.Abstraction; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace E_Commerce.Services +{ + public class CacheService : ICacheService + { + private readonly ICacheRepository _cacheRepository; + + public CacheService(ICacheRepository cacheRepository) + { + this._cacheRepository = cacheRepository; + } + + public async Task GetAsync(string Cachekey) + { + return await _cacheRepository.GetAsync(Cachekey); + } + + public async Task SetAsync(string Cachekey, object Cachevalue, TimeSpan TimeToLive) + { + var Value = JsonSerializer.Serialize(Cachevalue,new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + await _cacheRepository.SetAsync(Cachekey, Value, TimeToLive); + + } + } +} diff --git a/E-Commerce.Services/Exceptions/NotFoundException.cs b/E-Commerce.Services/Exceptions/NotFoundException.cs new file mode 100644 index 0000000..a9c107d --- /dev/null +++ b/E-Commerce.Services/Exceptions/NotFoundException.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace E_Commerce.Services.Exceptions +{ + public abstract class NotFoundException(string Message) : Exception(Message) + { + + } + + public sealed class ProductNotFoundException(int id) : NotFoundException($"Product With Id {id} is Not Found") + { + + } + + public sealed class BasketNotFoundException(string id) : NotFoundException($"Basket With Id {id} is Not Found") + { + + } +} diff --git a/E-Commerce.Services/ProductService.cs b/E-Commerce.Services/ProductService.cs index d90e59e..d7ea8e2 100644 --- a/E-Commerce.Services/ProductService.cs +++ b/E-Commerce.Services/ProductService.cs @@ -2,6 +2,7 @@ using E_Commerce.Domain.Contracts; using E_Commerce.Domain.Entities.ProductModule; using E_Commerce.Services.Abstraction; +using E_Commerce.Services.Exceptions; using E_Commerce.Services.Specifications; using E_Commerce.Shared; using E_Commerce.Shared.DTOs.ProductDTOs; @@ -58,6 +59,10 @@ public async Task GetProductByIdAsync(int id) { var spec = new ProductWithTypeAndBrandSpecification(id); var Product = await _unitOfWork.GetRepository().GetByIdAsync(spec); + + if (Product == null) + throw new ProductNotFoundException(id); + return _mapper.Map(Product); } } diff --git a/E-CommerceProject/CustomMiddleWares/ExceptionHandlerMiddleWare.cs b/E-CommerceProject/CustomMiddleWares/ExceptionHandlerMiddleWare.cs new file mode 100644 index 0000000..f334e4e --- /dev/null +++ b/E-CommerceProject/CustomMiddleWares/ExceptionHandlerMiddleWare.cs @@ -0,0 +1,64 @@ +using E_Commerce.Services.Exceptions; +using Microsoft.AspNetCore.Mvc; + +namespace E_CommerceProject.CustomMiddleWares +{ + public class ExceptionHandlerMiddleWare + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionHandlerMiddleWare(RequestDelegate Next,ILogger logger) + { + _next = Next; + this._logger = logger; + } + + public async Task Invoke(HttpContext httpcontext) + { + try + { + await _next.Invoke(httpcontext); + await HandleNotFoundEndPointAsync(httpcontext); + } + catch (Exception ex) + { + //1.Logging + + _logger.LogError(ex, "Something Went Wrong !"); + + //2.Return Custom Error Respone + + var Problem = new ProblemDetails() + { + Title = "An Unexpected Error Occurred !", + Detail = ex.Message, + Instance = httpcontext.Request.Path, + Status = ex switch + { + NotFoundException => StatusCodes.Status404NotFound, + _ => StatusCodes.Status500InternalServerError + } + }; + httpcontext.Response.StatusCode = Problem.Status.Value; + await httpcontext.Response.WriteAsJsonAsync(Problem); + } + + } + + private static async Task HandleNotFoundEndPointAsync(HttpContext httpcontext) + { + if (httpcontext.Response.StatusCode == StatusCodes.Status404NotFound) + { + var Problem = new ProblemDetails() + { + Title = "Error Will Processing The Http Request - EndPoint Not Found !", + Status = StatusCodes.Status404NotFound, + Detail = $"EndPoint {httpcontext.Request.Path} Not Found", + Instance = httpcontext.Request.Path + }; + await httpcontext.Response.WriteAsJsonAsync(Problem); + } + } + } +} diff --git a/E-CommerceProject/Factories/ApiResponseFactory.cs b/E-CommerceProject/Factories/ApiResponseFactory.cs new file mode 100644 index 0000000..d606280 --- /dev/null +++ b/E-CommerceProject/Factories/ApiResponseFactory.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace E_CommerceProject.Factories +{ + public static class ApiResponseFactory + { + public static IActionResult GenerateApiValidationResponse(ActionContext actionContext) + { + var Errors = actionContext.ModelState.Where(x => x.Value.Errors.Count() > 0) + .ToDictionary(x => x.Key, + x => x.Value.Errors.Select(x => x.ErrorMessage.ToArray())); + var Problem = new ProblemDetails + { + Title = "Validation Errors", + Detail = "One or More Validation Errors Occurred.", + Status = StatusCodes.Status400BadRequest, + Extensions = + { + {"Errors",Errors } + } + }; + return new BadRequestObjectResult(Problem); + + } + + } +} diff --git a/E-CommerceProject/Program.cs b/E-CommerceProject/Program.cs index 08aeaa3..68e0784 100644 --- a/E-CommerceProject/Program.cs +++ b/E-CommerceProject/Program.cs @@ -6,7 +6,10 @@ using E_Commerce.Services; using E_Commerce.Services.Abstraction; using E_Commerce.Services.MappingProfiels; +using E_CommerceProject.CustomMiddleWares; using E_CommerceProject.Extentions; +using E_CommerceProject.Factories; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using StackExchange.Redis; using System.Reflection; @@ -44,9 +47,16 @@ public static async Task Main(string[] args) { return ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("RedisConnection")!); }); + builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.Configure(options => + { + options.InvalidModelStateResponseFactory = ApiResponseFactory.GenerateApiValidationResponse; + }); #endregion #region Redis Connection @@ -63,6 +73,27 @@ public static async Task Main(string[] args) #region Configure the HTTP request pipeline. + //app.Use(async (Context, Next) => + //{ + // try + // { + // await Next.Invoke(Context); + + // } + // catch (Exception ex) + // { + // Console.WriteLine(ex.Message); + // Context.Response.StatusCode = StatusCodes.Status500InternalServerError; + // await Context.Response.WriteAsJsonAsync(new + // { + // StatusCode = StatusCodes.Status500InternalServerError, + // Error = $" An Unexpected Error Occurred : {ex.Message}" + // }); + // } + //}); + + app.UseMiddleware(); + if (app.Environment.IsDevelopment()) { app.UseSwagger(); diff --git a/E-CommerceProject/appsettings.Development.json b/E-CommerceProject/appsettings.Development.json index 09cb6cf..7da8afb 100644 --- a/E-CommerceProject/appsettings.Development.json +++ b/E-CommerceProject/appsettings.Development.json @@ -8,6 +8,9 @@ "ConnectionStrings": { "DefaultConnection": " Server =.; Database =Ecommerce; Trusted_Connection=True;TrustServerCertificate=True;", "RedisConnection": "localhost" + //"RedisConnection": "127.0.0.1:6379" + +