Implement Caching

This commit is contained in:
Alan Moon 2025-06-22 15:06:38 -07:00
parent ac2c8be4a2
commit 88b0615a30
5 changed files with 147 additions and 17 deletions

View File

@ -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:

View File

@ -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<ActionResult<ServiceResponse<List<UserInformationDto>>>> GetAllUsers()
{
var users = await _userService.GetAllUsers();
var users = new ServiceResponse<List<UserInformationDto>>();
string recordId = $"Users_{DateTime.Now.ToString("yyyyMMdd_hhmm")}";
users = await cache.GetRecordAsync<ServiceResponse<List<UserInformationDto>>>(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<ActionResult<ServiceResponse<UserInformationDto>>> GetUserInformation(string id)
{
var user = await _userService.GetUserInformationById(id);
var user = new ServiceResponse<UserInformationDto>();
string recordId = $"User_{DateTime.Now.ToString("yyyyMMdd_hhmm")}";
user = await cache.GetRecordAsync<ServiceResponse<UserInformationDto>>(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<ActionResult> 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<FileContentResult>();
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.");
}

View File

@ -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<T> (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<T?> GetRecordAsync<T>(this IDistributedCache cache, string recordId)
{
var jsonData = await cache.GetStringAsync(recordId);
if(jsonData == null)
{
return default;
}
return JsonSerializer.Deserialize<T>(jsonData);
}
public static async Task<byte[]?> GetImageAsync(this IDistributedCache cache, string recordId)
{
var bytes = await cache.GetAsync(recordId);
if (bytes == null)
{
return default;
}
return bytes;
}
}
}

View File

@ -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<DataContext>();
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

View File

@ -20,9 +20,11 @@
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.6" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.10.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
<PackageReference Include="MySql.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.10.0" />