Skip to content
9 changes: 9 additions & 0 deletions server/nt.microservice/infrastructure/nt.helper/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ public static class ReviewService
{
public const string ServiceName = "nt-reviewservice-service";


public static class Cache
{
public const string InstanceName = "nt-reviewservice-cache";
public const string ContainerName = "nt.reviewservice.cache";
public const string UserNameKey = $"{ServiceName}-UserName";
public const string PasswordKey = $"{ServiceName}-Password";
}

public static class Database
{
public const string InstanceName = "nt-reviewservice-db";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@
.WithDataVolume()
.WithMongoExpress();

var redisPassword = builder.AddParameter(Constants.ReviewService.Cache.PasswordKey, "Password123", secret: true);
var redisReview = builder.AddRedis(Constants.ReviewService.Cache.InstanceName, port: 6379, password: redisPassword)
.WithEnvironment("Redis__Host", Constants.ReviewService.Cache.InstanceName) // Set in your app
.WithEnvironment("Redis__Port", "6379")
.WithContainerName(Constants.ReviewService.Cache.ContainerName)
.WithHttpEndpoint(port: 8081, targetPort: 8081, isProxied: true)
.WithRedisInsight()
.WithRedisCommander();

var blobStorage = builder.AddContainer("nt-userservice-blobstorage", infrastructureSettings.BlobStorage.DockerImage)
.WithVolume("//d/Source/nt/server/nt.microservice/services/UserService/BlobStorage:/data")
.WithArgs("azurite-blob", "--blobHost", "0.0.0.0", "-l", "/data")
Expand Down Expand Up @@ -180,7 +189,9 @@
.WithEnvironment(Constants.Global.EnvironmentVariables.RunningWithVariable, Constants.Global.EnvironmentVariables.RunningWithValue)
.WithUrls(c => c.Urls.ForEach(u => u.DisplayText = $"Open API ({u.Endpoint?.EndpointName})"))
.WithReference(mongoDbReview)
.WaitFor(mongoDbReview);
.WaitFor(mongoDbReview)
.WaitFor(redisReview)
.WithReference(redisReview);


var gateway = builder.AddProject<Projects.nt_gateway>(Constants.Gateway.ServiceName, launchProfileName: Constants.Gateway.LaunchProfile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
<PackageReference Include="Aspire.Hosting.MongoDB" Version="9.3.0" />
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.3.0" />
<PackageReference Include="Aspire.Hosting.RabbitMQ" Version="9.3.0" />
<PackageReference Include="Aspire.Hosting.Redis" Version="9.3.1" />
<PackageReference Include="Aspire.Hosting.SqlServer" Version="9.3.0" />
<PackageReference Include="Aspire.StackExchange.Redis" Version="9.3.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ public class ReviewDto

public string Content { get; set; } = string.Empty;
public int Rating { get; set; }
public string UserName { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public DateTime CreatedOn { get; set; } = DateTime.UtcNow;

public IEnumerable<string> UpvotedBy { get; set; } = [];
public IEnumerable<string> DownvotedBy { get; set; } = [];

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace ReviewService.Application.Interfaces.Operations;

public interface IReviewService
{
Task<IEnumerable<ReviewDto>> GetRecentReviewsForUsersAsync(IEnumerable<Guid> userIds, int count = 3);
Task<IEnumerable<ReviewDto>> GetRecentReviewsForUsersAsync(IEnumerable<string> userIds, int count = 3);
Task<IEnumerable<ReviewDto>> GetReviewsByMovieIdAsync(Guid movieId);
Task<Guid> CreateReviewAsync(ReviewDto reviewDto);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace ReviewService.Application.Interfaces.Services;

public interface ICachingService
{
TimeSpan ExpirationTime { get; }
Task StringSetAsync<T>(string key, T value) where T : class;
Task SortedSetAsync<T>(string key, T value, double score) where T : class;
Task<T?> StringGetAsync<T>(string key) where T : class;
Task<IEnumerable<T>> SortedSetRangeByScoreAsync<T>(string key, int count) where T : class;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using ReviewService.Application.DTO.Reviews;

namespace ReviewService.Application.Interfaces.Services;

public interface IReviewCachingService
{
Task SaveInCache(ReviewDto review);
Task<ReviewDto?> ReadCache(Guid reviewId);
Task<IEnumerable<ReviewDto>> ReadUserRecentReviews(string userName, int count = 10);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ namespace ReviewService.Application.Orchestration.Queries;

public class GetRecentReviewsForUsersQuery : IRequest<IEnumerable<ReviewDto>>
{
public IEnumerable<Guid> UserIds { get; set; } = [];
public IEnumerable<string> UserIds { get; set; } = [];
public int Count { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace ReviewService.Application.Services.Extensions;

public static class DateTimeExtensions
{
/// <summary>
/// Converts a DateTime to a Unix timestamp.
/// </summary>
/// <param name="dateTime">The DateTime to convert.</param>
/// <returns>The Unix timestamp as a long.</returns>
public static long ToUnixTimestamp(this DateTime dateTime)
{
return new DateTimeOffset(dateTime).ToUnixTimeSeconds();
}
/// <summary>
/// Converts a Unix timestamp to a DateTime.
/// </summary>
/// <param name="unixTimestamp">The Unix timestamp to convert.</param>
/// <returns>The DateTime representation of the Unix timestamp.</returns>
public static DateTime FromUnixTimestamp(long unixTimestamp)
{
return DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).UtcDateTime;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using Microsoft.Extensions.Logging;
using ReviewService.Application.DTO.Reviews;
using ReviewService.Application.Interfaces.Operations;
using ReviewService.Application.Interfaces.Services;
using ReviewService.Application.Services.Extensions;
using ReviewService.Domain.Entities;
using ReviewService.Domain.Repositories;

Expand All @@ -12,11 +14,13 @@ public class ReviewService : IReviewService
private readonly IReviewRepository _reviewRepository;
private readonly IMapper _mapper;
private readonly ILogger _logger;
public ReviewService(IReviewRepository reviewRepository,IMapper mapper, ILogger<ReviewService> logger)
private readonly IReviewCachingService _reviewCachingService;
public ReviewService(IReviewRepository reviewRepository,IReviewCachingService cachingService, IMapper mapper, ILogger<ReviewService> logger)
{
_reviewRepository = reviewRepository ?? throw new ArgumentNullException(nameof(reviewRepository));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_reviewCachingService = cachingService ?? throw new ArgumentNullException(nameof(cachingService));
}
public Task<IEnumerable<ReviewDto>> GetReviewsByMovieIdAsync(Guid movieId)
{
Expand All @@ -27,7 +31,7 @@ public async Task<Guid> CreateReviewAsync(ReviewDto reviewDto)
{
try
{
var review = await _reviewRepository.AddAsync(_mapper.Map<ReviewDto, Review>(reviewDto)).ConfigureAwait(false);
var review = await _reviewRepository.AddAsync(_mapper.Map<ReviewDto, ReviewEntity>(reviewDto)).ConfigureAwait(false);
return review.Id;
}
catch (Exception ex)
Expand All @@ -37,16 +41,46 @@ public async Task<Guid> CreateReviewAsync(ReviewDto reviewDto)
}
}

public async Task<IEnumerable<ReviewDto>> GetRecentReviewsForUsersAsync(IEnumerable<Guid> userIds, int count = 3)
public async Task<IEnumerable<ReviewDto>> GetRecentReviewsForUsersAsync(IEnumerable<string> userIds, int count = 3)
{
try
{
var results = await _reviewRepository.GetRecentReviewsForUsersAsync(userIds, count);
return _mapper.Map<IEnumerable<Review>, IEnumerable<ReviewDto>>(results);
var results = new List<ReviewDto>();
var nonCachedUsers = new List<string>();

foreach(var id in userIds)
{
var cacheKey = $"user:{id}:recentReviews";
var cachedReviews = await _reviewCachingService.ReadUserRecentReviews(id,3).ConfigureAwait(false);

if (cachedReviews != null && cachedReviews.Any())
{
results.AddRange(cachedReviews);
}
else
{
nonCachedUsers.Add(id);
}

}
var dbResults = await _reviewRepository.GetRecentReviewsForUsersAsync(nonCachedUsers, count);

foreach (var review in dbResults)
{
var cacheKey = $"user:{review.Author}:recentReviews";
var reviewDto = _mapper.Map<ReviewEntity, ReviewDto>(review);

// Cache the review for future requests
await _reviewCachingService.SaveInCache(reviewDto).ConfigureAwait(false);
}

results.AddRange(_mapper.Map<IEnumerable<ReviewEntity>, IEnumerable<ReviewDto>>(dbResults));

return results.OrderByDescending(x=>x.CreatedOn);
}
catch (Exception)
catch (Exception ex)
{

_logger.LogError(ex, "An error occurred while creating a review.");
throw;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.StackExchange.Redis" Version="9.3.1" />
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Microsoft.Extensions.Options;
using ReviewService.Application.Interfaces.Services;
using StackExchange.Redis;
using System.Text.Json;

namespace ReviewService.Application.Services.Services;

public class CachingService(IConnectionMultiplexer connectionMultiplexer, int timeOutInMinutes) : ICachingService
{
public IConnectionMultiplexer ConnectionMultiplexer => connectionMultiplexer;
public IDatabase Database => ConnectionMultiplexer.GetDatabase();

public TimeSpan ExpirationTime { get; } = TimeSpan.FromMinutes(timeOutInMinutes);

public async Task StringSetAsync<T>(string key, T value) where T : class
{
await Database.StringSetAsync(key, JsonSerializer.Serialize(value), ExpirationTime);
}

public async Task SortedSetAsync<T>(string key, T value, double score) where T : class
{
var serializedValue = JsonSerializer.Serialize(value);
await Database.SortedSetAddAsync(key, serializedValue, score); //DateTimeOffset.UtcNow.ToUnixTimeSeconds());
await Database.KeyExpireAsync(key, ExpirationTime);
}
public async Task<T?> StringGetAsync<T>(string key) where T : class
{
var value = await Database.StringGetAsync(key);
return value.IsNullOrEmpty ? null : JsonSerializer.Deserialize<T>(value!);
}

public async Task<IEnumerable<T>> SortedSetRangeByScoreAsync<T>(string key, int count) where T : class
{
var values = await Database.SortedSetRangeByScoreAsync(key, order: Order.Descending, take: count);
return values.Select(value => JsonSerializer.Deserialize<T>(value!.ToString())).Where(value => value != null)!;
}

public async Task<IEnumerable<string>> SortedSetRangeByScoreAsync(string key, int count)
{
var values = await Database.SortedSetRangeByScoreAsync(key, order: Order.Descending, take: count);
return values.Select(value => value.ToString()!);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using AutoMapper;
using ReviewService.Application.DTO.Reviews;
using ReviewService.Application.Interfaces.Services;
using ReviewService.Application.Services.Extensions;

namespace ReviewService.Application.Services.Services;

public class ReviewCachingService(ICachingService cachingService, IMapper mapper) : IReviewCachingService
{
protected ICachingService CachingService => cachingService;
protected IMapper Mapper => mapper;

public async Task SaveInCache(ReviewDto review)
{
var reviewCacheKey = GenerateReviewCacheKey(review.Id);
var sortedCacheKey = GenerateUserRecentReviewsCacheKey(review.Author);

// Cache the review for future requests
await CachingService.StringSetAsync(reviewCacheKey, review).ConfigureAwait(false);
await CachingService.SortedSetAsync(sortedCacheKey, reviewCacheKey, review.CreatedOn.ToUnixTimestamp()).ConfigureAwait(false);
}

public async Task<ReviewDto?> ReadCache(Guid reviewId)
{
var reviewCacheKey = GenerateReviewCacheKey(reviewId);
return await CachingService.StringGetAsync<ReviewDto>(reviewCacheKey).ConfigureAwait(false);
}

public async Task<IEnumerable<ReviewDto>> ReadUserRecentReviews(string userName, int count = 10)
{
var sortedCacheKey = GenerateUserRecentReviewsCacheKey(userName);
var reviewKeys = await CachingService.SortedSetRangeByScoreAsync<string>(sortedCacheKey, count).ConfigureAwait(false);
var reviews = new List<ReviewDto>();
foreach (var reviewKey in reviewKeys)
{
var review = await CachingService.StringGetAsync<ReviewDto>(reviewKey).ConfigureAwait(false);
if (review != null)
{
reviews.Add(review);
}
}

return reviews.OrderByDescending(r => r.CreatedOn).ToList();
}

private static string GenerateReviewCacheKey(Guid id) => $"review:{id}";

private static string GenerateUserRecentReviewsCacheKey(string userName) => $"user:{userName}:recentReviews";
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
namespace ReviewService.Domain.Entities;
public class Review : IEntity
public class ReviewEntity : IEntity
{
public Guid Id { get; set; }
public string MovieId { get; set; } = null!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

namespace ReviewService.Domain.Repositories;

public interface IReviewRepository : IGenericRepository<Review>
public interface IReviewRepository : IGenericRepository<ReviewEntity>
{
Task<IEnumerable<Review>> GetReviewsByMovieIdAsync(Guid movieId);
Task<IEnumerable<Review>> GetReviewsByUserIdAsync(Guid userId);
Task<IEnumerable<Review>> GetReviewsByRatingAsync(int rating);
Task<IEnumerable<Review>> GetReviewsByDateRangeAsync(DateTime startDate, DateTime endDate);
Task<IEnumerable<Review>> GetRecentReviewsForUsersAsync(IEnumerable<Guid> userIds, int count = 3);
Task<IEnumerable<ReviewEntity>> GetReviewsByMovieIdAsync(Guid movieId);
Task<IEnumerable<ReviewEntity>> GetReviewsByUserIdAsync(Guid userId);
Task<IEnumerable<ReviewEntity>> GetReviewsByRatingAsync(int rating);
Task<IEnumerable<ReviewEntity>> GetReviewsByDateRangeAsync(DateTime startDate, DateTime endDate);
Task<IEnumerable<ReviewEntity>> GetRecentReviewsForUsersAsync(IEnumerable<string> userIds, int count = 3);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MongoDB.Entities;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Entities;

namespace ReviewService.Infrastructure.Repository.Documents;

Expand All @@ -8,22 +9,27 @@ public class ReviewDocument:Entity
internal const string CollectionName = "reviews";


[Field("movieId")]
[BsonElement("movieId")]
public Guid MovieId { get; set; }

[Field("title")]
[BsonElement("title")]
public string Title { get; set; } = string.Empty;

[Field("content")]
[BsonElement("content")]
public string Content { get; set; } = string.Empty;

[Field("rating")]
[BsonElement("rating")]

public int Rating { get; set; }

[Field("author")]
[BsonElement("author")]
public string Author { get; set; } = string.Empty;

[Field("createdOn")]
[BsonElement("createdOn")]
public DateTime CreatedOn { get; set; } = DateTime.UtcNow;

public IEnumerable<string> UpvotedBy { get; set; } = [];

public IEnumerable<string> DownvotedBy { get; set; } = [];

}
Loading