Complete CRUD for games
All checks were successful
Game Ideas build for PR / build_blazor_app (pull_request) Successful in 51s

This commit is contained in:
2025-04-12 21:32:10 +02:00
parent 5d655d8b4b
commit 2f9457fc7e
9 changed files with 276 additions and 98 deletions

View File

@@ -7,8 +7,10 @@ public class GameDto
public DateTime? ReleaseDate { get; set; } public DateTime? ReleaseDate { get; set; }
public DateTime? CreationDate { get; set; } public DateTime? CreationDate { get; set; }
public UserDto? CreationUser { get; set; } public UserDto? CreationUser { get; set; }
public int? CreationUserId { get; set; }
public DateTime? ModificationDate { get; set; } public DateTime? ModificationDate { get; set; }
public UserDto? ModificationUser { get; set; } public UserDto? ModificationUser { get; set; }
public int? ModificationUserId { get; set; }
public double? StorageSpace { get; set; } public double? StorageSpace { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public int? Interest { get; set; } public int? Interest { get; set; }

View File

@@ -8,6 +8,4 @@ public class UserDto
public string? Username { get; set; } public string? Username { get; set; }
public string? Password { get; set; } public string? Password { get; set; }
public Role? Role { get; set; } public Role? Role { get; set; }
public IEnumerable<GameDto>? CreationGames { get; set; }
public IEnumerable<GameDto>? ModificationGames { get; set; }
} }

View File

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

View File

@@ -7,11 +7,77 @@ namespace GameIdeas.WebAPI.Controllers;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class GameController(GameService gameService) : Controller public class GameController(GameService gameService, ILoggerFactory loggerFactory) : Controller
{ {
[HttpGet("Search")] private readonly ILogger<GameController> logger = loggerFactory.CreateLogger<GameController>();
public async Task<IEnumerable<GameDto>> FetchGames([FromQuery] PaggingDto pagging)
[HttpGet]
public async Task<ActionResult<IEnumerable<GameDto>>> SearchGames([FromQuery] PaggingDto pagging)
{ {
return await gameService.GetGames(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

@@ -8,29 +8,76 @@ public class CategoryProfile : Profile
{ {
public CategoryProfile() public CategoryProfile()
{ {
CreateMap<Platform, PlatformDto>() CreatePlaformMap();
CreatePropertyMap();
CreateTagMap();
CreateDeveloperMap();
CreatePublisherMap();
}
private void CreatePublisherMap()
{
CreateMap<Publisher, PublisherDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id)) .ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Label, o => o.MapFrom(s => s.Label)) .ForMember(d => d.Name, o => o.MapFrom(s => s.Name))
.ReverseMap(); .ReverseMap();
CreateMap<Property, PropertyDto>() CreateMap<PublisherDto, GamePublisher>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id)) .ForMember(d => d.PublisherId, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Label, o => o.MapFrom(s => s.Label)) .ForPath(d => d.Publisher.Id, o => o.MapFrom(s => s.Id))
.ReverseMap(); .ForPath(d => d.Publisher.Name, o => o.MapFrom(s => s.Name));
}
CreateMap<Tag, TagDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Label, o => o.MapFrom(s => s.Label))
.ReverseMap();
private void CreateDeveloperMap()
{
CreateMap<Developer, DeveloperDto>() CreateMap<Developer, DeveloperDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id)) .ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Name, o => o.MapFrom(s => s.Name)) .ForMember(d => d.Name, o => o.MapFrom(s => s.Name))
.ReverseMap(); .ReverseMap();
CreateMap<Publisher, PublisherDto>() 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.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Name, o => o.MapFrom(s => s.Name)) .ForMember(d => d.Label, o => o.MapFrom(s => s.Label))
.ReverseMap(); .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

@@ -13,10 +13,10 @@ public class GameProfile : Profile
.ForMember(d => d.Title, o => o.MapFrom(s => s.Title)) .ForMember(d => d.Title, o => o.MapFrom(s => s.Title))
.ForMember(d => d.ReleaseDate, o => o.MapFrom(s => s.ReleaseDate)) .ForMember(d => d.ReleaseDate, o => o.MapFrom(s => s.ReleaseDate))
.ForMember(d => d.CreationDate, o => o.MapFrom(s => s.CreationDate)) .ForMember(d => d.CreationDate, o => o.MapFrom(s => s.CreationDate))
.ForPath(d => d.CreationUser!.Id, o => o.MapFrom(s => s.CreationUserId)) .ForMember(d => d.CreationUserId, o => o.MapFrom(s => s.CreationUserId))
.ForMember(d => d.CreationUser, o => o.MapFrom(s => s.CreationUser)) .ForMember(d => d.CreationUser, o => o.MapFrom(s => s.CreationUser))
.ForMember(d => d.ModificationDate, o => o.MapFrom(s => s.ModificationDate)) .ForMember(d => d.ModificationDate, o => o.MapFrom(s => s.ModificationDate))
.ForPath(d => d.ModificationUser!.Id, o => o.MapFrom(s => s.ModificationUserId)) .ForMember(d => d.ModificationUserId, o => o.MapFrom(s => s.ModificationUserId))
.ForMember(d => d.ModificationUser, o => o.MapFrom(s => s.ModificationUser)) .ForMember(d => d.ModificationUser, o => o.MapFrom(s => s.ModificationUser))
.ForMember(d => d.StorageSpace, o => o.MapFrom(s => s.StorageSpace)) .ForMember(d => d.StorageSpace, o => o.MapFrom(s => s.StorageSpace))
.ForMember(d => d.Description, o => o.MapFrom(s => s.Description)) .ForMember(d => d.Description, o => o.MapFrom(s => s.Description))

