Skip to content
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
2 changes: 1 addition & 1 deletion AffirmationGenerator.Client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "affirmation-generator-client",
"private": true,
"version": "1.1.1",
"version": "1.4.0",
"packageManager": "pnpm@10.28.2",
"type": "module",
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public async Task Handle_WhenLanguageIsEnglish_ShouldReturnUntranslatedAffirmati
const string affirmationText = "Good day!";
const int maxRequestsPerDay = 10;

_affirmationService.Get().Returns(affirmationText);
_affirmationService.GetAffirmation().Returns(affirmationText);

var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.English };

Expand All @@ -53,7 +53,7 @@ public async Task Handle_WhenLanguageIsEnglish_ShouldReturnUntranslatedAffirmati
response.Text.ShouldBe(affirmationText);
response.RemainingCount.ShouldBeLessThan(maxRequestsPerDay);

await _affirmationService.Received(1).Get();
await _affirmationService.Received(1).GetAffirmation();
_languageCodeMapper.Received(1).Map(AffirmationLanguage.English);
await _translatorClient.DidNotReceiveWithAnyArgs().Translate(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
}
Expand All @@ -66,7 +66,7 @@ public async Task Handle_WhenLanguageIsGerman_ShouldReturnTranslatedAffirmationI
const string affirmationTextInGerman = "Guten Tag!";
const int maxRequestsPerDay = 10;

_affirmationService.Get().Returns(affirmationText);
_affirmationService.GetAffirmation().Returns(affirmationText);

var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.German };

Expand All @@ -83,7 +83,7 @@ public async Task Handle_WhenLanguageIsGerman_ShouldReturnTranslatedAffirmationI
response.Text.ShouldBe(affirmationTextInGerman);
response.RemainingCount.ShouldBeLessThan(maxRequestsPerDay);

