Skip to content

Commit

Permalink
Merge pull request #12 from moheladwy/AddCachingUsingRedis
Browse files Browse the repository at this point in the history
Interducing Caching Using Redis 👍🏿
  • Loading branch information
moheladwy authored Dec 24, 2024
2 parents 76dcb3f + 565d186 commit 2902ee2
Show file tree
Hide file tree
Showing 17 changed files with 219 additions and 94 deletions.
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

0 comments on commit 2902ee2

Please sign in to comment.