View File

@@ -14,8 +14,6 @@ public class UserProfile : Profile
.ForMember(d => d.Username, o => o.MapFrom(s => s.Username)) .ForMember(d => d.Username, o => o.MapFrom(s => s.Username))
.ForMember(d => d.Password, o => o.MapFrom(s => s.Password)) .ForMember(d => d.Password, o => o.MapFrom(s => s.Password))
.ForMember(d => d.Role, o => o.MapFrom(s => s.Role)) .ForMember(d => d.Role, o => o.MapFrom(s => s.Role))
.ForMember(d => d.CreationGames, o => o.MapFrom(s => s.CreationGames))
.ForMember(d => d.ModificationGames, o => o.MapFrom(s => s.ModificationGames))
.ReverseMap(); .ReverseMap();
} }

View File

@@ -3,6 +3,7 @@ using GameIdeas.WebAPI.Context;
using GameIdeas.WebAPI.Profiles; using GameIdeas.WebAPI.Profiles;
using GameIdeas.WebAPI.Services; using GameIdeas.WebAPI.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
var services = builder.Services; var services = builder.Services;
@@ -37,6 +38,7 @@ services.AddAutoMapper(typeof(UserProfile).Assembly);
services.AddAutoMapper(typeof(CategoryProfile).Assembly); services.AddAutoMapper(typeof(CategoryProfile).Assembly);
services.AddControllers(); services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
services.AddOpenApi(); services.AddOpenApi();
@@ -44,6 +46,7 @@ services.AddCors(option => option.AddDefaultPolicy(policy =>
policy.WithOrigins("http://localhost:5172", "http://localhost:7060") policy.WithOrigins("http://localhost:5172", "http://localhost:7060")
.AllowAnyHeader() .AllowAnyHeader()
.WithMethods("GET", "POST", "PUT", "DELETE"))); .WithMethods("GET", "POST", "PUT", "DELETE")));
var app = builder.Build(); var app = builder.Build();
await LoadTranslations(); await LoadTranslations();

View File

