diff --git a/DevDash/Attributes/CacheAttribute.cs b/DevDash/Attributes/CacheAttribute.cs new file mode 100644 index 0000000..e46f109 --- /dev/null +++ b/DevDash/Attributes/CacheAttribute.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc; +using System.Text; +using DevDash.Services.IService; + +namespace DevDash.Attributes +{ + public class CacheAttribute(int durationInSec) : Attribute, IAsyncActionFilter + { + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var cacheService = context.HttpContext.RequestServices.GetRequiredService(); + + var cacheKey = GenerateCacheKey(context.HttpContext.Request); + + var result = await cacheService.GetCacheValueAsync(cacheKey); + if (!string.IsNullOrEmpty(result)) + { + //return response + context.Result = new ContentResult() + { + ContentType = "application/json", + StatusCode = StatusCodes.Status200OK, + Content = result + }; + return; + } + //Execute the endpoint + var contextResult = await next.Invoke(); + if (contextResult.Result is OkObjectResult okObject) + { + await cacheService.SetCacheValueAsync(cacheKey, okObject, TimeSpan.FromSeconds(durationInSec)); + } + + } + + private string GenerateCacheKey(HttpRequest request) + { + var key = new StringBuilder(); + key.Append(request.Path); + foreach (var item in request.Query.OrderBy(q => q.Key)) + { + key.Append($"|{item.Key}-{item.Value}"); + } + // /api/Products?typeid=1&Sort=pricedesc&PageIndex=1&PageSize=5 + // /api/Products|typeid-1|Sort-pricedesc|PageIndex-1 + return key.ToString(); + + } + } +} diff --git a/DevDash/Controllers/AccountController.cs b/DevDash/Controllers/AccountController.cs index 304c953..d742930 100644 --- a/DevDash/Controllers/AccountController.cs +++ b/DevDash/Controllers/AccountController.cs @@ -20,6 +20,7 @@ using Org.BouncyCastle.Crypto.Generators; using AutoMapper; using DevDash.DTO.Tenant; +using DevDash.Attributes; namespace DevDash.Controllers { @@ -132,6 +133,8 @@ public async Task Logout([FromBody] TokenDTO tokenDTO) [HttpGet("Profile")] [Authorize] + [Cache(2000)] + public async Task GetUserProfile() { var userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; diff --git a/DevDash/Controllers/CommentController.cs b/DevDash/Controllers/CommentController.cs index 9edc032..3422d17 100644 --- a/DevDash/Controllers/CommentController.cs +++ b/DevDash/Controllers/CommentController.cs @@ -1,4 +1,5 @@ using AutoMapper; +using DevDash.Attributes; using DevDash.DTO.Comment; using DevDash.DTO.Sprint; using DevDash.Migrations; @@ -38,6 +39,8 @@ public CommentController(ICommentRepository commentRepo, IIssueRepository dbissu } [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] + [Cache(2000)] + public async Task> GetComments([FromQuery] int IssueId,[FromQuery] string? search, int pageSize = 0, int pageNumber = 1) { try @@ -68,6 +71,8 @@ public async Task> GetComments([FromQuery] int IssueId [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Cache(2000)] + public async Task> GetComment(int id) { try diff --git a/DevDash/Controllers/DashBoardController.cs b/DevDash/Controllers/DashBoardController.cs index 54e22d8..c453171 100644 --- a/DevDash/Controllers/DashBoardController.cs +++ b/DevDash/Controllers/DashBoardController.cs @@ -1,4 +1,5 @@ using AutoMapper; +using DevDash.Attributes; using DevDash.DTO.Account; using DevDash.DTO.Issue; using DevDash.DTO.Project; @@ -58,6 +59,7 @@ IPinnedItemRepository pinnedItemRepository [Authorize] [HttpGet("Tenants")] [ProducesResponseType(StatusCodes.Status200OK)] + [Cache(2000)] public async Task> GetTenantAnalysis([FromQuery] int Tenantid) { try @@ -96,9 +98,11 @@ public async Task> GetTenantAnalysis([FromQuery] int T return _response; } + [Authorize] [HttpGet("Projects")] [ProducesResponseType(StatusCodes.Status200OK)] + [Cache(2000)] public async Task> GetProjectAnalysis([FromQuery] int Projectid) { try @@ -137,23 +141,8 @@ public async Task> GetProjectAnalysis([FromQuery] int } - - - - - - - - - - - - - - - [Authorize] - + [Cache(2000)] [HttpGet("allproject")] public async Task> GetprojectDashboard() { @@ -185,6 +174,7 @@ public async Task> GetprojectDashboard() } [Authorize] + [Cache(2000)] [HttpGet("allissue")] public async Task> GetissueDashboard() { @@ -219,7 +209,7 @@ public async Task> GetissueDashboard() [Authorize] [HttpGet("Calender")] - + [Cache(2000)] public async Task> GetCalendar() { @@ -257,7 +247,7 @@ public async Task> GetCalendar() [Authorize] [HttpGet("Pinneditems")] - + [Cache(2000)] public async Task> GetPinnedItems() { try diff --git a/DevDash/Controllers/IssueController.cs b/DevDash/Controllers/IssueController.cs index 3ec3277..8f88902 100644 --- a/DevDash/Controllers/IssueController.cs +++ b/DevDash/Controllers/IssueController.cs @@ -1,4 +1,5 @@ using AutoMapper; +using DevDash.Attributes; using DevDash.DTO.Issue; using DevDash.model; using DevDash.Repository.IRepository; @@ -36,6 +37,7 @@ public IssueController(IIssueRepository dbissue, IProjectRepository dbProject, I } [HttpGet("backlog")] + [Cache(2000)] public async Task> GetBacklogIssues([FromQuery] int projectId, [FromQuery] string? search, int pageSize = 0, int pageNumber = 1) { try @@ -88,6 +90,7 @@ public async Task> GetBacklogIssues([FromQuery] int pr return _response; } [HttpGet("sprint")] + [Cache(2000)] public async Task> GetSprintIssues([FromQuery] int sprintId, [FromQuery] string? search, int pageSize = 0, int pageNumber = 1) { try @@ -129,6 +132,8 @@ public async Task> GetSprintIssues([FromQuery] int spr [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Cache(2000)] + public async Task> GetIssue(int id) { try @@ -348,9 +353,6 @@ public async Task> CreateSprintIssue([FromQuery] int s } - - - [HttpDelete("{id:int}", Name = "DeleteIssue")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] diff --git a/DevDash/Controllers/NotificationController.cs b/DevDash/Controllers/NotificationController.cs index 1536357..f55ce78 100644 --- a/DevDash/Controllers/NotificationController.cs +++ b/DevDash/Controllers/NotificationController.cs @@ -1,4 +1,5 @@ -using DevDash.model; +using DevDash.Attributes; +using DevDash.model; using DevDash.Repository.IRepository; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -24,6 +25,8 @@ public NotificationController(INotificationRepository notificationRepository,ITe } [HttpGet] + [Cache(2000)] + public async Task> GetNotifications(string? search = null) { try diff --git a/DevDash/Controllers/ProjectController.cs b/DevDash/Controllers/ProjectController.cs index 880dbe2..714b722 100644 --- a/DevDash/Controllers/ProjectController.cs +++ b/DevDash/Controllers/ProjectController.cs @@ -1,5 +1,6 @@ using AutoMapper; using Azure; +using DevDash.Attributes; using DevDash.DTO.Project; using DevDash.DTO.Tenant; using DevDash.DTO.User; @@ -51,6 +52,8 @@ public ProjectController(IProjectRepository dbProject, ITenantRepository dbTenan [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [Cache(2000)] + public async Task> GetProjects([FromQuery] int tenantId, [FromQuery] string? search, int pageSize = 0, int pageNumber = 1) { try @@ -128,6 +131,8 @@ public async Task> GetProjects([FromQuery] int tenantI [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [Cache(2000)] + public async Task> GetProject([FromRoute] int projectId) { try diff --git a/DevDash/Controllers/SearchController.cs b/DevDash/Controllers/SearchController.cs index 231bbdc..8f6f407 100644 --- a/DevDash/Controllers/SearchController.cs +++ b/DevDash/Controllers/SearchController.cs @@ -1,4 +1,5 @@ -using DevDash.model; +using DevDash.Attributes; +using DevDash.model; using DevDash.Repository.IRepository; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; @@ -16,6 +17,8 @@ public SearchController(ISearchRepository searchRepository) } [HttpGet("global")] + [Cache(2000)] + public async Task> GlobalSearch([FromQuery] string query) { try diff --git a/DevDash/Controllers/SprintController.cs b/DevDash/Controllers/SprintController.cs index cd93e48..c7ee1b9 100644 --- a/DevDash/Controllers/SprintController.cs +++ b/DevDash/Controllers/SprintController.cs @@ -1,4 +1,5 @@ using AutoMapper; +using DevDash.Attributes; using DevDash.DTO.Sprint; using DevDash.Migrations; using DevDash.model; @@ -37,6 +38,8 @@ public SprintController(ISprintRepository sprintRepo, IMapper mapper, IProjectRe [HttpGet( Name = "GetSprints")] [ProducesResponseType(StatusCodes.Status200OK)] + [Cache(2000)] + public async Task> GetSprints([FromQuery] int projectid, [FromQuery] string? search, int pageSize = 0, int pageNumber = 1) { try @@ -81,6 +84,8 @@ public async Task> GetSprints([FromQuery] int projecti [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Cache(2000)] + public async Task> GetSprint([FromRoute] int sprintId) { try diff --git a/DevDash/Controllers/TenantController.cs b/DevDash/Controllers/TenantController.cs index bf19690..db67c32 100644 --- a/DevDash/Controllers/TenantController.cs +++ b/DevDash/Controllers/TenantController.cs @@ -1,4 +1,5 @@ using AutoMapper; +using DevDash.Attributes; using DevDash.DTO.Tenant; using DevDash.DTO.UserTenant; using DevDash.Migrations; @@ -38,6 +39,7 @@ public TenantController(ITenantRepository tenantRepo,IMapper mapper [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [Cache(2000)] public async Task> GetTenants([FromQuery] string? search, int pageSize = 0, int pageNumber = 1) { try @@ -92,6 +94,8 @@ public async Task> GetTenants([FromQuery] string? sear [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [Cache(2000)] + public async Task> GetTenant([FromRoute] int tenantId) { try diff --git a/DevDash/DevDash.csproj b/DevDash/DevDash.csproj index aa1851e..970d211 100644 --- a/DevDash/DevDash.csproj +++ b/DevDash/DevDash.csproj @@ -29,6 +29,7 @@ + diff --git a/DevDash/Program.cs b/DevDash/Program.cs index 401d1b5..20fb0eb 100644 --- a/DevDash/Program.cs +++ b/DevDash/Program.cs @@ -14,6 +14,8 @@ using System.Text; using Stripe; using DevDash.Services.IServices; +using StackExchange.Redis; +using DevDash.Services.IService; namespace DevDash { @@ -69,6 +71,22 @@ public static void Main(string[] args) builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddSingleton(sp => + { + var configuration = sp.GetRequiredService(); + var redisConnectionString = configuration.GetConnectionString("Redis"); + + if (string.IsNullOrEmpty(redisConnectionString)) + { + throw new InvalidOperationException("Redis connection string is missing in configuration."); + } + + return ConnectionMultiplexer.Connect(redisConnectionString); + }); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.Configure(builder.Configuration.GetSection("EmailSettings")); builder.Services.AddScoped(); diff --git a/DevDash/Repository/CacheRepository.cs b/DevDash/Repository/CacheRepository.cs new file mode 100644 index 0000000..2914eec --- /dev/null +++ b/DevDash/Repository/CacheRepository.cs @@ -0,0 +1,22 @@ +using DevDash.Repository.IRepository; +using StackExchange.Redis; +using System.Text.Json; + +namespace DevDash.Repository +{ + public class CacheRepositroy(IConnectionMultiplexer connection) : ICacheRepository + { + private readonly IDatabase _database = connection.GetDatabase(); + public async Task GetAsync(string key) + { + var value = await _database.StringGetAsync(key); + return !value.IsNullOrEmpty ? value : default; + } + + public async Task SetAsync(string key, object value, TimeSpan duration) + { + var redisValue = JsonSerializer.Serialize(value); + await _database.StringSetAsync(key, redisValue, duration); + } + } +} diff --git a/DevDash/Repository/IRepository/ICacheRepository.cs b/DevDash/Repository/IRepository/ICacheRepository.cs new file mode 100644 index 0000000..722bc75 --- /dev/null +++ b/DevDash/Repository/IRepository/ICacheRepository.cs @@ -0,0 +1,8 @@ +namespace DevDash.Repository.IRepository +{ + public interface ICacheRepository + { + Task SetAsync(string key, object value, TimeSpan duration); + Task GetAsync(string key); + } +} diff --git a/DevDash/Services/CacheService.cs b/DevDash/Services/CacheService.cs new file mode 100644 index 0000000..5fa9d47 --- /dev/null +++ b/DevDash/Services/CacheService.cs @@ -0,0 +1,20 @@ +using DevDash.Repository.IRepository; +using DevDash.Services.IService; + +namespace DevDash.Services +{ + public class CacheService(ICacheRepository cacheRepository) : ICacheService + { + public async Task? GetCacheValueAsync(string key) + { + var value = await cacheRepository.GetAsync(key); + return value == null ? null : value; + } + + public async Task SetCacheValueAsync(string key, object value, TimeSpan duration) + { + await cacheRepository.SetAsync(key, value, duration); + + } + } +} \ No newline at end of file diff --git a/DevDash/Services/IService/ICacheService.cs b/DevDash/Services/IService/ICacheService.cs new file mode 100644 index 0000000..a27e9e3 --- /dev/null +++ b/DevDash/Services/IService/ICacheService.cs @@ -0,0 +1,8 @@ +namespace DevDash.Services.IService +{ + public interface ICacheService + { + Task SetCacheValueAsync(string key, object value, TimeSpan duration); + Task? GetCacheValueAsync(string key); + } +} diff --git a/DevDash/appsettings.json b/DevDash/appsettings.json index 053203b..0b72531 100644 --- a/DevDash/appsettings.json +++ b/DevDash/appsettings.json @@ -7,7 +7,8 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "Cs": "Server=db14416.public.databaseasp.net; Database=db14416; User Id=db14416; Password=Ks5!7%jE-L6y; Encrypt=False; MultipleActiveResultSets=True;" + "Cs": "Server=db14416.public.databaseasp.net; Database=db14416; User Id=db14416; Password=Ks5!7%jE-L6y; Encrypt=False; MultipleActiveResultSets=True;", + "Redis": "localhost" }, "Authentication": { "Google": { diff --git a/DevDash/obj/DevDash.csproj.nuget.dgspec.json b/DevDash/obj/DevDash.csproj.nuget.dgspec.json index 5868de3..46d37db 100644 --- a/DevDash/obj/DevDash.csproj.nuget.dgspec.json +++ b/DevDash/obj/DevDash.csproj.nuget.dgspec.json @@ -128,6 +128,10 @@ "target": "Package", "version": "[1.0.0, )" }, + "StackExchange.Redis": { + "target": "Package", + "version": "[2.8.41, )" + }, "Stripe.net": { "target": "Package", "version": "[48.3.0, )" diff --git a/DevDash/obj/DevDash.csproj.nuget.g.targets b/DevDash/obj/DevDash.csproj.nuget.g.targets index da3519f..56239e3 100644 --- a/DevDash/obj/DevDash.csproj.nuget.g.targets +++ b/DevDash/obj/DevDash.csproj.nuget.g.targets @@ -3,10 +3,10 @@ + - \ No newline at end of file diff --git a/DevDash/obj/project.assets.json b/DevDash/obj/project.assets.json index af6cf8c..7e99705 100644 --- a/DevDash/obj/project.assets.json +++ b/DevDash/obj/project.assets.json @@ -2020,6 +2020,39 @@ } } }, + "Pipelines.Sockets.Unofficial/2.2.8": { + "type": "package", + "dependencies": { + "System.IO.Pipelines": "5.0.1" + }, + "compile": { + "lib/net5.0/Pipelines.Sockets.Unofficial.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net5.0/Pipelines.Sockets.Unofficial.dll": { + "related": ".xml" + } + } + }, + "StackExchange.Redis/2.8.41": { + "type": "package", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" + }, + "compile": { + "lib/net8.0/StackExchange.Redis.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net8.0/StackExchange.Redis.dll": { + "related": ".xml" + } + } + }, "Stripe.net/48.3.0": { "type": "package", "dependencies": { @@ -5935,6 +5968,53 @@ "newtonsoft.json.bson.nuspec" ] }, + "Pipelines.Sockets.Unofficial/2.2.8": { + "sha512": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==", + "type": "package", + "path": "pipelines.sockets.unofficial/2.2.8", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/net461/Pipelines.Sockets.Unofficial.dll", + "lib/net461/Pipelines.Sockets.Unofficial.xml", + "lib/net472/Pipelines.Sockets.Unofficial.dll", + "lib/net472/Pipelines.Sockets.Unofficial.xml", + "lib/net5.0/Pipelines.Sockets.Unofficial.dll", + "lib/net5.0/Pipelines.Sockets.Unofficial.xml", + "lib/netcoreapp3.1/Pipelines.Sockets.Unofficial.dll", + "lib/netcoreapp3.1/Pipelines.Sockets.Unofficial.xml", + "lib/netstandard2.0/Pipelines.Sockets.Unofficial.dll", + "lib/netstandard2.0/Pipelines.Sockets.Unofficial.xml", + "lib/netstandard2.1/Pipelines.Sockets.Unofficial.dll", + "lib/netstandard2.1/Pipelines.Sockets.Unofficial.xml", + "pipelines.sockets.unofficial.2.2.8.nupkg.sha512", + "pipelines.sockets.unofficial.nuspec" + ] + }, + "StackExchange.Redis/2.8.41": { + "sha512": "EgtQzlucry9V2Gces1K19Xi8Sz/8DfoZlGQEmL/hSM/SXO/ucDS5RPmLzcmT489ZNGFbj9CGhlpscPLOXtYWwA==", + "type": "package", + "path": "stackexchange.redis/2.8.41", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "lib/net461/StackExchange.Redis.dll", + "lib/net461/StackExchange.Redis.xml", + "lib/net472/StackExchange.Redis.dll", + "lib/net472/StackExchange.Redis.xml", + "lib/net6.0/StackExchange.Redis.dll", + "lib/net6.0/StackExchange.Redis.xml", + "lib/net8.0/StackExchange.Redis.dll", + "lib/net8.0/StackExchange.Redis.xml", + "lib/netcoreapp3.1/StackExchange.Redis.dll", + "lib/netcoreapp3.1/StackExchange.Redis.xml", + "lib/netstandard2.0/StackExchange.Redis.dll", + "lib/netstandard2.0/StackExchange.Redis.xml", + "stackexchange.redis.2.8.41.nupkg.sha512", + "stackexchange.redis.nuspec" + ] + }, "Stripe.net/48.3.0": { "sha512": "uzxzn3Wg9bXEXCoktmgrrsRyNquhzyNoJvK7AlxYoxccKbljLmzfSajrj8r1LRIKBjXZ6HhB+I+siisIjR3ifg==", "type": "package", @@ -7261,6 +7341,7 @@ "Microsoft.Extensions.Http >= 8.0.0", "MimeKit >= 4.11.0", "NetcodeHub.Packages.Extensions.LocalStorage >= 1.0.0", + "StackExchange.Redis >= 2.8.41", "Stripe.net >= 48.3.0", "Swashbuckle.AspNetCore >= 6.6.2", "System.ComponentModel.Annotations >= 5.0.0" @@ -7394,6 +7475,10 @@ "target": "Package", "version": "[1.0.0, )" }, + "StackExchange.Redis": { + "target": "Package", + "version": "[2.8.41, )" + }, "Stripe.net": { "target": "Package", "version": "[48.3.0, )"