Skip to content

Commit

Permalink
Merge pull request #62 from akazad13/feature/issue-61-add-redis-cache
Browse files Browse the repository at this point in the history
Issue #61: Add Redis cache
  • Loading branch information
akazad13 authored Jan 22, 2025
2 parents 3bd5959 + 4fe227b commit 8bf7029
Show file tree
Hide file tree
Showing 17 changed files with 190 additions and 38 deletions.
2 changes: 1 addition & 1 deletion src/Shopizy.Api/DependencyInjectionRegister.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
5 changes: 4 additions & 1 deletion src/Shopizy.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
11 changes: 6 additions & 5 deletions src/Shopizy.Api/Shopizy.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@
<InvariantGlobalization>false</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.10"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.12"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0"/>
<PackageReference Include="Mapster" Version="7.4.0"/>
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.12">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.9.0"/>
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="7.2.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.12"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Shopizy.Contracts\Shopizy.Contracts.csproj"/>
<ProjectReference Include="..\Shopizy.Application\Shopizy.Application.csproj"/>
<ProjectReference Include="..\Shopizy.Infrastructure\Shopizy.Infrastructure.csproj"/>
</ItemGroup>
</Project>
</Project>
4 changes: 4 additions & 0 deletions src/Shopizy.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,9 @@
"SecretKey": "",
"PublishableKey": "",
"WebhookSecret": ""
},
"RedisCacheSettings": {
"ConnectionString": "",
"Password": ""
}
}
32 changes: 32 additions & 0 deletions src/Shopizy.Application/Common/Caching/ICacheHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Shopizy.Application.Common.Caching;

/// <summary>
/// Provides methods for caching operations.
/// </summary>
public interface ICacheHelper
{
/// <summary>
/// Retrieves a cached item by its key.
/// </summary>
/// <typeparam name="T">The type of the cached item.</typeparam>
/// <param name="key">The key of the cached item.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the cached item.</returns>
Task<T> GetAsync<T>(string key);

/// <summary>
/// Adds or updates a cached item with the specified key and value.
/// </summary>
/// <typeparam name="T">The type of the item to cache.</typeparam>
/// <param name="key">The key of the cached item.</param>
/// <param name="value">The value of the item to cache.</param>
/// <param name="expiration">The optional expiration time for the cached item.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null);

/// <summary>
/// Removes a cached item by its key.
/// </summary>
/// <param name="key">The key of the cached item to remove.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RemoveAsync(string key);
}
67 changes: 67 additions & 0 deletions src/Shopizy.Application/Common/Caching/RedisCacheHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Text.Json;
using Microsoft.Extensions.Options;
using StackExchange.Redis;

namespace Shopizy.Application.Common.Caching;

/// <summary>
/// Helper class for interacting with Redis cache.
/// </summary>
public class RedisCacheHelper : ICacheHelper
{
private readonly RedisSettings _redisSettings;
private readonly Lazy<ConnectionMultiplexer> _redisDbConnectionLazy;

/// <summary>
/// Initializes a new instance of the <see cref="RedisCacheHelper"/> class.
/// </summary>
/// <param name="redisSettingsOptions">The Redis settings options.</param>
public RedisCacheHelper(IOptions<RedisSettings> redisSettingsOptions)
{
_redisSettings = redisSettingsOptions.Value;
_redisDbConnectionLazy = new Lazy<ConnectionMultiplexer>(
() =>
ConnectionMultiplexer.Connect(
string.Format(_redisSettings.ConnectionString, _redisSettings.Password)
),
LazyThreadSafetyMode.ExecutionAndPublication
);
}

/// <summary>
/// Gets the cached value for the specified key.
/// </summary>
/// <typeparam name="T">The type of the cached value.</typeparam>
/// <param name="key">The cache key.</param>
/// <returns>The cached value, or default if the key does not exist.</returns>
public async Task<T> GetAsync<T>(string key)
{
var db = _redisDbConnectionLazy.Value.GetDatabase();
var data = await db.StringGetAsync(key);

return data.HasValue ? JsonSerializer.Deserialize<T>(data) : default;
}

/// <summary>
/// Sets the specified value in the cache with the specified key.
/// </summary>
/// <typeparam name="T">The type of the value to cache.</typeparam>
/// <param name="key">The cache key.</param>
/// <param name="value">The value to cache.</param>
/// <param name="expiration">The expiration time for the cached value. If null, the value does not expire.</param>
public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null)
{
var db = _redisDbConnectionLazy.Value.GetDatabase();
await db.StringSetAsync(key, JsonSerializer.Serialize(value), expiration);
}