@@ -1,11 +1,9 @@
using AutoMapper; using AutoMapper;
using GameIdeas.Shared.Dto; using GameIdeas.Shared.Dto;
using GameIdeas.Shared.Enum;
using GameIdeas.Shared.Exceptions; using GameIdeas.Shared.Exceptions;
using GameIdeas.Shared.Model; using GameIdeas.Shared.Model;
using GameIdeas.WebAPI.Context; using GameIdeas.WebAPI.Context;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Data.SqlTypes;
namespace GameIdeas.WebAPI.Services; namespace GameIdeas.WebAPI.Services;
@@ -13,7 +11,77 @@ public class GameService(GameIdeasContext context, IMapper mapper)
{ {
public async Task<IEnumerable<GameDto>> GetGames(PaggingDto pagging) public async Task<IEnumerable<GameDto>> GetGames(PaggingDto pagging)
{ {
var query = context.Games 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.CreationUser)
.Include(g => g.ModificationUser) .Include(g => g.ModificationUser)
.Include(g => g.GamePlatforms).ThenInclude(p => p.Platform) .Include(g => g.GamePlatforms).ThenInclude(p => p.Platform)
@@ -22,92 +90,85 @@ public class GameService(GameIdeasContext context, IMapper mapper)
.Include(g => g.GamePublishers).ThenInclude(p => p.Publisher) .Include(g => g.GamePublishers).ThenInclude(p => p.Publisher)
.Include(g => g.GameDevelopers).ThenInclude(p => p.Developer) .Include(g => g.GameDevelopers).ThenInclude(p => p.Developer)
.AsQueryable(); .AsQueryable();
var games = await query
.OrderBy(g => g.Title)
.Skip(pagging.CurrentPage * pagging.NumberPerPage)
.Take(pagging.NumberPerPage)
.ToListAsync();
List<Game> gameTest = [ new() { Id = 1,
Title = "Test",
CreationDate = DateTime.Today - TimeSpan.FromHours(-12),
CreationUser = new User() { Id = 1, Username = "Test", Role = (int)Role.Administrator },
CreationUserId = 1,
Description = "Test",
GameDevelopers = [new() { Developer = new Developer() { Id = 1, Name = "THQNordic" } }],
GamePlatforms = [new() { Platform = new Platform() { Id = 1, Label = "Steam" }, Url = "steam.com?app=55554131" }],
GameProperties = [new() { Property = new Property() { Id = 1, Label = "Coop" } }],
GamePublishers = [new() { Publisher = new Publisher() { Id = 1, Name = "Take-Two" } }],
GameTags = [new() { Tag = new Tag() { Id = 1, Label = "RPG" } }],
Interest = 5,
ModificationDate = DateTime.Today ,
ModificationUser = new User() { Id = 2, Username = "Test 2", Role = (int)Role.Member },
ModificationUserId = 2,
ReleaseDate = new DateTime(2025, 2, 1),
StorageSpace = 40
} ];
return mapper.Map<IEnumerable<GameDto>>(gameTest);
} }
public async Task<GameDto> GetGameById(int gameId) private async Task HandlePlatformsCreation(IEnumerable<PlatformDto>? categoriesToCreate, int gameId)
{ {
var game = await context.Games if (categoriesToCreate != null)
.Include(g => g.CreationUser) {
.Include(g => g.ModificationUser) var gps = mapper.Map<ICollection<GamePlatform>>(categoriesToCreate);
.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 foreach (var gp in gps)
? throw new NotFoundException($"[{typeof(Game).Name}] with ID {gameId} has not been found in context") {
: mapper.Map<GameDto>(game); gp.GameId = gameId;
}
context.Platforms.AttachRange(gps.Select(gp => gp.Platform));
await context.GamePlatforms.AddRangeAsync(gps);
}
} }
public async Task<GameDto> CreateGame(GameDto gameDto) private async Task HandlePropertiesCreation(IEnumerable<PropertyDto>? categoriesToCreate, int gameId)
{ {
var game = mapper.Map<Game>(gameDto); if (categoriesToCreate != null)
{
var gps = mapper.Map<ICollection<GameProperty>>(categoriesToCreate);
context.GameDevelopers.AttachRange(game.GameDevelopers); foreach (var gp in gps)
context.Developers.AttachRange(game.GameDevelopers.Select(gd => gd.Developer)); {
context.GamePlatforms.AttachRange(game.GamePlatforms); gp.GameId = gameId;
context.Platforms.AttachRange(game.GamePlatforms.Select(gp => gp.Platform)); }
context.GameProperties.AttachRange(game.GameProperties);
context.Properties.AttachRange(game.GameProperties.Select(gp => gp.Property));
context.GamePublishers.AttachRange(game.GamePublishers);
context.Publishers.AttachRange(game.GamePublishers.Select(gp => gp.Publisher));
context.GameTags.AttachRange(game.GameTags);
context.Tags.AttachRange(game.GameTags.Select(gt => gt.Tag));
await context.Games.AddAsync(game); context.Properties.AttachRange(gps.Select(gp => gp.Property));
await context.SaveChangesAsync(); await context.GameProperties.AddRangeAsync(gps);
}
return mapper.Map<GameDto>(game);
} }
public async Task<GameDto> UpdateGame(GameDto gameDto) private async Task HandleTagsCreation(IEnumerable<TagDto>? categoriesToCreate, int gameId)
{ {
var gameToUpdate = mapper.Map<Game>(gameDto); if (categoriesToCreate != null)
{
var gts = mapper.Map<ICollection<GameTag>>(categoriesToCreate);
context.GameDevelopers.AttachRange(gameToUpdate.GameDevelopers); foreach (var gt in gts)
context.Developers.AttachRange(gameToUpdate.GameDevelopers.Select(gd => gd.Developer)); {
context.GamePlatforms.AttachRange(gameToUpdate.GamePlatforms); gt.GameId = gameId;
context.Platforms.AttachRange(gameToUpdate.GamePlatforms.Select(gp => gp.Platform)); }
context.GameProperties.AttachRange(gameToUpdate.GameProperties);
context.Properties.AttachRange(gameToUpdate.GameProperties.Select(gp => gp.Property));
context.GamePublishers.AttachRange(gameToUpdate.GamePublishers);
context.Publishers.AttachRange(gameToUpdate.GamePublishers.Select(gp => gp.Publisher));
context.GameTags.AttachRange(gameToUpdate.GameTags);
context.Tags.AttachRange(gameToUpdate.GameTags.Select(gt => gt.Tag));
context.Tags.AttachRange(gts.Select(gt => gt.Tag));
await context.GameTags.AddRangeAsync(gts);
}
}
context.Games.Update(gameToUpdate); private async Task HandlePublishersCreation(IEnumerable<PublisherDto>? categoriesToCreate, int gameId)
await context.SaveChangesAsync(); {
if (categoriesToCreate != null)
{
var gps = mapper.Map<ICollection<GamePublisher>>(categoriesToCreate);
return mapper.Map<GameDto>(gameToUpdate); 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);
}
} }
} }