From b66bb911f10f3862736813e862225cb1a49d551d Mon Sep 17 00:00:00 2001 From: Maxime Adler Date: Fri, 18 Apr 2025 15:20:13 +0200 Subject: [PATCH] Add search for games and orders --- .../Pages/Games/Filter/GameFilter.razor.cs | 9 +- .../Controllers/GameController.cs | 17 +-- .../Server/GameIdeas.WebAPI/Program.cs | 6 +- .../{ => Categories}/CategoryService.cs | 2 +- .../ICategoryService.cs | 0 .../Services/Games/GameReadService.cs | 100 ++++++++++++++++++ .../GameWriteService.cs} | 53 +--------- .../Services/Games/IGameReadService.cs | 9 ++ .../IGameWriteService.cs} | 6 +- 9 files changed, 132 insertions(+), 70 deletions(-) rename src/GameIdeas/Server/GameIdeas.WebAPI/Services/{ => Categories}/CategoryService.cs (95%) rename src/GameIdeas/Server/GameIdeas.WebAPI/Services/{Interfaces => Categories}/ICategoryService.cs (100%) create mode 100644 src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/GameReadService.cs rename src/GameIdeas/Server/GameIdeas.WebAPI/Services/{GameService.cs => Games/GameWriteService.cs} (70%) create mode 100644 src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/IGameReadService.cs rename src/GameIdeas/Server/GameIdeas.WebAPI/Services/{Interfaces/IGameService.cs => Games/IGameWriteService.cs} (50%) diff --git a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Filter/GameFilter.razor.cs b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Filter/GameFilter.razor.cs index 749f0f5..21b236f 100644 --- a/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Filter/GameFilter.razor.cs +++ b/src/GameIdeas/Client/GameIdeas.BlazorApp/Pages/Games/Filter/GameFilter.razor.cs @@ -4,7 +4,6 @@ using GameIdeas.BlazorApp.Shared.Models; using GameIdeas.Shared.Dto; using GameIdeas.Shared.Enum; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Forms; namespace GameIdeas.BlazorApp.Pages.Games.Filter; @@ -23,10 +22,10 @@ public partial class GameFilter ]; private readonly List GameProperties = [ - new() { PropertyName = nameof(GameDetailDto.Title), Label = "Titre" }, - new() { PropertyName = nameof(GameDetailDto.ReleaseDate), Label = "Date de parution" }, - new() { PropertyName = nameof(GameDetailDto.StorageSpace), Label = "Espace de stockage" }, - new() { PropertyName = nameof(GameDetailDto.Interest), Label = "Interêt" } + new() { PropertyName = nameof(GameIdeas.Shared.Model.Game.Title), Label = "Titre" }, + new() { PropertyName = nameof(GameIdeas.Shared.Model.Game.ReleaseDate), Label = "Date de parution" }, + new() { PropertyName = nameof(GameIdeas.Shared.Model.Game.StorageSpace), Label = "Espace de stockage" }, + new() { PropertyName = nameof(GameIdeas.Shared.Model.Game.Interest), Label = "Interêt" } ]; private SelectParams SelectParams = new(); diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Controllers/GameController.cs b/src/GameIdeas/Server/GameIdeas.WebAPI/Controllers/GameController.cs index fd0e9a6..9b0e15f 100644 --- a/src/GameIdeas/Server/GameIdeas.WebAPI/Controllers/GameController.cs +++ b/src/GameIdeas/Server/GameIdeas.WebAPI/Controllers/GameController.cs @@ -1,5 +1,5 @@ using GameIdeas.Shared.Dto; -using GameIdeas.WebAPI.Services.Interfaces; +using GameIdeas.WebAPI.Services.Games; using Microsoft.AspNetCore.Mvc; namespace GameIdeas.WebAPI.Controllers; @@ -7,7 +7,10 @@ namespace GameIdeas.WebAPI.Controllers; [ApiController] [Route("api/[controller]")] -public class GameController(IGameService gameService, ILoggerFactory loggerFactory) : Controller +public class GameController( + IGameReadService gameReadService, + IGameWriteService gameWriteService, + ILoggerFactory loggerFactory) : Controller { private readonly ILogger logger = loggerFactory.CreateLogger(); @@ -16,7 +19,7 @@ public class GameController(IGameService gameService, ILoggerFactory loggerFacto { try { - return Ok(await gameService.GetGames(filter)); + return Ok(await gameReadService.GetGames(filter)); } catch (Exception e) { @@ -30,7 +33,7 @@ public class GameController(IGameService gameService, ILoggerFactory loggerFacto { try { - return Ok(await gameService.GetGameById(id)); + return Ok(await gameReadService.GetGameById(id)); } catch (Exception e) { @@ -44,7 +47,7 @@ public class GameController(IGameService gameService, ILoggerFactory loggerFacto { try { - var gameResult = await gameService.CreateGame(game); + var gameResult = await gameWriteService.CreateGame(game); return Created("/Create", gameResult.Id); } catch (Exception e) @@ -59,7 +62,7 @@ public class GameController(IGameService gameService, ILoggerFactory loggerFacto { try { - var gameResult = await gameService.UpdateGame(game); + var gameResult = await gameWriteService.UpdateGame(game); return Created($"/Update", gameResult.Id); } catch (Exception e) @@ -74,7 +77,7 @@ public class GameController(IGameService gameService, ILoggerFactory loggerFacto { try { - return Ok(await gameService.DeleteGame(id)); + return Ok(await gameWriteService.DeleteGame(id)); } catch (Exception e) { diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Program.cs b/src/GameIdeas/Server/GameIdeas.WebAPI/Program.cs index e78e427..6cfe582 100644 --- a/src/GameIdeas/Server/GameIdeas.WebAPI/Program.cs +++ b/src/GameIdeas/Server/GameIdeas.WebAPI/Program.cs @@ -1,7 +1,8 @@ using GameIdeas.Resources; using GameIdeas.WebAPI.Context; using GameIdeas.WebAPI.Profiles; -using GameIdeas.WebAPI.Services; +using GameIdeas.WebAPI.Services.Categories; +using GameIdeas.WebAPI.Services.Games; using GameIdeas.WebAPI.Services.Interfaces; using Microsoft.EntityFrameworkCore; using System.Text.Json.Serialization; @@ -32,7 +33,8 @@ services.AddDbContext(dbContextOptions); services.AddSingleton(); services.AddSingleton(); -services.AddScoped(); +services.AddScoped(); +services.AddScoped(); services.AddScoped(); services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/CategoryService.cs b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Categories/CategoryService.cs similarity index 95% rename from src/GameIdeas/Server/GameIdeas.WebAPI/Services/CategoryService.cs rename to src/GameIdeas/Server/GameIdeas.WebAPI/Services/Categories/CategoryService.cs index f3650a1..4d7c05d 100644 --- a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/CategoryService.cs +++ b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Categories/CategoryService.cs @@ -4,7 +4,7 @@ using GameIdeas.WebAPI.Context; using GameIdeas.WebAPI.Services.Interfaces; using Microsoft.EntityFrameworkCore; -namespace GameIdeas.WebAPI.Services; +namespace GameIdeas.WebAPI.Services.Categories; public class CategoryService(GameIdeasContext context, IMapper mapper) : ICategoryService { diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Interfaces/ICategoryService.cs b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Categories/ICategoryService.cs similarity index 100% rename from src/GameIdeas/Server/GameIdeas.WebAPI/Services/Interfaces/ICategoryService.cs rename to src/GameIdeas/Server/GameIdeas.WebAPI/Services/Categories/ICategoryService.cs diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/GameReadService.cs b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/GameReadService.cs new file mode 100644 index 0000000..7e7e77a --- /dev/null +++ b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/GameReadService.cs @@ -0,0 +1,100 @@ +using AutoMapper; +using GameIdeas.Shared.Constants; +using GameIdeas.Shared.Dto; +using GameIdeas.Shared.Exceptions; +using GameIdeas.Shared.Model; +using GameIdeas.WebAPI.Context; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace GameIdeas.WebAPI.Services.Games; + +public class GameReadService(GameIdeasContext context, IMapper mapper) : IGameReadService +{ + public async Task> GetGames(GameFilterDto filter) + { + var query = context.Games + .Include(g => g.GamePlatforms).ThenInclude(gp => gp.Platform) + .Include(g => g.GameProperties) + .Include(g => g.GameTags).ThenInclude(gt => gt.Tag) + .Include(g => g.GamePublishers) + .Include(g => g.GameDevelopers) + .AsQueryable(); + + ApplyFilter(ref query, filter); + + ApplyOrder(ref query, filter); + + var games = await query.Skip((filter.CurrentPage - 1) * GlobalConstants.NUMBER_PER_PAGE) + .Take(GlobalConstants.NUMBER_PER_PAGE) + .ToListAsync(); + + return mapper.Map>(games); + } + + public async Task GetGameById(int gameId) + { + var game = await context.Games + .Include(g => g.CreationUser) + .Include(g => g.ModificationUser) + .Include(g => g.GamePlatforms).ThenInclude(p => p.Platform) + .Include(g => g.GameProperties).ThenInclude(p => p.Property) + .Include(g => g.GameTags).ThenInclude(p => p.Tag) + .Include(g => g.GamePublishers).ThenInclude(p => p.Publisher) + .Include(g => g.GameDevelopers).ThenInclude(p => p.Developer) + .FirstOrDefaultAsync(g => g.Id == gameId); + + return game == null + ? throw new NotFoundException($"[{typeof(Game).FullName}] with ID {gameId} has not been found in context") + : mapper.Map(game); + } + + private static void ApplyOrder(ref IQueryable query, GameFilterDto filter) + { + if (filter.Title != null) + { + return; + } + + if (filter.SortType != null && filter.SortPropertyName != null) + { + var param = Expression.Parameter(typeof(Game), "x"); + Expression propertyAccess = Expression.PropertyOrField(param, filter.SortPropertyName); + var converted = Expression.Convert(propertyAccess, typeof(object)); + var lambda = Expression.Lambda>(converted, param); + + if (filter.SortType == Shared.Enum.SortType.Ascending) + { + query = query.OrderBy(lambda.Compile()).AsQueryable(); + } + else + { + query = query.OrderByDescending(lambda.Compile()).AsQueryable(); + } + } + } + + private static void ApplyFilter(ref IQueryable query, GameFilterDto filter) + { + if (filter.Title != null) + { + var keywords = filter.Title + .Split([' '], StringSplitOptions.RemoveEmptyEntries) + .Select(k => k.Trim()) + .ToArray(); + + query = query + .Where(game => keywords.All( + kw => game.Title.IndexOf(kw, StringComparison.OrdinalIgnoreCase) >= 0 + )) + .OrderBy(game => + keywords.Min(kw => + game.Title.IndexOf(kw, StringComparison.OrdinalIgnoreCase) + ) + ) + .ThenBy(game => game.Title.Length); + + return; + } + } +} diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/GameService.cs b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/GameWriteService.cs similarity index 70% rename from src/GameIdeas/Server/GameIdeas.WebAPI/Services/GameService.cs rename to src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/GameWriteService.cs index b19d43b..b1b05b1 100644 --- a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/GameService.cs +++ b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/GameWriteService.cs @@ -4,51 +4,12 @@ using GameIdeas.Shared.Dto; using GameIdeas.Shared.Exceptions; using GameIdeas.Shared.Model; using GameIdeas.WebAPI.Context; -using GameIdeas.WebAPI.Services.Interfaces; using Microsoft.EntityFrameworkCore; -namespace GameIdeas.WebAPI.Services; +namespace GameIdeas.WebAPI.Services.Games; -public class GameService(GameIdeasContext context, IMapper mapper) : IGameService +public class GameWriteService(GameIdeasContext context, IMapper mapper) : IGameWriteService { - public async Task> GetGames(GameFilterDto filter) - { - var query = context.Games - .Include(g => g.GamePlatforms).ThenInclude(gp => gp.Platform) - .Include(g => g.GameProperties) - .Include(g => g.GameTags).ThenInclude(gt => gt.Tag) - .Include(g => g.GamePublishers) - .Include(g => g.GameDevelopers) - .AsQueryable(); - - ApplyFilter(ref query, filter); - - ApplyOrder(ref query, filter); - - var games = await query.Skip((filter.CurrentPage - 1) * GlobalConstants.NUMBER_PER_PAGE) - .Take(GlobalConstants.NUMBER_PER_PAGE) - .ToListAsync(); - - return mapper.Map>(games); - } - - public async Task GetGameById(int gameId) - { - var game = await context.Games - .Include(g => g.CreationUser) - .Include(g => g.ModificationUser) - .Include(g => g.GamePlatforms).ThenInclude(p => p.Platform) - .Include(g => g.GameProperties).ThenInclude(p => p.Property) - .Include(g => g.GameTags).ThenInclude(p => p.Tag) - .Include(g => g.GamePublishers).ThenInclude(p => p.Publisher) - .Include(g => g.GameDevelopers).ThenInclude(p => p.Developer) - .FirstOrDefaultAsync(g => g.Id == gameId); - - return game == null - ? throw new NotFoundException($"[{typeof(Game).FullName}] with ID {gameId} has not been found in context") - : mapper.Map(game); - } - public async Task CreateGame(GameDetailDto gameDto) { var gameToCreate = mapper.Map(gameDto); @@ -98,16 +59,6 @@ public class GameService(GameIdeasContext context, IMapper mapper) : IGameServic return await context.SaveChangesAsync() != 0; } - private void ApplyOrder(ref IQueryable query, GameFilterDto filter) - { - - } - - private void ApplyFilter(ref IQueryable query, GameFilterDto filter) - { - - } - private async Task HandlePlatformsCreation(IEnumerable? categoriesToCreate, int gameId) { if (categoriesToCreate != null) diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/IGameReadService.cs b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/IGameReadService.cs new file mode 100644 index 0000000..4c814d3 --- /dev/null +++ b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/IGameReadService.cs @@ -0,0 +1,9 @@ +using GameIdeas.Shared.Dto; + +namespace GameIdeas.WebAPI.Services.Games; + +public interface IGameReadService +{ + Task> GetGames(GameFilterDto filter); + Task GetGameById(int gameId); +} diff --git a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Interfaces/IGameService.cs b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/IGameWriteService.cs similarity index 50% rename from src/GameIdeas/Server/GameIdeas.WebAPI/Services/Interfaces/IGameService.cs rename to src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/IGameWriteService.cs index ff2749d..d7fcc92 100644 --- a/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Interfaces/IGameService.cs +++ b/src/GameIdeas/Server/GameIdeas.WebAPI/Services/Games/IGameWriteService.cs @@ -1,11 +1,9 @@ using GameIdeas.Shared.Dto; -namespace GameIdeas.WebAPI.Services.Interfaces; +namespace GameIdeas.WebAPI.Services.Games; -public interface IGameService +public interface IGameWriteService { - Task> GetGames(GameFilterDto filter); - Task GetGameById(int gameId); Task CreateGame(GameDetailDto gameDto); Task UpdateGame(GameDetailDto gameDto); Task DeleteGame(int gameId);