Add services and controllers for games (#12)

Co-authored-by: Maxime Adler <madler@sqli.com>
Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
2025-04-12 21:34:45 +02:00
parent 3537465588
commit 3ea96186e7
32 changed files with 531 additions and 23 deletions

View File

@@ -7,7 +7,10 @@ public class GameIdeasContext : DbContext
{
public GameIdeasContext(DbContextOptions<GameIdeasContext> option)
: base(option)
{ }
{
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
AppContext.SetSwitch("Npgsql.DisableDateTimeInfinityConversions", true);
}
public virtual DbSet<User> Users { get; set; } = null!;
public virtual DbSet<Developer> Developers { get; set; } = null!;

View File

@@ -0,0 +1,83 @@
using GameIdeas.Shared.Dto;
using GameIdeas.WebAPI.Services;
using Microsoft.AspNetCore.Mvc;
namespace GameIdeas.WebAPI.Controllers;
[ApiController]
[Route("api/[controller]")]
public class GameController(GameService gameService, ILoggerFactory loggerFactory) : Controller
{
private readonly ILogger<GameController> logger = loggerFactory.CreateLogger<GameController>();
[HttpGet]
public async Task<ActionResult<IEnumerable<GameDto>>> SearchGames([FromQuery] PaggingDto pagging)
{
try
{
return Ok(await gameService.GetGames(pagging));
}
catch (Exception e)
{
logger.LogError(e, "Internal error while search games");
return StatusCode(500, e.Message);
}
}
[HttpGet("{id:int}")]
public async Task<ActionResult<GameDto>> GetGameById(int id)
{
try
{
return Ok(await gameService.GetGameById(id));
}
catch (Exception e)
{
logger.LogError(e, "Internal error while get game with id {id}", id);
return StatusCode(500, e.Message);
}
}
[HttpPost("Create")]
public async Task<ActionResult<GameDto>> CreateGame([FromBody] GameDto game)
{
try
{
return Created("/Create", await gameService.CreateGame(game));
}
catch (Exception e)
{
logger.LogError(e, "Internal error while create game");
return StatusCode(500, e.Message);
}
}
[HttpPut("Update")]
public async Task<ActionResult<GameDto>> UpdateGame([FromBody] GameDto game)
{
try
{
return Created($"/Update", await gameService.UpdateGame(game));
}
catch (Exception e)
{
logger.LogError(e, "Internal error while update game");
return StatusCode(500, e.Message);
}
}
[HttpDelete("Delete/{id:int}")]
public async Task<ActionResult<bool>> DeleteGame(int id)
{
try
{
return Ok(await gameService.DeleteGame(id));
}
catch (Exception e)
{
logger.LogError(e, "Internal error while delete game");
return StatusCode(500, e.Message);
}
}
}

View File

@@ -11,6 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4">

View File

@@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace GameIdeas.WebAPI.Migrations
{
[DbContext(typeof(GameIdeasContext))]
[Migration("20250409210640_InitialCreate")]
[Migration("20250409225125_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
@@ -176,7 +176,7 @@ namespace GameIdeas.WebAPI.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Libelle")
b.Property<string>("Label")
.IsRequired()
.HasColumnType("text");

View File

@@ -31,7 +31,7 @@ namespace GameIdeas.WebAPI.Migrations
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Libelle = table.Column<string>(type: "text", nullable: false)
Label = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{

View File

@@ -173,7 +173,7 @@ namespace GameIdeas.WebAPI.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Libelle")
b.Property<string>("Label")
.IsRequired()
.HasColumnType("text");

View File

@@ -0,0 +1,83 @@
using AutoMapper;
using GameIdeas.Shared.Dto;
using GameIdeas.Shared.Model;
namespace GameIdeas.WebAPI.Profiles;
public class CategoryProfile : Profile
{
public CategoryProfile()
{
CreatePlaformMap();
CreatePropertyMap();
CreateTagMap();
CreateDeveloperMap();
CreatePublisherMap();
}
private void CreatePublisherMap()
{
CreateMap<Publisher, PublisherDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Name, o => o.MapFrom(s => s.Name))
.ReverseMap();
CreateMap<PublisherDto, GamePublisher>()
.ForMember(d => d.PublisherId, o => o.MapFrom(s => s.Id))
.ForPath(d => d.Publisher.Id, o => o.MapFrom(s => s.Id))
.ForPath(d => d.Publisher.Name, o => o.MapFrom(s => s.Name));
}
private void CreateDeveloperMap()
{
CreateMap<Developer, DeveloperDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Name, o => o.MapFrom(s => s.Name))
.ReverseMap();
CreateMap<DeveloperDto, GameDeveloper>()
.ForMember(d => d.DeveloperId, o => o.MapFrom(s => s.Id))
.ForPath(d => d.Developer.Id, o => o.MapFrom(s => s.Id))
.ForPath(d => d.Developer.Name, o => o.MapFrom(s => s.Name));
}
private void CreateTagMap()
{
CreateMap<Tag, TagDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Label, o => o.MapFrom(s => s.Label))
.ReverseMap();
CreateMap<TagDto, GameTag>()
.ForMember(d => d.TagId, o => o.MapFrom(s => s.Id))
.ForPath(d => d.Tag.Id, o => o.MapFrom(s => s.Id))
.ForPath(d => d.Tag.Label, o => o.MapFrom(s => s.Label));
}
private void CreatePropertyMap()
{
CreateMap<Property, PropertyDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Label, o => o.MapFrom(s => s.Label))
.ReverseMap();
CreateMap<PropertyDto, GameProperty>()
.ForMember(d => d.PropertyId, o => o.MapFrom(s => s.Id))
.ForPath(d => d.Property.Id, o => o.MapFrom(s => s.Id))
.ForPath(d => d.Property.Label, o => o.MapFrom(s => s.Label));
}
private void CreatePlaformMap()
{
CreateMap<Platform, PlatformDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Label, o => o.MapFrom(s => s.Label))
.ReverseMap();
CreateMap<PlatformDto, GamePlatform>()
.ForMember(d => d.PlatformId, o => o.MapFrom(s => s.Id))
.ForPath(d => d.Platform.Id, o => o.MapFrom(s => s.Id))
.ForPath(d => d.Platform.Label, o => o.MapFrom(s => s.Label))
.ForMember(d => d.Url, o => o.MapFrom(s => s.Url));
}
}

View File

@@ -0,0 +1,31 @@
using AutoMapper;
using GameIdeas.Shared.Dto;
using GameIdeas.Shared.Model;
namespace GameIdeas.WebAPI.Profiles;
public class GameProfile : Profile
{
public GameProfile()
{
CreateMap<Game, GameDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Title, o => o.MapFrom(s => s.Title))
.ForMember(d => d.ReleaseDate, o => o.MapFrom(s => s.ReleaseDate))
.ForMember(d => d.CreationDate, o => o.MapFrom(s => s.CreationDate))
.ForMember(d => d.CreationUserId, o => o.MapFrom(s => s.CreationUserId))
.ForMember(d => d.CreationUser, o => o.MapFrom(s => s.CreationUser))
.ForMember(d => d.ModificationDate, o => o.MapFrom(s => s.ModificationDate))
.ForMember(d => d.ModificationUserId, o => o.MapFrom(s => s.ModificationUserId))
.ForMember(d => d.ModificationUser, o => o.MapFrom(s => s.ModificationUser))
.ForMember(d => d.StorageSpace, o => o.MapFrom(s => s.StorageSpace))
.ForMember(d => d.Description, o => o.MapFrom(s => s.Description))
.ForMember(d => d.Interest, o => o.MapFrom(s => s.Interest))
.ForMember(d => d.Platforms, o => o.MapFrom(s => s.GamePlatforms.Select(p => new PlatformDto() { Id = p.Platform.Id, Label = p.Platform.Label, Url = p.Url })))
.ForMember(d => d.Properties, o => o.MapFrom(s => s.GameProperties.Select(p => p.Property)))
.ForMember(d => d.Tags, o => o.MapFrom(s => s.GameTags.Select(t => t.Tag)))
.ForMember(d => d.Publishers, o => o.MapFrom(s => s.GamePublishers.Select(p => p.Publisher)))
.ForMember(d => d.Developers, o => o.MapFrom(s => s.GameDevelopers.Select(gd => gd.Developer)))
.ReverseMap();
}
}