/// <summary>
/// Removes the cached value for the specified key.
/// </summary>
/// <param name="key">The cache key.</param>
public async Task RemoveAsync(string key)
{
var db = _redisDbConnectionLazy.Value.GetDatabase();
await db.KeyDeleteAsync(key);
}
}
8 changes: 8 additions & 0 deletions src/Shopizy.Application/Common/Caching/RedisSettings.cs
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 9 additions & 1 deletion src/Shopizy.Application/DependencyInjectionRegister.cs
Original file line number Diff line number Diff line change
@@ -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 =>
{
Expand All @@ -18,7 +23,10 @@ public static IServiceCollection AddApplication(this IServiceCollection services
services.AddValidatorsFromAssemblyContaining(typeof(DependencyInjectionRegister));
// services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

services.Configure<RedisSettings>(configuration.GetSection(RedisSettings.Section));

services.AddScoped<ICurrentUser, CurrentUser>();
services.AddSingleton<ICacheHelper, RedisCacheHelper>();

return services;
}
Expand Down
15 changes: 8 additions & 7 deletions src/Shopizy.Application/Shopizy.Application.csproj
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Shopizy.Domain\Shopizy.Domain.csproj"/>
<ProjectReference Include="..\Shopizy.Domain\Shopizy.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.4.1"/>
<PackageReference Include="FluentValidation" Version="11.10.0"/>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.10.0"/>
<PackageReference Include="Ardalis.GuardClauses" Version="5.0.0"/>
<PackageReference Include="ErrorOr" Version="2.1.0"/>
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageReference Include="Ardalis.GuardClauses" Version="5.0.0" />
<PackageReference Include="ErrorOr" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.12" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
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;
using Shopizy.Domain.Users.ValueObjects;

namespace Shopizy.Application.Users.Queries.GetUser;

public class GetUserQueryHandler(IUserRepository userRepository, IOrderRepository orderRepository)
: IRequestHandler<GetUserQuery, ErrorOr<UserDto>>
public class GetUserQueryHandler(
IUserRepository userRepository,
IOrderRepository orderRepository,
ICacheHelper cacheHelper
) : IRequestHandler<GetUserQuery, ErrorOr<UserDto>>
{
private readonly IUserRepository _userRepository = userRepository;
private readonly IOrderRepository _orderRepository = orderRepository;
private readonly ICacheHelper _cacheHelper = cacheHelper;

public async Task<ErrorOr<UserDto>> Handle(
GetUserQuery request,
Expand All @@ -25,6 +30,12 @@ CancellationToken cancellationToken
return CustomErrors.User.UserNotFound;
}

var cachedUser = await _cacheHelper.GetAsync<UserDto>($"user-{user.Id.Value}");
if (cachedUser is not null)
{
return cachedUser;
}

var userOrders = _orderRepository
.GetOrdersByUserId(user.Id)
.Select(o => new { o.Id, o.OrderStatus })
Expand All @@ -50,6 +61,8 @@ CancellationToken cancellationToken
user.ModifiedOn
);

await _cacheHelper.SetAsync($"user-{user.Id.Value}", userDto, TimeSpan.FromMinutes(60));

return userDto;
}
}
2 changes: 2 additions & 0 deletions src/Shopizy.Domain/Orders/ValueObjects/Address.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json.Serialization;
using Shopizy.Domain.Common.Models;

namespace Shopizy.Domain.Orders.ValueObjects;
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/Shopizy.Domain/Users/ValueObjects/UserId.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text.Json.Serialization;
using Shopizy.Domain.Common.Models;

namespace Shopizy.Domain.Users.ValueObjects;
Expand All @@ -6,6 +7,7 @@ public sealed class UserId : AggregateRootId<Guid>
{
public override Guid Value { get; protected set; }

[JsonConstructor]
private UserId(Guid value)
{
Value = value;
Expand Down
12 changes: 6 additions & 6 deletions src/Shopizy.Infrastructure/Shopizy.Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
<ProjectReference Include="..\Shopizy.Application\Shopizy.Application.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.1.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
<PackageReference Include="CloudinaryDotNet" Version="1.26.2" />
<PackageReference Include="Stripe.net" Version="46.2.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.12" />
<PackageReference Include="CloudinaryDotNet" Version="1.27.1" />
<PackageReference Include="Stripe.net" Version="47.2.0" />
<PackageReference Include="Ardalis.GuardClauses" Version="5.0.0" />
</ItemGroup>
<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.1" />
<PackageReference Include="FluentAssertions" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Shopizy.Application\Shopizy.Application.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,12 +14,18 @@ public class GetUserQueryHandlerTests
private readonly GetUserQueryHandler _sut;
private readonly Mock<IUserRepository> _mockUserRepository;
private readonly Mock<IOrderRepository> _mockOrderRepository;
private readonly Mock<ICacheHelper> _mockCacheHelper;

public GetUserQueryHandlerTests()
{
_mockUserRepository = new Mock<IUserRepository>();
_mockOrderRepository = new Mock<IOrderRepository>();
_sut = new GetUserQueryHandler(_mockUserRepository.Object, _mockOrderRepository.Object);
_mockCacheHelper = new Mock<ICacheHelper>();
_sut = new GetUserQueryHandler(
_mockUserRepository.Object,
_mockOrderRepository.Object,
_mockCacheHelper.Object
);
}

[Fact]
Expand All @@ -32,6 +39,10 @@ public async Task ShouldReturnUserObjectWhenValidUserIdIsProvided()
.Setup(c => c.GetUserById(UserId.Create(query.UserId)))
.ReturnsAsync(user);

_mockCacheHelper
.Setup(c => c.GetAsync<UserDto>($"user-{user.Id.Value}"))
.ReturnsAsync(() => null);

// Act
var result = (await _sut.Handle(query, CancellationToken.None)).Match(x => x, x => null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Loading

0 comments on commit 8bf7029

Please sign in to comment.