diff --git a/server/nt.microservice/infrastructure/nt.helper/Constants.cs b/server/nt.microservice/infrastructure/nt.helper/Constants.cs index 03ac84ae..6d2c350c 100644 --- a/server/nt.microservice/infrastructure/nt.helper/Constants.cs +++ b/server/nt.microservice/infrastructure/nt.helper/Constants.cs @@ -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"; diff --git a/server/nt.microservice/infrastructure/nt.orchestrator.AppHost/Program.cs b/server/nt.microservice/infrastructure/nt.orchestrator.AppHost/Program.cs index 25daa839..82692a89 100644 --- a/server/nt.microservice/infrastructure/nt.orchestrator.AppHost/Program.cs +++ b/server/nt.microservice/infrastructure/nt.orchestrator.AppHost/Program.cs @@ -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") @@ -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(Constants.Gateway.ServiceName, launchProfileName: Constants.Gateway.LaunchProfile) diff --git a/server/nt.microservice/infrastructure/nt.orchestrator.AppHost/nt.orchestrator.AppHost.csproj b/server/nt.microservice/infrastructure/nt.orchestrator.AppHost/nt.orchestrator.AppHost.csproj index 5e65dca2..35af664b 100644 --- a/server/nt.microservice/infrastructure/nt.orchestrator.AppHost/nt.orchestrator.AppHost.csproj +++ b/server/nt.microservice/infrastructure/nt.orchestrator.AppHost/nt.orchestrator.AppHost.csproj @@ -17,7 +17,9 @@ + + diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Application.DTO/Reviews/ReviewDto.cs b/server/nt.microservice/services/ReviewService/ReviewService.Application.DTO/Reviews/ReviewDto.cs index 8f5c2e43..a3bc5e3a 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Application.DTO/Reviews/ReviewDto.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Application.DTO/Reviews/ReviewDto.cs @@ -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 UpvotedBy { get; set; } = []; + public IEnumerable DownvotedBy { get; set; } = []; } diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Application.Interfaces/Operations/IReviewService.cs b/server/nt.microservice/services/ReviewService/ReviewService.Application.Interfaces/Operations/IReviewService.cs index 067fcdee..3e5cdc9f 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Application.Interfaces/Operations/IReviewService.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Application.Interfaces/Operations/IReviewService.cs @@ -4,7 +4,7 @@ namespace ReviewService.Application.Interfaces.Operations; public interface IReviewService { - Task> GetRecentReviewsForUsersAsync(IEnumerable userIds, int count = 3); + Task> GetRecentReviewsForUsersAsync(IEnumerable userIds, int count = 3); Task> GetReviewsByMovieIdAsync(Guid movieId); Task CreateReviewAsync(ReviewDto reviewDto); } diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Application.Interfaces/Services/ICachingService.cs b/server/nt.microservice/services/ReviewService/ReviewService.Application.Interfaces/Services/ICachingService.cs new file mode 100644 index 00000000..ac84ef42 --- /dev/null +++ b/server/nt.microservice/services/ReviewService/ReviewService.Application.Interfaces/Services/ICachingService.cs @@ -0,0 +1,10 @@ +namespace ReviewService.Application.Interfaces.Services; + +public interface ICachingService +{ + TimeSpan ExpirationTime { get; } + Task StringSetAsync(string key, T value) where T : class; + Task SortedSetAsync(string key, T value, double score) where T : class; + Task StringGetAsync(string key) where T : class; + Task> SortedSetRangeByScoreAsync(string key, int count) where T : class; +} diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Application.Interfaces/Services/IReviewCachingService.cs b/server/nt.microservice/services/ReviewService/ReviewService.Application.Interfaces/Services/IReviewCachingService.cs new file mode 100644 index 00000000..78e662ce --- /dev/null +++ b/server/nt.microservice/services/ReviewService/ReviewService.Application.Interfaces/Services/IReviewCachingService.cs @@ -0,0 +1,10 @@ +using ReviewService.Application.DTO.Reviews; + +namespace ReviewService.Application.Interfaces.Services; + +public interface IReviewCachingService +{ + Task SaveInCache(ReviewDto review); + Task ReadCache(Guid reviewId); + Task> ReadUserRecentReviews(string userName, int count = 10); +} diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Application.Orchestration/Queries/GetRecentReviewsForUsersQuery.cs b/server/nt.microservice/services/ReviewService/ReviewService.Application.Orchestration/Queries/GetRecentReviewsForUsersQuery.cs index ea9b6c04..87919a92 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Application.Orchestration/Queries/GetRecentReviewsForUsersQuery.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Application.Orchestration/Queries/GetRecentReviewsForUsersQuery.cs @@ -5,6 +5,6 @@ namespace ReviewService.Application.Orchestration.Queries; public class GetRecentReviewsForUsersQuery : IRequest> { - public IEnumerable UserIds { get; set; } = []; + public IEnumerable UserIds { get; set; } = []; public int Count { get; set; } } \ No newline at end of file diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Extensions/DateTimeExtensions.cs b/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Extensions/DateTimeExtensions.cs new file mode 100644 index 00000000..e9d494c1 --- /dev/null +++ b/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Extensions/DateTimeExtensions.cs @@ -0,0 +1,23 @@ +namespace ReviewService.Application.Services.Extensions; + +public static class DateTimeExtensions +{ + /// + /// Converts a DateTime to a Unix timestamp. + /// + /// The DateTime to convert. + /// The Unix timestamp as a long. + public static long ToUnixTimestamp(this DateTime dateTime) + { + return new DateTimeOffset(dateTime).ToUnixTimeSeconds(); + } + /// + /// Converts a Unix timestamp to a DateTime. + /// + /// The Unix timestamp to convert. + /// The DateTime representation of the Unix timestamp. + public static DateTime FromUnixTimestamp(long unixTimestamp) + { + return DateTimeOffset.FromUnixTimeSeconds(unixTimestamp).UtcDateTime; + } +} diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Operations/ReviewService.cs b/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Operations/ReviewService.cs index f6e5af83..f250eaa0 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Operations/ReviewService.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Operations/ReviewService.cs @@ -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; @@ -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 logger) + private readonly IReviewCachingService _reviewCachingService; + public ReviewService(IReviewRepository reviewRepository,IReviewCachingService cachingService, IMapper mapper, ILogger 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> GetReviewsByMovieIdAsync(Guid movieId) { @@ -27,7 +31,7 @@ public async Task CreateReviewAsync(ReviewDto reviewDto) { try { - var review = await _reviewRepository.AddAsync(_mapper.Map(reviewDto)).ConfigureAwait(false); + var review = await _reviewRepository.AddAsync(_mapper.Map(reviewDto)).ConfigureAwait(false); return review.Id; } catch (Exception ex) @@ -37,16 +41,46 @@ public async Task CreateReviewAsync(ReviewDto reviewDto) } } - public async Task> GetRecentReviewsForUsersAsync(IEnumerable userIds, int count = 3) + public async Task> GetRecentReviewsForUsersAsync(IEnumerable userIds, int count = 3) { try { - var results = await _reviewRepository.GetRecentReviewsForUsersAsync(userIds, count); - return _mapper.Map, IEnumerable>(results); + var results = new List(); + var nonCachedUsers = new List(); + + 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(review); + + // Cache the review for future requests + await _reviewCachingService.SaveInCache(reviewDto).ConfigureAwait(false); + } + + results.AddRange(_mapper.Map, IEnumerable>(dbResults)); + + return results.OrderByDescending(x=>x.CreatedOn); } - catch (Exception) + catch (Exception ex) { - + _logger.LogError(ex, "An error occurred while creating a review."); throw; } } diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/ReviewService.Application.Services.csproj b/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/ReviewService.Application.Services.csproj index 57630032..c300ab49 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/ReviewService.Application.Services.csproj +++ b/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/ReviewService.Application.Services.csproj @@ -7,6 +7,7 @@ + diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Services/CachingService.cs b/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Services/CachingService.cs new file mode 100644 index 00000000..9211d7a5 --- /dev/null +++ b/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Services/CachingService.cs @@ -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(string key, T value) where T : class + { + await Database.StringSetAsync(key, JsonSerializer.Serialize(value), ExpirationTime); + } + + public async Task SortedSetAsync(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 StringGetAsync(string key) where T : class + { + var value = await Database.StringGetAsync(key); + return value.IsNullOrEmpty ? null : JsonSerializer.Deserialize(value!); + } + + public async Task> SortedSetRangeByScoreAsync(string key, int count) where T : class + { + var values = await Database.SortedSetRangeByScoreAsync(key, order: Order.Descending, take: count); + return values.Select(value => JsonSerializer.Deserialize(value!.ToString())).Where(value => value != null)!; + } + + public async Task> SortedSetRangeByScoreAsync(string key, int count) + { + var values = await Database.SortedSetRangeByScoreAsync(key, order: Order.Descending, take: count); + return values.Select(value => value.ToString()!); + } +} diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Services/ReviewCachingService.cs b/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Services/ReviewCachingService.cs new file mode 100644 index 00000000..ac3b00a8 --- /dev/null +++ b/server/nt.microservice/services/ReviewService/ReviewService.Application.Services/Services/ReviewCachingService.cs @@ -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 ReadCache(Guid reviewId) + { + var reviewCacheKey = GenerateReviewCacheKey(reviewId); + return await CachingService.StringGetAsync(reviewCacheKey).ConfigureAwait(false); + } + + public async Task> ReadUserRecentReviews(string userName, int count = 10) + { + var sortedCacheKey = GenerateUserRecentReviewsCacheKey(userName); + var reviewKeys = await CachingService.SortedSetRangeByScoreAsync(sortedCacheKey, count).ConfigureAwait(false); + var reviews = new List(); + foreach (var reviewKey in reviewKeys) + { + var review = await CachingService.StringGetAsync(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"; +} diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Domain/Entities/Review.cs b/server/nt.microservice/services/ReviewService/ReviewService.Domain/Entities/ReviewEntity.cs similarity index 93% rename from server/nt.microservice/services/ReviewService/ReviewService.Domain/Entities/Review.cs rename to server/nt.microservice/services/ReviewService/ReviewService.Domain/Entities/ReviewEntity.cs index 189c50d2..da27038d 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Domain/Entities/Review.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Domain/Entities/ReviewEntity.cs @@ -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!; diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Domain/Repositories/IReviewRepository.cs b/server/nt.microservice/services/ReviewService/ReviewService.Domain/Repositories/IReviewRepository.cs index b4be932f..e757f3c0 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Domain/Repositories/IReviewRepository.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Domain/Repositories/IReviewRepository.cs @@ -2,11 +2,11 @@ namespace ReviewService.Domain.Repositories; -public interface IReviewRepository : IGenericRepository +public interface IReviewRepository : IGenericRepository { - Task> GetReviewsByMovieIdAsync(Guid movieId); - Task> GetReviewsByUserIdAsync(Guid userId); - Task> GetReviewsByRatingAsync(int rating); - Task> GetReviewsByDateRangeAsync(DateTime startDate, DateTime endDate); - Task> GetRecentReviewsForUsersAsync(IEnumerable userIds, int count = 3); + Task> GetReviewsByMovieIdAsync(Guid movieId); + Task> GetReviewsByUserIdAsync(Guid userId); + Task> GetReviewsByRatingAsync(int rating); + Task> GetReviewsByDateRangeAsync(DateTime startDate, DateTime endDate); + Task> GetRecentReviewsForUsersAsync(IEnumerable userIds, int count = 3); } \ No newline at end of file diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Documents/ReviewDocument.cs b/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Documents/ReviewDocument.cs index 0add6001..1e5a2fa1 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Documents/ReviewDocument.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Documents/ReviewDocument.cs @@ -1,4 +1,5 @@ -using MongoDB.Entities; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Entities; namespace ReviewService.Infrastructure.Repository.Documents; @@ -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 UpvotedBy { get; set; } = []; + + public IEnumerable DownvotedBy { get; set; } = []; + } diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Repositories/ReviewRepository.cs b/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Repositories/ReviewRepository.cs index 5add2101..4ca7d54d 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Repositories/ReviewRepository.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Repositories/ReviewRepository.cs @@ -6,19 +6,19 @@ namespace ReviewService.Infrastructure.Repository.Repositories; -public class ReviewRepository(IMongoDatabase mongoDatabase,IMapper mapper) : GenericRepository(mongoDatabase,mapper, "Reviews"), IReviewRepository +public class ReviewRepository(IMongoDatabase mongoDatabase,IMapper mapper) : GenericRepository(mongoDatabase,mapper, ReviewDocument.CollectionName), IReviewRepository { - public async Task> GetRecentReviewsForUsersAsync(IEnumerable userIds, int count = 10) + public async Task> GetRecentReviewsForUsersAsync(IEnumerable userIds, int count = 10) { var filter = Builders.Filter.In(r => r.Author, userIds.Select(u => u.ToString())); - var result = await Collection + var query = Collection .Find(filter) - .SortByDescending(r => r.CreatedOn) - .Limit(count).ToCursorAsync().ConfigureAwait(false); - return Mapper.Map>(result.ToEnumerable()) ?? throw new InvalidOperationException("No recent reviews found for the specified users."); + .SortByDescending(r => r.CreatedOn).Limit(count); + var result = await query.ToCursorAsync().ConfigureAwait(false); + return Mapper.Map>(result.ToEnumerable()) ?? throw new InvalidOperationException("No recent reviews found for the specified users."); } - public async Task> GetReviewsByDateRangeAsync(DateTime startDate, DateTime endDate) + public async Task> GetReviewsByDateRangeAsync(DateTime startDate, DateTime endDate) { var filter = Builders.Filter.And( @@ -36,20 +36,20 @@ public async Task> GetReviewsByDateRangeAsync(DateTime start .ToCursorAsync() .ConfigureAwait(false); - return Mapper.Map>(result.ToEnumerable() ?? throw new InvalidOperationException("No reviews found in the specified date range.")); + return Mapper.Map>(result.ToEnumerable() ?? throw new InvalidOperationException("No reviews found in the specified date range.")); } - public async Task> GetReviewsByMovieIdAsync(Guid movieId) + public async Task> GetReviewsByMovieIdAsync(Guid movieId) { var filter = Builders.Filter.Eq(r => r.MovieId, movieId); var result = await Collection .Find(filter) .ToCursorAsync().ConfigureAwait(false); - return Mapper.Map>(result.ToEnumerable() ?? throw new InvalidOperationException("No reviews found for the specified movie.")); + return Mapper.Map>(result.ToEnumerable() ?? throw new InvalidOperationException("No reviews found for the specified movie.")); } - public async Task> GetReviewsByRatingAsync(int rating) + public async Task> GetReviewsByRatingAsync(int rating) { var filter = Builders.Filter.Eq(r => r.Rating, rating); var result = await Collection @@ -57,10 +57,10 @@ public async Task> GetReviewsByRatingAsync(int rating) .ToCursorAsync() .ConfigureAwait(false); - return Mapper.Map>(result.ToEnumerable() ?? throw new InvalidOperationException("No reviews found with the specified rating.")); + return Mapper.Map>(result.ToEnumerable() ?? throw new InvalidOperationException("No reviews found with the specified rating.")); } - public async Task> GetReviewsByUserIdAsync(Guid userId) + public async Task> GetReviewsByUserIdAsync(Guid userId) { var filter = Builders.Filter.Eq(r => r.Author, userId.ToString()); var result = await Collection @@ -68,6 +68,6 @@ public async Task> GetReviewsByUserIdAsync(Guid userId) .ToCursorAsync() .ConfigureAwait(false); - return Mapper.Map>(result.ToEnumerable() ?? throw new InvalidOperationException("No reviews found for the specified user.")); + return Mapper.Map>(result.ToEnumerable() ?? throw new InvalidOperationException("No reviews found for the specified user.")); } } diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Seed/MalayalamReviewsSeed.cs b/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Seed/MalayalamReviewsSeed.cs index c2ad9435..5c9738ef 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Seed/MalayalamReviewsSeed.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Infrastructure.Repository/Seed/MalayalamReviewsSeed.cs @@ -11,7 +11,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("6191e634-14c8-45d1-898f-191060cdbec1"), Rating = 5, Title = "Ustad Hotel Feels", - Author = "moviebuff_91" + Author = "jia.anu" }, new() { Content = "Dulquer and Thilakan make this movie a beautiful emotional ride. Music was perfect.", @@ -19,7 +19,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("6191e634-14c8-45d1-898f-191060cdbec1"), Rating = 4, Title = "Beautiful Blend", - Author = "cinemalover" + Author = "jia.anu" }, new() { Content = "Joji is an intense, slow burn thriller. Brilliant acting by Fahadh as always.", @@ -27,7 +27,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("bea50cb7-41b6-4a35-b77f-358e8c43f850"), Rating = 4, Title = "Dark and Gripping", - Author = "screenaddict" + Author = "naina.anu" }, new() { Content = "Minimal dialogues, maximum impact. Joji is a masterclass in modern Malayalam cinema.", @@ -35,7 +35,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("bea50cb7-41b6-4a35-b77f-358e8c43f850"), Rating = 5, Title = "Minimalist Brilliance", - Author = "cinecritic" + Author = "jia.anu" }, new() { Content = "A hilarious ride with unexpected twists. Oru Vadakkan Selfie is pure fun.", @@ -43,7 +43,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("bd4b80a0-1516-4b71-887b-714c52459f23"), Rating = 4, Title = "Comedy Hit", - Author = "ajay.m" + Author = "naina.anu" }, new() { Content = "Selfie gone wrong but in the best way possible. Loved the storytelling and humor.", @@ -51,7 +51,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("bd4b80a0-1516-4b71-887b-714c52459f23"), Rating = 5, Title = "Smart Comedy", - Author = "techjunkie" + Author = "jia.anu" }, new() { Content = "Malik is powerful. A political saga with gripping performances. Fahadh nailed it!", @@ -59,7 +59,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("0003be11-f19a-4b9c-a1b2-b7e195b53d3e"), Rating = 5, Title = "Political Power", - Author = "seriouscinema" + Author = "naina.anu" }, new() { Content = "One of the best performances by Fahadh. Malik stays with you long after it ends.", @@ -67,7 +67,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("0003be11-f19a-4b9c-a1b2-b7e195b53d3e"), Rating = 5, Title = "Brilliant Execution", - Author = "rajfilm" + Author = "jia.anu" }, new() { Content = "Premam is nostalgic, fun, and full of charm. Every phase of love was shown beautifully.", @@ -75,7 +75,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("af3e9bed-3e04-4f06-856d-3c572605bf4d"), Rating = 5, Title = "Love Story Goals", - Author = "dreamy_eyes" + Author = "naina.anu" }, new() { Content = "Great music, wonderful performances. Premam is a modern Malayalam classic.", @@ -83,7 +83,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("af3e9bed-3e04-4f06-856d-3c572605bf4d"), Rating = 5, Title = "Evergreen", - Author = "anu_reviews" + Author = "jia.anu" }, new() { Content = "Jana Gana Mana is thought-provoking. Raises valid questions about justice and media.", @@ -91,7 +91,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("47435a1d-6b24-4cfc-b3a6-0be543322187"), Rating = 4, Title = "Relevant and Bold", - Author = "truthseeker" + Author = "naina.anu" }, new() { Content = "Powerful script and solid performances. Worth watching more than once.", @@ -99,7 +99,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("47435a1d-6b24-4cfc-b3a6-0be543322187"), Rating = 5, Title = "Must Watch", - Author = "prithvi_rules" + Author = "jia.anu" }, new() { Content = "Ustad Hotel remains a comfort movie. It warms the soul and stirs the appetite.", @@ -107,7 +107,7 @@ public static class MalayalamReviewsSeed MovieId = Guid.Parse("6191e634-14c8-45d1-898f-191060cdbec1"), Rating = 5, Title = "Feel-Good Watch", - Author = "greenchili" + Author = "naina.anu" }, new() { Content = "Malik's political layers and gripping visuals make it a standout Malayalam film.", diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Controllers/UserReviewsController.cs b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Controllers/UserReviewsController.cs index 422f131b..8e332f95 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Controllers/UserReviewsController.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Controllers/UserReviewsController.cs @@ -29,8 +29,9 @@ public ActionResult GetReviewsForMovie(Guid movieId) return default!; } - - public async Task GetRecentReviewsForUsers(GetRecentReviewsForUsersRequest request) + [HttpPost] + [Route(@"GetRecentReviewsForUsers")] + public async Task GetRecentReviewsForUsers([FromBody]GetRecentReviewsForUsersRequest request) { try { @@ -52,7 +53,7 @@ public async Task GetRecentReviewsForUsers(Get MovieId = r.MovieId, Content = r.Content, Rating = r.Rating, - UserName = r.UserName, + UserName = r.Author, }).ToList() }; } diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Helpers/IServiceCollectionExtension.cs b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Helpers/IServiceCollectionExtension.cs index c4b1d9cb..587dff07 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Helpers/IServiceCollectionExtension.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Helpers/IServiceCollectionExtension.cs @@ -1,4 +1,11 @@ -using ReviewService.Application.Interfaces.Operations; +using MediatR; +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using ReviewService.Application.Interfaces.Operations; +using ReviewService.Application.Interfaces.Services; +using ReviewService.Domain.Entities; +using ReviewService.Presenation.Api.Options; +using StackExchange.Redis; namespace ReviewService.Presenation.Api.Helpers; @@ -6,16 +13,72 @@ public static class IServiceCollectionExtension { public static void RegisterServices(this IServiceCollection serviceCollection) { + + // Register your services here // Example: serviceCollection.AddSingleton(); + serviceCollection.AddAutoMapper(typeof(IServiceCollectionExtension)); + serviceCollection.AddMediatR(typeof(ReviewService.Application.Orchestration.Commands.CreateReviewCommand).Assembly); + + serviceCollection.AddSingleton(sp => + { + var dbOptions = sp.GetRequiredService>().Value; + var connectionString = dbOptions.ConnectionString; + return new MongoClient(connectionString); + }); + + serviceCollection.AddSingleton(sp => + { + var dbOptions = sp.GetRequiredService>().Value; + var client = sp.GetRequiredService(); + var databaseName = dbOptions.DatabaseName; + return client.GetDatabase(databaseName); + }); + + RegisterRepositories(serviceCollection); + // User Services + serviceCollection.AddScoped(); + + // Generic Services + serviceCollection.AddSingleton(sp => + { + var connectionMultiplexer = sp.GetRequiredService(); + var cacheOptions = sp.GetRequiredService>().Value; + return new ReviewService.Application.Services.Services.CachingService(connectionMultiplexer, cacheOptions.ExpirationInMinutes); + }); + + serviceCollection.AddSingleton(); + // Register initializers and providers RegisterInitializersAndProviders(serviceCollection); } + + private static void RegisterRepositories(IServiceCollection serviceCollection) + { + // Register your repositories here + // Example: serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + } private static void RegisterInitializersAndProviders(IServiceCollection serviceCollection) { // Add any initializers or providers needed for the application // Example: serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(sp => + { + var cacheOptions = sp.GetRequiredService>().Value; + if (cacheOptions.ConnectionString is null) + { + throw new InvalidOperationException("Redis connection string is not configured."); + } + + var options = ConfigurationOptions.Parse(cacheOptions.ConnectionString); + options.AbortOnConnectFail = false; + options.ConnectRetry = 5; + options.ConnectTimeout = 10000; + options.SyncTimeout = 10000; + return ConnectionMultiplexer.Connect(options); + }); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Maps/ProfileMap.cs b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Maps/ProfileMap.cs new file mode 100644 index 00000000..e1a6310d --- /dev/null +++ b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Maps/ProfileMap.cs @@ -0,0 +1,30 @@ +using AutoMapper; + +namespace ReviewService.Presenation.Api.Maps; + +public class ProfileMap:Profile +{ + public ProfileMap() + { + // Define your mappings here + // Example: CreateMap(); + + CreateMap() + .ForMember(dest => dest.ID, opt => opt.MapFrom(src => src.Id.ToString())) + .ForMember(dest => dest.MovieId, opt => opt.MapFrom(src => src.MovieId.ToString())) + .ForMember(dest => dest.CreatedOn, opt => opt.MapFrom(src => src.CreatedOn)) + .ForMember(dest => dest.UpvotedBy, opt => opt.MapFrom(src => src.UpvotedBy.Select(u => u.ToString()))) + .ForMember(dest => dest.DownvotedBy, opt => opt.MapFrom(src => src.DownvotedBy.Select(u => u.ToString()))) + .ReverseMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => Guid.Parse(src.ID))); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.MovieId, opt => opt.MapFrom(src => src.MovieId.ToString())) + .ForMember(dest => dest.CreatedOn, opt => opt.MapFrom(src => src.CreatedOn)) + .ForMember(dest => dest.UpvotedBy, opt => opt.MapFrom(src => src.UpvotedBy.Select(u => u.ToString()))) + .ForMember(dest => dest.DownvotedBy, opt => opt.MapFrom(src => src.DownvotedBy.Select(u => u.ToString()))) + .ReverseMap(); + + } +} diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Models/CreateReviewRequest.cs b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Models/CreateReviewRequest.cs index 4aeb03fc..e39ec21e 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Models/CreateReviewRequest.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Models/CreateReviewRequest.cs @@ -8,21 +8,5 @@ public record CreateReviewRequest public string UserName { get; set; } = string.Empty; } -public record GetRecentReviewsForUsersRequest(IEnumerable UserIds, int Count = 3); - -public record GetRecentReviewsForUsersResponse -{ - public IEnumerable Reviews { get; init; } = []; -}; -public record GetRcentReviewsForUserReviewItem -{ - public Guid ReviewId { get; init; } - public Guid MovieId { get; init; } - public string MovieTitle { get; init; } = string.Empty; - public string Content { get; init; } = string.Empty; - public int Rating { get; init; } - public string UserName { get; init; } = string.Empty; - public string UserDisplayName { get; set; } = string.Empty; -} diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Models/GetRecentReviewsForUsersRequest.cs b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Models/GetRecentReviewsForUsersRequest.cs new file mode 100644 index 00000000..3957c672 --- /dev/null +++ b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Models/GetRecentReviewsForUsersRequest.cs @@ -0,0 +1,3 @@ +namespace ReviewService.Presenation.Api.Models; + +public record GetRecentReviewsForUsersRequest(IEnumerable UserIds, int Count = 3); diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Models/GetRecentReviewsForUsersResponse.cs b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Models/GetRecentReviewsForUsersResponse.cs new file mode 100644 index 00000000..a495400a --- /dev/null +++ b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Models/GetRecentReviewsForUsersResponse.cs @@ -0,0 +1,16 @@ +namespace ReviewService.Presenation.Api.Models; + +public record GetRecentReviewsForUsersResponse +{ + public IEnumerable Reviews { get; init; } = []; +}; + +public record GetRcentReviewsForUserReviewItem +{ + public Guid ReviewId { get; init; } + public Guid MovieId { get; init; } + public string MovieTitle { get; init; } = string.Empty; + public string Content { get; init; } = string.Empty; + public int Rating { get; init; } + public string UserName { get; init; } = string.Empty; +} diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Options/CacheOptions.cs b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Options/CacheOptions.cs new file mode 100644 index 00000000..05eadc10 --- /dev/null +++ b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Options/CacheOptions.cs @@ -0,0 +1,9 @@ +namespace ReviewService.Presenation.Api.Options; + +public record CacheOptions +{ + public string? CacheName { get; init; } = "ReviewServiceCache"; // Default cache name + public int ExpirationInMinutes { get; init; } = 60; // Default expiration time in minutes + public bool EnableCaching { get; init; } = true; // Flag to enable or disable caching + public string? ConnectionString { get; init; } = null; // Optional connection string for distributed cache +} diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Program.cs b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Program.cs index 7d69f589..12f79df4 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Program.cs +++ b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/Program.cs @@ -1,6 +1,9 @@ +using Microsoft.Extensions.Options; +using MongoDB.Driver; using ReviewService.Presenation.Api; using ReviewService.Presenation.Api.Helpers; using ReviewService.Presenation.Api.Options; +using StackExchange.Redis; namespace ReviewService.Api; @@ -11,14 +14,15 @@ public static async Task Main(string[] args) var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); builder.Services.Configure(builder.Configuration.GetSection(nameof(DatabaseOptions))); - - + builder.Services.Configure(builder.Configuration.GetSection(nameof(CacheOptions))); + // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); builder.Services.RegisterServices(); - + builder.Services.AddEndpointsApiExplorer(); // for minimal APIs + builder.Services.AddSwaggerGen(); // generates the Swagger JSON var app = builder.Build(); app.MapDefaultEndpoints(); @@ -36,7 +40,8 @@ public static async Task Main(string[] args) // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - app.MapOpenApi(); + app.UseSwagger(); // serves /swagger/v1/swagger.json + app.UseSwaggerUI(); // serves /swagger } app.UseHttpsRedirection(); diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/ReviewService.Presenation.Api.csproj b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/ReviewService.Presenation.Api.csproj index 8f686d5a..b7af8bd9 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/ReviewService.Presenation.Api.csproj +++ b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/ReviewService.Presenation.Api.csproj @@ -11,18 +11,21 @@ + + + diff --git a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/appsettings.json b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/appsettings.json index aa4a93c6..454469a6 100644 --- a/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/appsettings.json +++ b/server/nt.microservice/services/ReviewService/ReviewService.Presentation.Api/appsettings.json @@ -11,5 +11,11 @@ "ConnectionString": "mongodb://root:mypass@nt.reviewservice.db:27018/?authSource=admin", "DatabaseName": "ntreviewstore", "ReviewCollectionName": "reviews" + }, + "CacheOptions": { + "ConnectionString": "localhost:6379,password=Password123,abortConnect=false", + "InstanceName": "nt-reviewservice-cache", + "ExpirationInMinutes": 60, + "EnableCaching": true } }