View File

@@ -0,0 +1,20 @@
using AutoMapper;
using GameIdeas.Shared.Dto;
using GameIdeas.Shared.Enum;
using GameIdeas.Shared.Model;
namespace GameIdeas.WebAPI.Profiles;
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Username, o => o.MapFrom(s => s.Username))
.ForMember(d => d.Password, o => o.MapFrom(s => s.Password))
.ForMember(d => d.Role, o => o.MapFrom(s => s.Role))
.ReverseMap();
}
}

View File

@@ -1,17 +1,17 @@
using GameIdeas.Resources;
using GameIdeas.WebAPI.Context;
using GameIdeas.WebAPI.Profiles;
using GameIdeas.WebAPI.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.Data;
using System.Data.Common;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
#if DEBUG
Load("../../../../.env");
LoadEnvironmentVariable("../../../../.env");
#else
Load(".env");
LoadEnvironmentVariable(".env");
#endif
Action<DbContextOptionsBuilder> dbContextOptions = options =>
@@ -22,8 +22,7 @@ Action<DbContextOptionsBuilder> dbContextOptions = options =>
{
npgOption.CommandTimeout(60);
npgOption.MigrationsAssembly("GameIdeas.WebAPI");
})
.LogTo(Console.WriteLine);
});
};
// Add services to the container.
@@ -32,7 +31,14 @@ services.AddDbContext<GameIdeasContext>(dbContextOptions);
services.AddSingleton<TranslationService>();
services.AddSingleton<Translations>();
services.AddScoped<GameService>();
services.AddAutoMapper(typeof(GameProfile).Assembly);
services.AddAutoMapper(typeof(UserProfile).Assembly);
services.AddAutoMapper(typeof(CategoryProfile).Assembly);
services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
services.AddOpenApi();
@@ -40,6 +46,7 @@ services.AddCors(option => option.AddDefaultPolicy(policy =>
policy.WithOrigins("http://localhost:5172", "http://localhost:7060")
.AllowAnyHeader()
.WithMethods("GET", "POST", "PUT", "DELETE")));
var app = builder.Build();
await LoadTranslations();
@@ -85,7 +92,7 @@ string GetConnectionString()
return $"Host={host};Username={login};Password={pass};Database={database}";
}
static void Load(string filePath)
static void LoadEnvironmentVariable(string filePath)
{
if (!File.Exists(filePath))
return;

View File

@@ -0,0 +1,174 @@
using AutoMapper;
using GameIdeas.Shared.Dto;
using GameIdeas.Shared.Exceptions;
using GameIdeas.Shared.Model;
using GameIdeas.WebAPI.Context;
using Microsoft.EntityFrameworkCore;
namespace GameIdeas.WebAPI.Services;
public class GameService(GameIdeasContext context, IMapper mapper)
{
public async Task<IEnumerable<GameDto>> GetGames(PaggingDto pagging)
{
var games = await SelectGames()
.OrderBy(g => g.Title)
.Skip((pagging.CurrentPage - 1) * pagging.NumberPerPage)
.Take(pagging.NumberPerPage)
.ToListAsync();
return mapper.Map<IEnumerable<GameDto>>(games);
}
public async Task<GameDto> GetGameById(int gameId)
{
var game = await SelectGames()
.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<GameDto>(game);
}
public async Task<GameDto> CreateGame(GameDto gameDto)
{
var gameToCreate = mapper.Map<Game>(gameDto);
await context.Games.AddAsync(gameToCreate);
await context.SaveChangesAsync();
await HandlePlatformsCreation(gameDto.Platforms, gameToCreate.Id);
await HandlePropertiesCreation(gameDto.Properties, gameToCreate.Id);
await HandleTagsCreation(gameDto.Tags, gameToCreate.Id);
await HandlePublishersCreation(gameDto.Publishers, gameToCreate.Id);
await HandleDevelopersCreation(gameDto.Developers, gameToCreate.Id);
await context.SaveChangesAsync();
return mapper.Map<GameDto>(gameToCreate);
}
public async Task<GameDto> UpdateGame(GameDto gameDto)
{
if (await context.Games.CountAsync(g => g.Id == gameDto.Id) == 0)
{
throw new NotFoundException($"[{typeof(Game).FullName}] with ID {gameDto.Id} has not been found in context");
}
var gameToUpdate = mapper.Map<Game>(gameDto);
await HandlePlatformsCreation(gameDto.Platforms, gameToUpdate.Id);
await HandlePropertiesCreation(gameDto.Properties, gameToUpdate.Id);
await HandleTagsCreation(gameDto.Tags, gameToUpdate.Id);
await HandlePublishersCreation(gameDto.Publishers, gameToUpdate.Id);
await HandleDevelopersCreation(gameDto.Developers, gameToUpdate.Id);
context.Games.Update(gameToUpdate);
await context.SaveChangesAsync();
return mapper.Map<GameDto>(gameToUpdate);
}
public async Task<bool> DeleteGame(int gameId)
{
var gameToRemove = await context.Games
.FirstOrDefaultAsync(g => g.Id == gameId)
?? throw new NotFoundException($"[{typeof(Game).FullName}] with ID {gameId} has not been found in context");
context.Games.Remove(gameToRemove);
return await context.SaveChangesAsync() != 0;
}
private IQueryable<Game> SelectGames()
{
return 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)
.AsQueryable();
}
private async Task HandlePlatformsCreation(IEnumerable<PlatformDto>? categoriesToCreate, int gameId)
{
if (categoriesToCreate != null)
{
var gps = mapper.Map<ICollection<GamePlatform>>(categoriesToCreate);
foreach (var gp in gps)
{
gp.GameId = gameId;
}
context.Platforms.AttachRange(gps.Select(gp => gp.Platform));
await context.GamePlatforms.AddRangeAsync(gps);
}
}
private async Task HandlePropertiesCreation(IEnumerable<PropertyDto>? categoriesToCreate, int gameId)
{
if (categoriesToCreate != null)
{
var gps = mapper.Map<ICollection<GameProperty>>(categoriesToCreate);
foreach (var gp in gps)
{
gp.GameId = gameId;
}
context.Properties.AttachRange(gps.Select(gp => gp.Property));
await context.GameProperties.AddRangeAsync(gps);
}
}
private async Task HandleTagsCreation(IEnumerable<TagDto>? categoriesToCreate, int gameId)
{
if (categoriesToCreate != null)
{
var gts = mapper.Map<ICollection<GameTag>>(categoriesToCreate);
foreach (var gt in gts)
{
gt.GameId = gameId;
}
context.Tags.AttachRange(gts.Select(gt => gt.Tag));
await context.GameTags.AddRangeAsync(gts);
}
}
private async Task HandlePublishersCreation(IEnumerable<PublisherDto>? categoriesToCreate, int gameId)
{
if (categoriesToCreate != null)
{
var gps = mapper.Map<ICollection<GamePublisher>>(categoriesToCreate);
foreach (var gp in gps)
{
gp.GameId = gameId;
}
context.Publishers.AttachRange(gps.Select(gp => gp.Publisher));
await context.GamePublishers.AddRangeAsync(gps);
}
}
private async Task HandleDevelopersCreation(IEnumerable<DeveloperDto>? categoriesToCreate, int gameId)
{
if (categoriesToCreate != null)
{
var gds = mapper.Map<ICollection<GameDeveloper>>(categoriesToCreate);
foreach (var gd in gds)
{
gd.GameId = gameId;
}
context.Developers.AttachRange(gds.Select(gd => gd.Developer));
await context.GameDevelopers.AddRangeAsync(gds);
}
}
}

View File

@@ -0,0 +1,5 @@
namespace GameIdeas.WebAPI.Services;
public class UserService
{
}