From b0fadc496cbd8f9dfd80ae6541f82a3bbd11fb32 Mon Sep 17 00:00:00 2001 From: "Md. Abul Kalam" Date: Wed, 22 Jan 2025 23:27:14 +0600 Subject: [PATCH 1/2] Issue #61: Add Redis cache --- .../DependencyInjectionRegister.cs | 2 +- src/Shopizy.Api/Program.cs | 5 +- src/Shopizy.Api/Shopizy.Api.csproj | 11 +-- src/Shopizy.Api/appsettings.json | 4 ++ .../Common/Caching/ICacheHelper.cs | 32 +++++++++ .../Common/Caching/RedisCacheHelper.cs | 67 +++++++++++++++++++ .../Common/Caching/RedisSettings.cs | 8 +++ .../DependencyInjectionRegister.cs | 10 ++- .../Shopizy.Application.csproj | 15 +++-- .../Queries/GetUser/GetUserQueryHandler.cs | 17 ++++- .../Orders/ValueObjects/Address.cs | 2 + .../Users/ValueObjects/UserId.cs | 2 + .../Shopizy.Infrastructure.csproj | 12 ++-- .../Shopizy.Application.UnitTests.csproj | 10 +-- .../Shopizy.Domain.UnitTests.csproj | 8 +-- .../Shopizy.Infrastructure.UnitTests.csproj | 10 +-- 16 files changed, 178 insertions(+), 37 deletions(-) create mode 100644 src/Shopizy.Application/Common/Caching/ICacheHelper.cs create mode 100644 src/Shopizy.Application/Common/Caching/RedisCacheHelper.cs create mode 100644 src/Shopizy.Application/Common/Caching/RedisSettings.cs diff --git a/src/Shopizy.Api/DependencyInjectionRegister.cs b/src/Shopizy.Api/DependencyInjectionRegister.cs index a8267c7..983cecd 100644 --- a/src/Shopizy.Api/DependencyInjectionRegister.cs +++ b/src/Shopizy.Api/DependencyInjectionRegister.cs @@ -6,7 +6,7 @@ public static class DependencyInjectionRegister { public static IServiceCollection AddPresentation(this IServiceCollection services) { - services.AddControllers(); + services.AddControllers().AddNewtonsoftJson(); services.AddEndpointsApiExplorer().AddSwaggerGen(); services.AddMappings(); diff --git a/src/Shopizy.Api/Program.cs b/src/Shopizy.Api/Program.cs index 0c6c98b..82fbf0a 100644 --- a/src/Shopizy.Api/Program.cs +++ b/src/Shopizy.Api/Program.cs @@ -8,7 +8,10 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services.AddPresentation().AddApplication().AddInfrastructure(builder.Configuration); +builder + .Services.AddPresentation() + .AddApplication(builder.Configuration) + .AddInfrastructure(builder.Configuration); var app = builder.Build(); diff --git a/src/Shopizy.Api/Shopizy.Api.csproj b/src/Shopizy.Api/Shopizy.Api.csproj index da6ae32..3da07e6 100644 --- a/src/Shopizy.Api/Shopizy.Api.csproj +++ b/src/Shopizy.Api/Shopizy.Api.csproj @@ -6,19 +6,20 @@ false - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + + - + \ No newline at end of file diff --git a/src/Shopizy.Api/appsettings.json b/src/Shopizy.Api/appsettings.json index fec5c29..ae00e12 100644 --- a/src/Shopizy.Api/appsettings.json +++ b/src/Shopizy.Api/appsettings.json @@ -22,5 +22,9 @@ "SecretKey": "", "PublishableKey": "", "WebhookSecret": "" + }, + "RedisCacheSettings": { + "ConnectionString": "", + "Password": "" } } diff --git a/src/Shopizy.Application/Common/Caching/ICacheHelper.cs b/src/Shopizy.Application/Common/Caching/ICacheHelper.cs new file mode 100644 index 0000000..47adc5e --- /dev/null +++ b/src/Shopizy.Application/Common/Caching/ICacheHelper.cs @@ -0,0 +1,32 @@ +namespace Shopizy.Application.Common.Caching; + +/// +/// Provides methods for caching operations. +/// +public interface ICacheHelper +{ + /// + /// Retrieves a cached item by its key. + /// + /// The type of the cached item. + /// The key of the cached item. + /// A task that represents the asynchronous operation. The task result contains the cached item. + Task GetAsync(string key); + + /// + /// Adds or updates a cached item with the specified key and value. + /// + /// The type of the item to cache. + /// The key of the cached item. + /// The value of the item to cache. + /// The optional expiration time for the cached item. + /// A task that represents the asynchronous operation. + Task SetAsync(string key, T value, TimeSpan? expiration = null); + + /// + /// Removes a cached item by its key. + /// + /// The key of the cached item to remove. + /// A task that represents the asynchronous operation. + Task RemoveAsync(string key); +} diff --git a/src/Shopizy.Application/Common/Caching/RedisCacheHelper.cs b/src/Shopizy.Application/Common/Caching/RedisCacheHelper.cs new file mode 100644 index 0000000..3ee5ecb --- /dev/null +++ b/src/Shopizy.Application/Common/Caching/RedisCacheHelper.cs @@ -0,0 +1,67 @@ +using System.Text.Json; +using Microsoft.Extensions.Options; +using StackExchange.Redis; + +namespace Shopizy.Application.Common.Caching; + +/// +/// Helper class for interacting with Redis cache. +/// +public class RedisCacheHelper : ICacheHelper +{ + private readonly RedisSettings _redisSettings; + private readonly Lazy _redisDbConnectionLazy; + + /// + /// Initializes a new instance of the class. + /// + /// The Redis settings options. + public RedisCacheHelper(IOptions redisSettingsOptions) + { + _redisSettings = redisSettingsOptions.Value; + _redisDbConnectionLazy = new Lazy( + () => + ConnectionMultiplexer.Connect( + string.Format(_redisSettings.ConnectionString, _redisSettings.Password) + ), + LazyThreadSafetyMode.ExecutionAndPublication + ); + } + + /// + /// Gets the cached value for the specified key. + /// + /// The type of the cached value. + /// The cache key. + /// The cached value, or default if the key does not exist. + public async Task GetAsync(string key) + { + var db = _redisDbConnectionLazy.Value.GetDatabase(); + var data = await db.StringGetAsync(key); + + return data.HasValue ? JsonSerializer.Deserialize(data) : default; + } + + /// + /// Sets the specified value in the cache with the specified key. + /// + /// The type of the value to cache. + /// The cache key. + /// The value to cache. + /// The expiration time for the cached value. If null, the value does not expire. + public async Task SetAsync(string key, T value, TimeSpan? expiration = null) + { + var db = _redisDbConnectionLazy.Value.GetDatabase(); + await db.StringSetAsync(key, JsonSerializer.Serialize(value), expiration); + } + + /// + /// Removes the cached value for the specified key. + /// + /// The cache key. + public async Task RemoveAsync(string key) + { + var db = _redisDbConnectionLazy.Value.GetDatabase(); + await db.KeyDeleteAsync(key); + } +} diff --git a/src/Shopizy.Application/Common/Caching/RedisSettings.cs b/src/Shopizy.Application/Common/Caching/RedisSettings.cs new file mode 100644 index 0000000..d8db610 --- /dev/null +++ b/src/Shopizy.Application/Common/Caching/RedisSettings.cs @@ -0,0 +1,8 @@ +namespace Shopizy.Application.Common.Caching; + +public class RedisSettings +{ + public const string Section = "RedisCacheSettings"; + public string ConnectionString { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} diff --git a/src/Shopizy.Application/DependencyInjectionRegister.cs b/src/Shopizy.Application/DependencyInjectionRegister.cs index 69174c3..d292ec2 100644 --- a/src/Shopizy.Application/DependencyInjectionRegister.cs +++ b/src/Shopizy.Application/DependencyInjectionRegister.cs @@ -1,13 +1,18 @@ using FluentValidation; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Shopizy.Application.Common.Behaviors; +using Shopizy.Application.Common.Caching; using Shopizy.Application.Common.Security.CurrentUser; namespace Shopizy.Application; public static class DependencyInjectionRegister { - public static IServiceCollection AddApplication(this IServiceCollection services) + public static IServiceCollection AddApplication( + this IServiceCollection services, + IConfiguration configuration + ) { services.AddMediatR(msc => { @@ -18,7 +23,10 @@ public static IServiceCollection AddApplication(this IServiceCollection services services.AddValidatorsFromAssemblyContaining(typeof(DependencyInjectionRegister)); // services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + services.Configure(configuration.GetSection(RedisSettings.Section)); + services.AddScoped(); + services.AddSingleton(); return services; } diff --git a/src/Shopizy.Application/Shopizy.Application.csproj b/src/Shopizy.Application/Shopizy.Application.csproj index 67b1a07..348f0f3 100644 --- a/src/Shopizy.Application/Shopizy.Application.csproj +++ b/src/Shopizy.Application/Shopizy.Application.csproj @@ -1,16 +1,17 @@ - + - - - - - + + + + + + - + net8.0 diff --git a/src/Shopizy.Application/Users/Queries/GetUser/GetUserQueryHandler.cs b/src/Shopizy.Application/Users/Queries/GetUser/GetUserQueryHandler.cs index 76bc9a0..f5a546e 100644 --- a/src/Shopizy.Application/Users/Queries/GetUser/GetUserQueryHandler.cs +++ b/src/Shopizy.Application/Users/Queries/GetUser/GetUserQueryHandler.cs @@ -1,5 +1,6 @@ using ErrorOr; using MediatR; +using Shopizy.Application.Common.Caching; using Shopizy.Application.Common.Interfaces.Persistence; using Shopizy.Domain.Common.CustomErrors; using Shopizy.Domain.Orders.Enums; @@ -7,11 +8,15 @@ namespace Shopizy.Application.Users.Queries.GetUser; -public class GetUserQueryHandler(IUserRepository userRepository, IOrderRepository orderRepository) - : IRequestHandler> +public class GetUserQueryHandler( + IUserRepository userRepository, + IOrderRepository orderRepository, + ICacheHelper cacheHelper +) : IRequestHandler> { private readonly IUserRepository _userRepository = userRepository; private readonly IOrderRepository _orderRepository = orderRepository; + private readonly ICacheHelper _cacheHelper = cacheHelper; public async Task> Handle( GetUserQuery request, @@ -25,6 +30,12 @@ CancellationToken cancellationToken return CustomErrors.User.UserNotFound; } + var cachedUser = await _cacheHelper.GetAsync($"user-{user.Id.Value}"); + if (cachedUser is not null) + { + return cachedUser; + } + var userOrders = _orderRepository .GetOrdersByUserId(user.Id) .Select(o => new { o.Id, o.OrderStatus }) @@ -50,6 +61,8 @@ CancellationToken cancellationToken user.ModifiedOn ); + await _cacheHelper.SetAsync($"user-{user.Id.Value}", userDto, TimeSpan.FromMinutes(60)); + return userDto; } } diff --git a/src/Shopizy.Domain/Orders/ValueObjects/Address.cs b/src/Shopizy.Domain/Orders/ValueObjects/Address.cs index 250d600..1bd5426 100644 --- a/src/Shopizy.Domain/Orders/ValueObjects/Address.cs +++ b/src/Shopizy.Domain/Orders/ValueObjects/Address.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using Shopizy.Domain.Common.Models; namespace Shopizy.Domain.Orders.ValueObjects; @@ -10,6 +11,7 @@ public sealed class Address : ValueObject public string Country { get; private set; } public string ZipCode { get; private set; } + [JsonConstructor] private Address(string street, string city, string state, string country, string zipCode) { Street = street; diff --git a/src/Shopizy.Domain/Users/ValueObjects/UserId.cs b/src/Shopizy.Domain/Users/ValueObjects/UserId.cs index dede614..dd92060 100644 --- a/src/Shopizy.Domain/Users/ValueObjects/UserId.cs +++ b/src/Shopizy.Domain/Users/ValueObjects/UserId.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using Shopizy.Domain.Common.Models; namespace Shopizy.Domain.Users.ValueObjects; @@ -6,6 +7,7 @@ public sealed class UserId : AggregateRootId { public override Guid Value { get; protected set; } + [JsonConstructor] private UserId(Guid value) { Value = value; diff --git a/src/Shopizy.Infrastructure/Shopizy.Infrastructure.csproj b/src/Shopizy.Infrastructure/Shopizy.Infrastructure.csproj index bfd520f..e39aa37 100644 --- a/src/Shopizy.Infrastructure/Shopizy.Infrastructure.csproj +++ b/src/Shopizy.Infrastructure/Shopizy.Infrastructure.csproj @@ -3,15 +3,15 @@ - + - - - - - + + + + + diff --git a/tests/UnitTests/Shopizy.Application.UnitTests/Shopizy.Application.UnitTests.csproj b/tests/UnitTests/Shopizy.Application.UnitTests/Shopizy.Application.UnitTests.csproj index 0c99cf6..da55785 100644 --- a/tests/UnitTests/Shopizy.Application.UnitTests/Shopizy.Application.UnitTests.csproj +++ b/tests/UnitTests/Shopizy.Application.UnitTests/Shopizy.Application.UnitTests.csproj @@ -7,18 +7,18 @@ true - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/tests/UnitTests/Shopizy.Domain.UnitTests/Shopizy.Domain.UnitTests.csproj b/tests/UnitTests/Shopizy.Domain.UnitTests/Shopizy.Domain.UnitTests.csproj index 9e468ee..98ae632 100644 --- a/tests/UnitTests/Shopizy.Domain.UnitTests/Shopizy.Domain.UnitTests.csproj +++ b/tests/UnitTests/Shopizy.Domain.UnitTests/Shopizy.Domain.UnitTests.csproj @@ -10,13 +10,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/UnitTests/Shopizy.Infrastructure.UnitTests/Shopizy.Infrastructure.UnitTests.csproj b/tests/UnitTests/Shopizy.Infrastructure.UnitTests/Shopizy.Infrastructure.UnitTests.csproj index 1eaffd8..622eaff 100644 --- a/tests/UnitTests/Shopizy.Infrastructure.UnitTests/Shopizy.Infrastructure.UnitTests.csproj +++ b/tests/UnitTests/Shopizy.Infrastructure.UnitTests/Shopizy.Infrastructure.UnitTests.csproj @@ -7,18 +7,18 @@ true - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + From 4fe227bdf41d4c32800288bf263bd25af7e010f3 Mon Sep 17 00:00:00 2001 From: "Md. Abul Kalam" Date: Wed, 22 Jan 2025 23:30:50 +0600 Subject: [PATCH 2/2] Issue #61: Update unit test --- .../Queries/GetUser/GetUserQueryHandler.test.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/UnitTests/Shopizy.Application.UnitTests/Users/Queries/GetUser/GetUserQueryHandler.test.cs b/tests/UnitTests/Shopizy.Application.UnitTests/Users/Queries/GetUser/GetUserQueryHandler.test.cs index adcb35d..c84ee74 100644 --- a/tests/UnitTests/Shopizy.Application.UnitTests/Users/Queries/GetUser/GetUserQueryHandler.test.cs +++ b/tests/UnitTests/Shopizy.Application.UnitTests/Users/Queries/GetUser/GetUserQueryHandler.test.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Moq; +using Shopizy.Application.Common.Caching; using Shopizy.Application.Common.Interfaces.Persistence; using Shopizy.Application.UnitTests.Users.TestUtils; using Shopizy.Application.Users.Queries.GetUser; @@ -13,12 +14,18 @@ public class GetUserQueryHandlerTests private readonly GetUserQueryHandler _sut; private readonly Mock _mockUserRepository; private readonly Mock _mockOrderRepository; + private readonly Mock _mockCacheHelper; public GetUserQueryHandlerTests() { _mockUserRepository = new Mock(); _mockOrderRepository = new Mock(); - _sut = new GetUserQueryHandler(_mockUserRepository.Object, _mockOrderRepository.Object); + _mockCacheHelper = new Mock(); + _sut = new GetUserQueryHandler( + _mockUserRepository.Object, + _mockOrderRepository.Object, + _mockCacheHelper.Object + ); } [Fact] @@ -32,6 +39,10 @@ public async Task ShouldReturnUserObjectWhenValidUserIdIsProvided() .Setup(c => c.GetUserById(UserId.Create(query.UserId))) .ReturnsAsync(user); + _mockCacheHelper + .Setup(c => c.GetAsync($"user-{user.Id.Value}")) + .ReturnsAsync(() => null); + // Act var result = (await _sut.Handle(query, CancellationToken.None)).Match(x => x, x => null);