await _affirmationService.Received(1).Get();
await _affirmationService.Received(1).GetAffirmation();
_languageCodeMapper.Received(1).Map(AffirmationLanguage.German);
await _translatorClient.Received(1).Translate(affirmationText, LanguageCode.English, LanguageCode.German);
}
Expand All @@ -92,7 +92,7 @@ public async Task Handle_WhenLanguageIsGerman_ShouldReturnTranslatedAffirmationI
public async Task Handle_WhenNoAffirmation_ShouldReturnError()
{
// Arrange
_affirmationService.Get().Returns(Result<string>.Error(new AffirmationNotFound()));
_affirmationService.GetAffirmation().Returns(Result<string>.Error(new AffirmationNotFound()));

var getAffirmationRequest = new GetAffirmationRequest { TargetLanguage = AffirmationLanguage.German };

Expand All @@ -102,7 +102,7 @@ public async Task Handle_WhenNoAffirmation_ShouldReturnError()
// Assert
result.ShouldBeError().ShouldBeOfType<AffirmationNotFound>();

await _affirmationService.Received(1).Get();
await _affirmationService.Received(1).GetAffirmation();
_languageCodeMapper.DidNotReceiveWithAnyArgs().Map(Arg.Any<AffirmationLanguage>());
await _translatorClient.DidNotReceiveWithAnyArgs().Translate(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void SetUp()
public async Task Handle_ShouldReturnRemainingAffirmationsCount(int maxRequestsPerDay)
{
// Arrange
_affirmationService.Count().Returns(maxRequestsPerDay);
_affirmationService.GetRemainingAffirmationsCount().Returns(maxRequestsPerDay);

// Act
var result = await _query.Handle();
Expand All @@ -37,6 +37,6 @@ public async Task Handle_ShouldReturnRemainingAffirmationsCount(int maxRequestsP
response.RemainingCount.ShouldBe(maxRequestsPerDay);
response.RemainingCount.ShouldBeGreaterThanOrEqualTo(0);

await _affirmationService.Received(1).Count();
await _affirmationService.Received(1).GetRemainingAffirmationsCount();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
<PackageReference Include="Refit.HttpClientFactory">
<Version>10.0.1</Version>
</PackageReference>
<PackageReference Include="StackExchange.Redis">
<Version>2.11.8</Version>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI">
<Version>10.1.4</Version>
</PackageReference>
Expand Down
1 change: 0 additions & 1 deletion AffirmationGenerator.Server/Application/DiConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ public IServiceCollection AddApplication(IConfiguration configuration)
services.Configure<ClientOptions>(configurationSection.GetSection(nameof(ClientOptions)));

services.AddHttpContextAccessor();
services.AddMemoryCache();

services.AddScoped<GetAffirmationQuery>();
services.AddScoped<GetRemainingAffirmationsQuery>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ ILanguageCodeMapper<AffirmationLanguage> languageCodeMapper
{
public async Task<Result<AffirmationResponse>> Handle(GetAffirmationRequest request) =>
await (
from affirmation in affirmationService.Get()
from affirmation in affirmationService.GetAffirmation()
from targetLanguageCode in languageCodeMapper.Map(request.TargetLanguage)
from translatedAffirmation in Translate(affirmation, targetLanguageCode)
select ToResponse(request.TargetLanguage, translatedAffirmation)
Expand All @@ -40,6 +40,6 @@ private async Task<AffirmationResponse> ToResponse(AffirmationLanguage targetLan
{
TargetLanguage = targetLanguage,
Text = affirmation,
RemainingCount = await affirmationService.Count(),
RemainingCount = await affirmationService.GetRemainingAffirmationsCount(),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public sealed class GetRemainingAffirmationsQuery(IAffirmationService affirmatio
{
public async Task<Result<RemainingAffirmationsResponse>> Handle()
{
var remainingCount = await affirmationService.Count();
var remainingCount = await affirmationService.GetRemainingAffirmationsCount();
return new RemainingAffirmationsResponse { RemainingCount = remainingCount };
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
using AffirmationGenerator.Server.Application.Extensions;
using AffirmationGenerator.Server.Core;
using AffirmationGenerator.Server.Core.Extensions;
using AffirmationGenerator.Server.Domain;
using AffirmationGenerator.Server.Infrastructure.Affirmation;
using Microsoft.Extensions.Caching.Memory;
using AffirmationGenerator.Server.Infrastructure.Redis;
using Microsoft.Extensions.Options;

namespace AffirmationGenerator.Server.Application.Services.Affirmation;

public sealed class AffirmationService(
ILogger<AffirmationService> logger,
IAffirmationClient affirmationClient,
IMemoryCache memoryCache,
IRedisClient redisClient,
IHttpContextAccessor httpContextAccessor,
IOptions<ClientOptions> clientOptions
) : IAffirmationService
Expand All @@ -21,11 +22,9 @@ IOptions<ClientOptions> clientOptions

private string CacheKey => $"{ClientIpAddress}";

private static TimeSpan OneDay => TimeSpan.FromDays(1);

public async Task<Result<string>> Get()
public async Task<Result<string>> GetAffirmation()
{
var remainingAffirmations = await Count();
var remainingAffirmations = await GetRemainingAffirmationsCount();

var affirmationResponse = await affirmationClient.GetAffirmation();
var affirmation = affirmationResponse.Affirmation ?? string.Empty;
Expand All @@ -36,29 +35,28 @@ public async Task<Result<string>> Get()
return Result<string>.Error(new AffirmationNotFound());
}

SetCount(remainingAffirmations);
await SetRemainingAffirmationsCount(remainingAffirmations);

return Result<string>.Success(affirmation);
}

public async Task<int> Count()
public async Task<int> GetRemainingAffirmationsCount()
{
var remainingCount = await memoryCache.GetOrCreateAsync(
CacheKey,
entry =>
{
entry.SetAbsoluteExpiration(OneDay);
return Task.FromResult(ClientOptions.MaxRequestsPerDay);
}
);
var cachedValue = await redisClient.GetString(CacheKey);

if (int.TryParse(cachedValue, out var remainingCount) == false)
{
remainingCount = ClientOptions.MaxRequestsPerDay;
await redisClient.SetString(CacheKey, $"{remainingCount}", TimeSpan.OneDay);
}

if (logger.IsEnabled(LogLevel.Information))
logger.LogInformation("{RemainingCount} affirmations remain for user {ClientIpAddress}", remainingCount, ClientIpAddress);

return remainingCount;
}

private void SetCount(int count)
private async Task SetRemainingAffirmationsCount(int count)
{
if (count <= 0)
return;
Expand All @@ -68,6 +66,6 @@ private void SetCount(int count)
if (count <= 0)
count = 0;

memoryCache.Set(CacheKey, count, OneDay);
await redisClient.SetString(CacheKey, $"{count}", TimeSpan.OneDay);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace AffirmationGenerator.Server.Application.Services.Affirmation;

public interface IAffirmationService
{
Task<Result<string>> Get();
Task<Result<string>> GetAffirmation();

Task<int> Count();
Task<int> GetRemainingAffirmationsCount();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace AffirmationGenerator.Server.Core.Extensions;

public static class TimeSpanExtensions
{
extension(TimeSpan)
{
public static TimeSpan OneDay => TimeSpan.FromDays(1);
}
}
19 changes: 17 additions & 2 deletions AffirmationGenerator.Server/Infrastructure/DiConfig.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using AffirmationGenerator.Server.Infrastructure.Affirmation;
using AffirmationGenerator.Server.Infrastructure.DeepL;
using AffirmationGenerator.Server.Infrastructure.Redis;
using Microsoft.Extensions.Options;
using Refit;
using StackExchange.Redis;

namespace AffirmationGenerator.Server.Infrastructure;

Expand All @@ -12,8 +14,10 @@ public static class DiConfig
public IServiceCollection AddInfrastructure(IConfiguration configuration)
{
var configurationSection = configuration.GetSection("Infrastructure");

return services.AddDeepLTranslatorClient(configurationSection).AddAffirmationClient(configurationSection);
return services
.AddDeepLTranslatorClient(configurationSection)
.AddAffirmationClient(configurationSection)
.AddRedis(configurationSection);
}

private IServiceCollection AddDeepLTranslatorClient(IConfiguration configuration)
Expand All @@ -36,6 +40,17 @@ private IServiceCollection AddAffirmationClient(IConfiguration configuration)
httpClient.BaseAddress = new Uri(baseUrl);
}
);
return services;
}

private IServiceCollection AddRedis(IConfiguration configuration)
{
var connectionString =
configuration.GetSection(nameof(RedisClientOptions)).GetValue<string>(nameof(RedisClientOptions.ConnectionString))
?? string.Empty;

services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(connectionString));
services.AddScoped<IRedisClient, RedisClient>();

return services;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace AffirmationGenerator.Server.Infrastructure.Redis;

public interface IRedisClient
{
Task<string?> GetString(string key);

Task<bool> SetString(string key, string value, TimeSpan expiration);
}
19 changes: 19 additions & 0 deletions AffirmationGenerator.Server/Infrastructure/Redis/RedisClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using StackExchange.Redis;

namespace AffirmationGenerator.Server.Infrastructure.Redis;

public sealed class RedisClient(IConnectionMultiplexer redis) : IRedisClient
{
private IDatabase Database => redis.GetDatabase();

public async Task<string?> GetString(string key)
{
var value = await Database.StringGetAsync(GetPrefixKey(key));
return value.HasValue == false ? null : value.ToString();
}

public async Task<bool> SetString(string key, string value, TimeSpan expiration) =>
await Database.StringSetAsync(GetPrefixKey(key), value, expiration);

private static string GetPrefixKey(string key) => $"{nameof(RedisClient)}:{key}";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace AffirmationGenerator.Server.Infrastructure.Redis;

public sealed record RedisClientOptions
{
public required string ConnectionString { get; init; }
}
3 changes: 3 additions & 0 deletions AffirmationGenerator.Server/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
},
"AffirmationClientOptions": {
"BaseUrl": "https://www.affirmations.dev"
},
"RedisClientOptions": {
"ConnectionString": ""
}
}
}