From a687c6153cc983f7b33b4075223d1eee7ee83344 Mon Sep 17 00:00:00 2001 From: Marcin Bator Date: Wed, 25 Sep 2024 22:37:58 +0200 Subject: [PATCH 1/4] refactor: #23 game stats cache --- .../Config/ServiceRegistrationExtension.cs | 3 +- rag-2-backend/DTO/Stats/GameStatsResponse.cs | 1 + rag-2-backend/Services/StatsService.cs | 46 ++++++++++++++----- rag-2-backend/Test/StatsServiceTest.cs | 28 ++++++++++- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/rag-2-backend/Config/ServiceRegistrationExtension.cs b/rag-2-backend/Config/ServiceRegistrationExtension.cs index a233c93..a7bc634 100644 --- a/rag-2-backend/Config/ServiceRegistrationExtension.cs +++ b/rag-2-backend/Config/ServiceRegistrationExtension.cs @@ -43,8 +43,9 @@ private static void ConfigServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); + + services.AddSingleton(); } private static void ConfigSwagger(IServiceCollection services) diff --git a/rag-2-backend/DTO/Stats/GameStatsResponse.cs b/rag-2-backend/DTO/Stats/GameStatsResponse.cs index c1c9a37..882efdc 100644 --- a/rag-2-backend/DTO/Stats/GameStatsResponse.cs +++ b/rag-2-backend/DTO/Stats/GameStatsResponse.cs @@ -7,4 +7,5 @@ public class GameStatsResponse public double TotalStorageMb { get; set; } public DateTime FirstPlayed { get; init; } public DateTime LastPlayed { get; init; } + public DateTime StatsUpdatedDate { get; init; } } \ No newline at end of file diff --git a/rag-2-backend/Services/StatsService.cs b/rag-2-backend/Services/StatsService.cs index 2ea6beb..c06da33 100644 --- a/rag-2-backend/Services/StatsService.cs +++ b/rag-2-backend/Services/StatsService.cs @@ -5,22 +5,28 @@ using rag_2_backend.Config; using rag_2_backend.DTO.Stats; using rag_2_backend.Models; -using rag_2_backend.Utils; +using rag_2_backend.models.entity; #endregion namespace rag_2_backend.Services; -public class StatsService(DatabaseContext context, UserUtil userUtil) +public class StatsService(IServiceProvider serviceProvider) { + private readonly DatabaseContext _context = + serviceProvider.CreateScope().ServiceProvider.GetRequiredService(); + + private Dictionary CachedGameStats { get; } = new(); + public UserStatsResponse GetStatsForUser(string email, int userId) { - var user = userUtil.GetUserByEmailOrThrow(email); + var user = _context.Users.FirstOrDefault(u => u.Id == userId) + ?? throw new NotFoundException("User not found"); if (user.Email != email && user.Role == Role.Student) throw new ForbiddenException("Permission denied"); - var records = context.RecordedGames + var records = _context.RecordedGames .OrderBy(r => r.Started) .Where(r => r.User.Id == userId).Include(recordedGame => recordedGame.Game) .ToList(); @@ -37,26 +43,42 @@ public UserStatsResponse GetStatsForUser(string email, int userId) public GameStatsResponse GetStatsForGame(int gameId) { - var records = context.RecordedGames + var game = _context.Games.FirstOrDefault(g => g.Id == gameId) + ?? throw new NotFoundException("Game not found"); + + if (CachedGameStats.TryGetValue(game.Id, out var value) + && value.StatsUpdatedDate.AddDays(1) >= DateTime.Now) + return CachedGameStats[game.Id]; + + return UpdateCachedStats(gameId, game); + } + + // + + private GameStatsResponse UpdateCachedStats(int gameId, Game game) + { + var records = _context.RecordedGames .OrderBy(r => r.Started) - .Where(r => r.Game.Id == gameId).Include(recordedGame => recordedGame.User) + .Where(r => r.Game.Id == gameId) + .Include(recordedGame => recordedGame.User) .ToList(); - return new GameStatsResponse + CachedGameStats[game.Id] = new GameStatsResponse { FirstPlayed = records[0].Started, LastPlayed = records.Last().Ended, Plays = records.Count, TotalStorageMb = GetSizeByGame(gameId, 0), - TotalPlayers = records.Select(r => r.User.Id).Distinct().Count() + TotalPlayers = records.Select(r => r.User.Id).Distinct().Count(), + StatsUpdatedDate = DateTime.Now }; - } - // + return CachedGameStats[game.Id]; + } private double GetSizeByGame(int gameId, double initialSizeBytes) { - var results = context.RecordedGames + var results = _context.RecordedGames .Where(e => e.Game.Id == gameId) .Select(e => new { @@ -70,7 +92,7 @@ private double GetSizeByGame(int gameId, double initialSizeBytes) private double GetSizeByUser(int userId, double initialSizeBytes) { - var results = context.RecordedGames + var results = _context.RecordedGames .Where(e => e.User.Id == userId) .Select(e => new { diff --git a/rag-2-backend/Test/StatsServiceTest.cs b/rag-2-backend/Test/StatsServiceTest.cs index 475fccf..407af97 100644 --- a/rag-2-backend/Test/StatsServiceTest.cs +++ b/rag-2-backend/Test/StatsServiceTest.cs @@ -44,7 +44,10 @@ public class StatsServiceTests public StatsServiceTests() { _mockUserUtil = new Mock(_contextMock.Object); - _statsService = new StatsService(_contextMock.Object, _mockUserUtil.Object); + + var serviceProvider = MockServiceProvider(); + + _statsService = new StatsService(serviceProvider.Object); _contextMock.Setup(c => c.RecordedGames).Returns( _recordedGames.AsQueryable().BuildMockDbSet().Object @@ -91,4 +94,27 @@ public void ShouldReturnStatsForGame() Assert.Equal(1, result.Plays); Assert.Equal(1, result.TotalPlayers); } + + // + + private Mock MockServiceProvider() + { + var serviceProvider = new Mock(); + serviceProvider + .Setup(x => x.GetService(typeof(DatabaseContext))) + .Returns(_contextMock.Object); + + var serviceScope = new Mock(); + serviceScope.Setup(x => x.ServiceProvider).Returns(serviceProvider.Object); + + var serviceScopeFactory = new Mock(); + serviceScopeFactory + .Setup(x => x.CreateScope()) + .Returns(serviceScope.Object); + + serviceProvider + .Setup(x => x.GetService(typeof(IServiceScopeFactory))) + .Returns(serviceScopeFactory.Object); + return serviceProvider; + } } \ No newline at end of file From 3248b263cda86b4a00ca075dafdccf4ad83e3635 Mon Sep 17 00:00:00 2001 From: Marcin Bator Date: Sat, 28 Sep 2024 16:22:29 +0200 Subject: [PATCH 2/4] feat: #dev ERD and class diagrams --- .../GraphChartBuilderImpl@42101562.uml | 142 ++++++++++++++++++ rag-2-backend/public.uml | 49 ++++++ 2 files changed, 191 insertions(+) create mode 100644 rag-2-backend/GraphChartBuilderImpl@42101562.uml create mode 100644 rag-2-backend/public.uml diff --git a/rag-2-backend/GraphChartBuilderImpl@42101562.uml b/rag-2-backend/GraphChartBuilderImpl@42101562.uml new file mode 100644 index 0000000..5ba1d97 --- /dev/null +++ b/rag-2-backend/GraphChartBuilderImpl@42101562.uml @@ -0,0 +1,142 @@ + + + GraphChartAdapterForDiagramProvider + com.intellij.uml.v2.GraphChartBuilderImpl@42101562 + + RdDiagramClrNode (clrFullName = "rag_2_backend.Utils.EmailSendingUtil"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = Unknownicon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "EmailSendingUtil") + RdDiagramClrNode (clrFullName = "rag_2_backend.controllers.GameRecordController"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = LogicOwnericon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "GameRecordController") + RdDiagramClrNode (clrFullName = "rag_2_backend.controllers.GameController"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = LogicOwnericon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "GameController") + RdDiagramClrNode (clrFullName = "rag_2_backend.controllers.AdministrationController"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = LogicOwnericon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "AdministrationController") + RdDiagramClrNode (clrFullName = "rag_2_backend.Services.AdministrationService"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = LogicOwnericon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "AdministrationService") + RdDiagramClrNode (clrFullName = "rag_2_backend.Config.ExceptionHandlingMiddleware"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = Unknownicon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "ExceptionHandlingMiddleware") + RdDiagramClrNode (clrFullName = "rag_2_backend.Config.DatabaseContext"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = Unknownicon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "DatabaseContext") + RdDiagramClrNode (clrFullName = "rag_2_backend.Utils.UserUtil"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = Unknownicon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "UserUtil") + RdDiagramClrNode (clrFullName = "rag_2_backend.Services.GameService"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = LogicOwnericon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "GameService") + RdDiagramClrNode (clrFullName = "rag_2_backend.Services.GameRecordService"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = LogicOwnericon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "GameRecordService") + RdDiagramClrNode (clrFullName = "rag_2_backend.Utils.JwtUtil"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = Unknownicon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "JwtUtil") + RdDiagramClrNode (clrFullName = "rag_2_backend.Services.StatsService"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = LogicOwnericon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "StatsService") + RdDiagramClrNode (clrFullName = "rag_2_backend.controllers.UserController"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = LogicOwnericon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "UserController") + RdDiagramClrNode (clrFullName = "rag_2_backend.Services.EmailService"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = LogicOwnericon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "EmailService") + RdDiagramClrNode (clrFullName = "rag_2_backend.controllers.StatsController"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = LogicOwnericon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "StatsController") + RdDiagramClrNode (clrFullName = "rag_2_backend.Services.UserService"psiModuleId = "25fc3b7d-db69-451a-bae0-781ac1694a3c(rag-2-backend)-13[net8.0]"kind = LogicOwnericon = ImageSourceIconModel (iconPackStringId = "PsiSymbols"iconNameStringId = "Class")name = "UserService") + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rag-2-backend/public.uml b/rag-2-backend/public.uml new file mode 100644 index 0000000..4a37327 --- /dev/null +++ b/rag-2-backend/public.uml @@ -0,0 +1,49 @@ + + + DATABASE + fa1a1854-887a-48ed-9eec-82dbd016d9ae.SCHEMA:postgres.public + + fa1a1854-887a-48ed-9eec-82dbd016d9ae.TABLE:postgres.public.recorded_game + fa1a1854-887a-48ed-9eec-82dbd016d9ae.TABLE:postgres.public.password_reset_token + fa1a1854-887a-48ed-9eec-82dbd016d9ae.TABLE:postgres.public.game_table + fa1a1854-887a-48ed-9eec-82dbd016d9ae.TABLE:postgres.public.account_confirmation_token + fa1a1854-887a-48ed-9eec-82dbd016d9ae.TABLE:postgres.public.user_table + fa1a1854-887a-48ed-9eec-82dbd016d9ae.TABLE:postgres.public.blacklisted_jwt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Columns + Comments + Key columns + Virtual foreign keys + + + From f26ad166a279234188fbb865598dbeb77e09c413 Mon Sep 17 00:00:00 2001 From: Marcin Bator Date: Wed, 9 Oct 2024 10:37:41 +0200 Subject: [PATCH 3/4] Dockerfile --- rag-2-backend/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rag-2-backend/Dockerfile b/rag-2-backend/Dockerfile index 1087799..398b1c9 100644 --- a/rag-2-backend/Dockerfile +++ b/rag-2-backend/Dockerfile @@ -6,9 +6,9 @@ EXPOSE 8081 FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src -COPY ["rag-2-backend/rag-2-backend/rag-2-backend.csproj", "./"] +COPY ["rag-2-backend/rag-2-backend.csproj", "./"] RUN dotnet restore "rag-2-backend.csproj" -COPY ["rag-2-backend/rag-2-backend/", "."] +COPY ["rag-2-backend/", "."] WORKDIR "/src/" RUN dotnet build "rag-2-backend.csproj" -c --no-launch-profile -o /app/build From e6d8a0efa6e78267f7a6eed1a035cf5bacab4ca0 Mon Sep 17 00:00:00 2001 From: Marcin Bator Date: Tue, 15 Oct 2024 10:59:24 +0200 Subject: [PATCH 4/4] fix: #25 endpoints and stats fix --- rag-2-backend/Controllers/GameRecordController.cs | 8 ++++++-- rag-2-backend/Services/GameRecordService.cs | 4 ++-- rag-2-backend/Test/GameRecordServiceTest.cs | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/rag-2-backend/Controllers/GameRecordController.cs b/rag-2-backend/Controllers/GameRecordController.cs index 13957f3..4ab3f21 100644 --- a/rag-2-backend/Controllers/GameRecordController.cs +++ b/rag-2-backend/Controllers/GameRecordController.cs @@ -15,18 +15,22 @@ namespace rag_2_backend.controllers; [Route("api/[controller]")] public class GameRecordController(GameRecordService gameRecordService) : ControllerBase { - /// Get all recorded games for user by game ID (Auth) + /// Get all recorded games for user by game ID and user (Auth) /// User or game not found [HttpGet] + [Authorize] public List GetRecordsByGame([Required] int gameId) { - return gameRecordService.GetRecordsByGame(gameId); + var email = UserUtil.GetPrincipalEmail(User); + + return gameRecordService.GetRecordsByGameAndUser(gameId, email); } /// Download JSON file from specific game, admin and teacher can download everyone's data (Auth) /// User or game record not found /// Permission denied [HttpGet("{recordedGameId:int}")] + [Authorize] public FileContentResult DownloadRecordData([Required] int recordedGameId) { var email = UserUtil.GetPrincipalEmail(User); diff --git a/rag-2-backend/Services/GameRecordService.cs b/rag-2-backend/Services/GameRecordService.cs index 46e3c87..4b344f6 100644 --- a/rag-2-backend/Services/GameRecordService.cs +++ b/rag-2-backend/Services/GameRecordService.cs @@ -18,12 +18,12 @@ namespace rag_2_backend.Services; public class GameRecordService(DatabaseContext context, IConfiguration configuration, UserUtil userUtil) { - public List GetRecordsByGame(int gameId) + public List GetRecordsByGameAndUser(int gameId, string email) { return context.RecordedGames .Include(r => r.Game) .Include(r => r.User) - .Where(r => r.Game.Id == gameId) + .Where(r => r.Game.Id == gameId && r.User.Email == email) .ToList() .Select(RecordedGameMapper.Map) .ToList(); diff --git a/rag-2-backend/Test/GameRecordServiceTest.cs b/rag-2-backend/Test/GameRecordServiceTest.cs index 114f4f5..d70749f 100644 --- a/rag-2-backend/Test/GameRecordServiceTest.cs +++ b/rag-2-backend/Test/GameRecordServiceTest.cs @@ -84,7 +84,7 @@ public GameRecordServiceTest() [Fact] public void GetRecordsByGameTest() { - var actualRecords = _gameRecordService.GetRecordsByGame(1); + var actualRecords = _gameRecordService.GetRecordsByGameAndUser(1, "email@prz.edu.pl"); List expectedRecords = [ new()