From 88b0615a30632b981556145764fe9f7dcaaf3845 Mon Sep 17 00:00:00 2001 From: AlanMoonbase Date: Sun, 22 Jun 2025 15:06:38 -0700 Subject: [PATCH] Implement Caching --- docker-compose.yml | 13 ++- qtc-net-server/Controllers/UsersController.cs | 83 +++++++++++++++---- .../Extensions/DistributedCacheExtensions.cs | 53 ++++++++++++ qtc-net-server/Program.cs | 13 ++- qtc-net-server/qtc-net-server.csproj | 2 + 5 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 qtc-net-server/Extensions/DistributedCacheExtensions.cs diff --git a/docker-compose.yml b/docker-compose.yml index bb7cacf..2daf5b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,9 +11,13 @@ services: - "8080:8080" networks: - qtc-backend - restart: unless-stopped depends_on: - - db + db: + condition: service_healthy + restart: true + redis: + condition: service_started + restart: unless-stopped # Traefik Config Example # labels: # - "traefik.enable=true" @@ -37,6 +41,11 @@ services: timeout: 30s restart: always + redis: + container_name: qtc-cache + image: redis + networks: + - qtc-backend volumes: qtc-data: diff --git a/qtc-net-server/Controllers/UsersController.cs b/qtc-net-server/Controllers/UsersController.cs index 51de99c..cf39de7 100644 --- a/qtc-net-server/Controllers/UsersController.cs +++ b/qtc-net-server/Controllers/UsersController.cs @@ -1,11 +1,6 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using qtc_api.Dtos.User; -using System.Net.Mime; +using Microsoft.Extensions.Caching.Distributed; +using qtc_api.Extensions; using System.Security.Claims; -using System.Text.Json; namespace qtc_api.Controllers { @@ -14,17 +9,28 @@ namespace qtc_api.Controllers public class UsersController : ControllerBase { private readonly IUserService _userService; - - public UsersController(IUserService userService) + private readonly IDistributedCache cache; + public UsersController(IUserService userService, IDistributedCache distributedCache) { _userService = userService; + cache = distributedCache; } [HttpGet("all")] [Authorize] public async Task>>> GetAllUsers() { - var users = await _userService.GetAllUsers(); + var users = new ServiceResponse>(); + + string recordId = $"Users_{DateTime.Now.ToString("yyyyMMdd_hhmm")}"; + users = await cache.GetRecordAsync>>(recordId); + + if(users == null) + { + users = await _userService.GetAllUsers(); + await cache.SetRecordAsync(recordId, users); + } + return Ok(users); } @@ -32,7 +38,17 @@ namespace qtc_api.Controllers [Authorize] public async Task>> GetUserInformation(string id) { - var user = await _userService.GetUserInformationById(id); + var user = new ServiceResponse(); + + string recordId = $"User_{DateTime.Now.ToString("yyyyMMdd_hhmm")}"; + user = await cache.GetRecordAsync>(recordId); + + if (user == null) + { + user = await _userService.GetUserInformationById(id); + await cache.SetRecordAsync(recordId, user); + } + return Ok(user); } @@ -83,6 +99,11 @@ namespace qtc_api.Controllers if(id != null && id == user.Id) { var updatedUser = await _userService.UpdateUserInfo(user); + + // always try to overwrite cache when updating user info + string recordId = $"User_{DateTime.Now.ToString("yyyyMMdd_hhmm")}"; + await cache.SetRecordAsync(recordId, updatedUser); + return Ok(updatedUser); } else { @@ -114,6 +135,19 @@ namespace qtc_api.Controllers var response = await _userService.UpdateUserPic(userId, file); + // always try to overwrite cache when updating pfp + string recordId = $"UserPfp_{DateTime.Now.ToString("yyyyMMdd_hhmm")}"; + using(var stream = file.OpenReadStream()) + { + using(var ms = new MemoryStream()) + { + stream.CopyTo(ms); + await cache.SetImageAsync(recordId, ms.ToArray()); + ms.Dispose(); + } + stream.Dispose(); + } + return Ok(response); } else { @@ -129,15 +163,36 @@ namespace qtc_api.Controllers [Authorize] public async Task GetUserProfilePicture(string userId) { - var result = await _userService.GetUserPic(userId); + string recordId = $"UserPfp_{DateTime.Now.ToString("yyyyMMdd_hhmm")}"; + byte[] pfpBytes = await cache.GetImageAsync(recordId); + + var result = new ServiceResponse(); + if (pfpBytes == null) + { + result = await _userService.GetUserPic(userId); + if (result != null && result.Success && result.Data != null) + { + pfpBytes = result.Data.FileContents; + await cache.SetImageAsync(recordId, pfpBytes); + } + } + else + { + // explicitly set from cache + result.Success = true; + result.Data = new FileContentResult(pfpBytes, "image/jpeg"); + result.Message = $"{userId}.pfp"; + } if (result != null && result.Success != false) { return result.Data!; - } else if (result!.Message == "User Does Not Have A Profile Picture." || result!.Message == "User Content Folder Does Not Exist Yet.") + } + else if (result!.Message == "User Does Not Have A Profile Picture." || result!.Message == "User Content Folder Does Not Exist Yet.") { return BadRequest("User has no profile picture."); - } else + } + else { return BadRequest("Failed To Get Profile Picture."); } diff --git a/qtc-net-server/Extensions/DistributedCacheExtensions.cs b/qtc-net-server/Extensions/DistributedCacheExtensions.cs new file mode 100644 index 0000000..82be513 --- /dev/null +++ b/qtc-net-server/Extensions/DistributedCacheExtensions.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace qtc_api.Extensions +{ + public static class DistributedCacheExtensions + { + public static async Task SetRecordAsync (this IDistributedCache cache, string recordId, T data, TimeSpan? absoluteExpireTime = null, TimeSpan? unusuedExpireTime = null) + { + var options = new DistributedCacheEntryOptions(); + + options.AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromSeconds(60); + options.SlidingExpiration = unusuedExpireTime; + + var jsonData = JsonSerializer.Serialize(data); + await cache.SetStringAsync(recordId, jsonData, options); + } + + public static async Task SetImageAsync(this IDistributedCache cache, string recordId, byte[] data, TimeSpan? absoluteExpireTime = null, TimeSpan? unusuedExpireTime = null) + { + var options = new DistributedCacheEntryOptions(); + + options.AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromSeconds(60); + options.SlidingExpiration = unusuedExpireTime; + + await cache.SetAsync(recordId, data, options); + } + + public static async Task GetRecordAsync(this IDistributedCache cache, string recordId) + { + var jsonData = await cache.GetStringAsync(recordId); + + if(jsonData == null) + { + return default; + } + + return JsonSerializer.Deserialize(jsonData); + } + + public static async Task GetImageAsync(this IDistributedCache cache, string recordId) + { + var bytes = await cache.GetAsync(recordId); + + if (bytes == null) + { + return default; + } + + return bytes; + } + } +} diff --git a/qtc-net-server/Program.cs b/qtc-net-server/Program.cs index ecd3c55..f2610a2 100644 --- a/qtc-net-server/Program.cs +++ b/qtc-net-server/Program.cs @@ -13,7 +13,6 @@ global using qtc_api.Services.TokenService; global using qtc_api.Hubs; using qtc_api.Services.RoomService; using qtc_api.Services.ContactService; -using Microsoft.EntityFrameworkCore.Diagnostics; var builder = WebApplication.CreateBuilder(args); @@ -23,6 +22,18 @@ builder.Services.AddDbContext(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSignalR(); +builder.Services.AddStackExchangeRedisCache(options => +{ + var redisConnectionString = Environment.GetEnvironmentVariable("REDIS_CONNECTIONSTRING"); + if (redisConnectionString != null) + options.Configuration = redisConnectionString; + + if (!builder.Environment.IsProduction()) + options.InstanceName = "QtCNetServerDev_"; + else + options.InstanceName = "QtCNetServer_"; +}); + builder.Services.AddAuthentication().AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters diff --git a/qtc-net-server/qtc-net-server.csproj b/qtc-net-server/qtc-net-server.csproj index 30f42d0..70a02fb 100644 --- a/qtc-net-server/qtc-net-server.csproj +++ b/qtc-net-server/qtc-net-server.csproj @@ -20,9 +20,11 @@ + +