Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interducing Caching Using Redis 👍🏿 #12

Merged
merged 1 commit into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Jenkinsfile
LICENSE
README.md
.github
.gitignore
.idea
.vscode
Dockerfile
docker-compose.yml
.dockerignore
3 changes: 2 additions & 1 deletion Todo.Api/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"ConnectionStrings": {
"SqliteConnection": "Data Source=Database/todo.db"
"SqliteConnection": "Data Source=Database/todo.db",
"RedisConnection": "localhost:6379"
},
"Logging": {
"LogLevel": {
Expand Down
2 changes: 1 addition & 1 deletion Todo.Api/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"ConnectionStrings": {
"SqliteConnection": "Data Source=Database/todo.db",
"SqlServerConnection": "Server=localhost,1433;Database=AuthDemo;User Id=sa;Password=23-7-2003@Mohamed;TrustServerCertificate=True;"
"RedisConnection": "localhost:6379"
},
"Logging": {
"LogLevel": {
Expand Down
66 changes: 66 additions & 0 deletions Todo.Core/Interfaces/IRedisCacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace Todo.Core.Interfaces;

/// <summary>
/// Interface for Redis Cache Service
/// </summary>
public interface IRedisCacheService
{
/// <summary>
/// Get data from Redis Cache
/// </summary>
/// <param name="key">
/// Key to get data from Redis Cache
/// </param>
/// <typeparam name="T">
/// Type of data to get from Redis Cache
/// </typeparam>
/// <returns>
/// Data from Redis Cache with the given key if exists, otherwise default value of the type
/// </returns>
Task<T?> GetData<T>(string key);

/// <summary>
/// Set data in Redis Cache
/// </summary>
/// <param name="key">
/// Key to set data in Redis Cache
/// </param>
/// <param name="data">
/// Data to set in Redis Cache
/// </param>
/// <typeparam name="T">
/// Type of data to set in Redis Cache
/// </typeparam>
/// <returns>
/// A task that represents the asynchronous operation
/// </returns>
Task SetData<T>(string key, T data);

/// <summary>
/// Update data in Redis Cache
/// </summary>
/// <param name="key">
/// Key to update data in Redis Cache
/// </param>
/// <param name="data">
/// Data to update in Redis Cache
/// </param>
/// <typeparam name="T">
/// Type of data to update in Redis Cache
/// </typeparam>
/// <returns>
/// A task that represents the asynchronous operation, containing the updated data
/// </returns>
Task<T> UpdateData<T>(string key, T data);

/// <summary>
/// Remove data from Redis Cache
/// </summary>
/// <param name="key">
/// Key to remove data from Redis Cache
/// </param>
/// <returns>
/// A task that represents the asynchronous operation
/// </returns>
Task RemoveData(string key);
}
1 change: 1 addition & 0 deletions Todo.Infrastructure/Configurations/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Todo.Infrastructure.Configurations;
public static class Constants
{
public const string ConnectionStringName = "SqliteConnection";
public const string RedisConnectionStringName = "RedisConnection";
public const string JwtConfigurationsSectionKey = "JwtConfigurations";
public const string AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@";
public const int TimeSpanByMinutesForCaching = 5;
Expand Down
10 changes: 8 additions & 2 deletions Todo.Infrastructure/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
using Todo.Core.Interfaces;
using Todo.Infrastructure.Configurations;
using Todo.Infrastructure.DatabaseContexts;
using Todo.Infrastructure.Repositories;
using Todo.Infrastructure.Repositories.Cached;
using Todo.Infrastructure.Repositories.DB;
using Todo.Infrastructure.Services;
using Task = Todo.Core.Entities.Task;

Expand All @@ -36,7 +37,12 @@ public static void RegisterRepositories(this WebApplicationBuilder builder)

public static void RegisterCachingRepositories(this WebApplicationBuilder builder)
{
builder.Services.AddMemoryCache();
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString(Constants.RedisConnectionStringName);
options.InstanceName = "TodoFullStack_";
});
builder.Services.AddScoped<IRedisCacheService, RedisCachingService>();
builder.Services.AddScoped<IAccountRepository, CachedAccountRepository>();
builder.Services.AddScoped<IRepository<TaskList, AddListDto, UpdateListDto>, CachedListRepository>();
builder.Services.AddScoped<IRepository<Task, AddTaskDto, UpdateTaskDto>, CachedTasksRepository>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.Extensions.Caching.Memory;
using Todo.Core.DTOs.AccountDTOs;
using Todo.Core.Entities;
using Todo.Core.Exceptions;
using Todo.Core.Interfaces;
using Todo.Infrastructure.Configurations;
using Todo.Infrastructure.Repositories.DB;
using Task = System.Threading.Tasks.Task;

namespace Todo.Infrastructure.Repositories;
namespace Todo.Infrastructure.Repositories.Cached;

public class CachedAccountRepository : IAccountRepository
{
private readonly AccountRepository _accountRepository;
private readonly IMemoryCache _memoryCache;
private readonly IRedisCacheService _cacheService;

public CachedAccountRepository(AccountRepository accountRepository, IMemoryCache memoryCache)
/// <summary>
/// Initializes a new instance of the <see cref="CachedAccountRepository"/> class.
/// </summary>
/// <param name="accountRepository"></param>
/// <param name="cacheService"></param>
public CachedAccountRepository(AccountRepository accountRepository, IRedisCacheService cacheService)
{
_accountRepository = accountRepository;
_memoryCache = memoryCache;
_cacheService = cacheService;
}

public async Task<User> GetUserById(string userId)
{
var cacheKey = $"User-{userId}";

var cachedUser = await _memoryCache.GetOrCreateAsync(
cacheKey,
entry =>
{
entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(Constants.TimeSpanByMinutesForCaching));
return _accountRepository.GetUserById(userId);
});
var cachedUser = await _cacheService.GetData<User>(cacheKey);
if (cachedUser is not null) return cachedUser;

return cachedUser ?? throw new UserNotFoundException($"User with id: {userId} not found");
var user = await _accountRepository.GetUserById(userId);
await _cacheService.SetData(cacheKey, user);
return user;
}

public async Task<User> GetUserByClaims(ClaimsPrincipal claims)
Expand All @@ -46,25 +46,20 @@ public async Task<User> GetUserByClaims(ClaimsPrincipal claims)
public async Task ChangePassword(ChangePasswordDto changePasswordDto)
{
await _accountRepository.ChangePassword(changePasswordDto);

var cacheKey = $"User-{changePasswordDto.Id}";
_memoryCache.Remove(cacheKey);
_memoryCache.CreateEntry(cacheKey).Value = await _accountRepository.GetUserById(changePasswordDto.Id);
await _cacheService.UpdateData($"User-{changePasswordDto.Id}",
await _accountRepository.GetUserById(changePasswordDto.Id));
}

public async Task UpdateUserInfo(UpdateUserInfoDto updateUserInfoDto)
{
await _accountRepository.UpdateUserInfo(updateUserInfoDto);

var cacheKey = $"User-{updateUserInfoDto.Id}";
_memoryCache.Remove(cacheKey);
_memoryCache.CreateEntry(cacheKey).Value = await _accountRepository.GetUserById(updateUserInfoDto.Id);
await _cacheService.UpdateData($"User-{updateUserInfoDto.Id}",
await _accountRepository.GetUserById(updateUserInfoDto.Id));
}

public async Task DeleteAccount(string id)
{
await _accountRepository.DeleteAccount(id);
var cacheKey = $"User-{id}";
_memoryCache.Remove(cacheKey);
await _cacheService.RemoveData($"User-{id}");
}
}
Original file line number Diff line number Diff line change
@@ -1,52 +1,50 @@
using Microsoft.Extensions.Caching.Memory;
using Todo.Core.DTOs.ListDTOs;
using Todo.Core.Entities;
using Todo.Core.Exceptions;
using Todo.Core.Interfaces;
using Todo.Infrastructure.Configurations;
using Todo.Infrastructure.Repositories.DB;
using Task = System.Threading.Tasks.Task;

namespace Todo.Infrastructure.Repositories;
namespace Todo.Infrastructure.Repositories.Cached;

public class CachedListRepository : IRepository<TaskList, AddListDto, UpdateListDto>
{
private readonly ListRepository _listRepository;
private readonly IMemoryCache _memoryCache;

public CachedListRepository(ListRepository listRepository, IMemoryCache memoryCache)
private readonly IRedisCacheService _cacheService;

/// <summary>
/// Initializes a new instance of the <see cref="CachedListRepository" /> class.
/// </summary>
/// <param name="listRepository"></param>
/// <param name="cacheService"></param>
public CachedListRepository(ListRepository listRepository, IRedisCacheService cacheService)
{
_listRepository = listRepository;
_memoryCache = memoryCache;
_cacheService = cacheService;
}

public async Task<IEnumerable<TaskList>> GetAllAsync(string id)
{
var cacheKey = $"User-{id}-Lists";

var cachedLists = await _memoryCache.GetOrCreateAsync<IEnumerable<TaskList>>(
cacheKey,
entry =>
{
entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(Constants.TimeSpanByMinutesForCaching));
return _listRepository.GetAllAsync(id);
});
var cachedLists = await _cacheService.GetData<IEnumerable<TaskList>>(cacheKey);
if (cachedLists is not null) return cachedLists;

return cachedLists ?? [];
var lists = await _listRepository.GetAllAsync(id);
var taskLists = lists.ToList();
await _cacheService.SetData(cacheKey, taskLists);
return taskLists;
}

public async Task<TaskList> GetByIdAsync(Guid id)
{
var cacheKey = $"List-{id}";

var cachedList = await _memoryCache.GetOrCreateAsync(
cacheKey,
entry =>
{
entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(Constants.TimeSpanByMinutesForCaching));
return _listRepository.GetByIdAsync(id);
});
var cachedList = await _cacheService.GetData<TaskList>(cacheKey);
if (cachedList is not null) return cachedList;

return cachedList ?? throw new ListNotFoundException($"The list with id: {id} was not found.");
var list = await _listRepository.GetByIdAsync(id);
await _cacheService.SetData(cacheKey, list);
return list;
}

public async Task<TaskList> AddAsync(AddListDto entity)
Expand All @@ -61,8 +59,7 @@ public async Task<TaskList> UpdateAsync(UpdateListDto entity)
var updatedList = await _listRepository.UpdateAsync(entity);

var cacheKey = $"List-{entity.Id}";
_memoryCache.Remove(cacheKey);
_memoryCache.CreateEntry(cacheKey).Value = updatedList;
await _cacheService.UpdateData(cacheKey, updatedList);

var list = await _listRepository.GetByIdAsync(entity.Id);
await UpdateAllListsInCache(list.UserId ?? throw new ArgumentNullException(list.UserId,
Expand All @@ -78,16 +75,16 @@ public async Task DeleteAsync(Guid id)
var list = await _listRepository.GetByIdAsync(id);
await _listRepository.DeleteAsync(id);

_memoryCache.Remove($"List-{id}");

await _cacheService.RemoveData($"List-{id}");
await UpdateAllListsInCache(list.UserId ?? throw new ArgumentNullException(list.UserId,
"The list with the given id does not have a user id."));
}

private async Task UpdateAllListsInCache(string userId)
{
var cacheKey = $"User-{userId}-Lists";
_memoryCache.Remove(cacheKey);
_memoryCache.CreateEntry(cacheKey).Value = await _listRepository.GetAllAsync(userId);
var lists = await _listRepository.GetAllAsync(userId);
var taskLists = lists.ToList();
await _cacheService.UpdateData(cacheKey, taskLists);
}
}
Loading
Loading