Decouple workflow services from HTTP result types

This commit is contained in:
2026-02-08 21:43:07 +01:00
parent fe6a9d5da4
commit 2d2201d0a2
14 changed files with 242 additions and 137 deletions

View File

@@ -7,7 +7,7 @@ namespace GameList.Endpoints;
internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFactory httpFactory)
{
public async Task<IResult> GetMineAsync(Guid playerId)
public async Task<ServiceResult<IReadOnlyList<SuggestionDto>>> GetMineAsync(Guid playerId)
{
var mine = await db.Suggestions
.AsNoTracking()
@@ -29,18 +29,19 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
})
.ToListAsync();
var ordered = mine
IReadOnlyList<SuggestionDto> ordered = mine
.OrderBy(s => s.CreatedAt)
.Select(s => new SuggestionDto(s.Id, s.Name, s.Genre, s.Description, s.ScreenshotUrl, s.YoutubeUrl, s.GameUrl, s.MinPlayers, s.MaxPlayers, s.ParentSuggestionId));
.Select(s => new SuggestionDto(s.Id, s.Name, s.Genre, s.Description, s.ScreenshotUrl, s.YoutubeUrl, s.GameUrl, s.MinPlayers, s.MaxPlayers, s.ParentSuggestionId))
.ToList();
return Results.Ok(ordered);
return ServiceResult<IReadOnlyList<SuggestionDto>>.Success(ordered);
}
public async Task<IResult> CreateAsync(Guid playerId, SuggestionInput input)
public async Task<ServiceResult<SuggestionCreatedResponse>> CreateAsync(Guid playerId, SuggestionInput input)
{
var validationError = await SuggestionValidator.ValidateAsync(input, httpFactory);
if (validationError is not null)
return EndpointHelpers.BadRequestError(validationError);
return ServiceResult<SuggestionCreatedResponse>.Failure(ServiceError.BadRequest(validationError));
var playerState = await db.Players
.AsNoTracking()
@@ -55,14 +56,14 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
var usingJoker = phase == Phase.Vote && playerState.HasJoker;
if (phase != Phase.Suggest && !usingJoker)
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
return ServiceResult<SuggestionCreatedResponse>.Failure(ServiceError.PhaseMismatch(Phase.Suggest, phase));
if (string.IsNullOrWhiteSpace(playerState.DisplayName))
return EndpointHelpers.BadRequestError("Set a display name before submitting suggestions.");
return ServiceResult<SuggestionCreatedResponse>.Failure(ServiceError.BadRequest("Set a display name before submitting suggestions."));
var existingCount = await db.Suggestions.AsNoTracking().CountAsync(s => s.PlayerId == playerId);
if (!usingJoker && existingCount >= 5)
return EndpointHelpers.BadRequestError("You have reached the 5 suggestion limit.");
return ServiceResult<SuggestionCreatedResponse>.Failure(ServiceError.BadRequest("You have reached the 5 suggestion limit."));
var suggestion = new Suggestion
{
@@ -97,13 +98,13 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
}
catch (DbUpdateException ex) when (EndpointHelpers.IsSqliteConstraintViolation(ex, EndpointHelpers.SuggestionLimitTriggerError))
{
return EndpointHelpers.BadRequestError("You have reached the 5 suggestion limit.");
return ServiceResult<SuggestionCreatedResponse>.Failure(ServiceError.BadRequest("You have reached the 5 suggestion limit."));
}
return Results.Created($"/api/suggestions/{suggestion.Id}", new SuggestionCreatedResponse(suggestion.Id));
return ServiceResult<SuggestionCreatedResponse>.Success(new SuggestionCreatedResponse(suggestion.Id));
}
public async Task<IResult> DeleteAsync(Guid playerId, int suggestionId)
public async Task<ServiceResult<Unit>> DeleteAsync(Guid playerId, int suggestionId)
{
var actor = await db.Players
.AsNoTracking()
@@ -119,14 +120,14 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
{
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
if (phase != Phase.Suggest)
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
return ServiceResult<Unit>.Failure(ServiceError.PhaseMismatch(Phase.Suggest, phase));
}
var suggestion = isAdmin
? await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId)
: await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId && s.PlayerId == playerId);
if (suggestion == null)
return EndpointHelpers.NotFoundError("Suggestion not found.");
return ServiceResult<Unit>.Failure(ServiceError.NotFound("Suggestion not found."));
await using var tx = await db.Database.BeginTransactionAsync();
@@ -139,14 +140,14 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
db.Suggestions.Remove(suggestion);
await db.SaveChangesAsync();
await tx.CommitAsync();
return Results.NoContent();
return ServiceResult<Unit>.Success(default);
}
public async Task<IResult> UpdateAsync(Guid playerId, int suggestionId, SuggestionInput input)
public async Task<ServiceResult<SuggestionUpdatedResponse>> UpdateAsync(Guid playerId, int suggestionId, SuggestionInput input)
{
var validationError = await SuggestionValidator.ValidateAsync(input, httpFactory);
if (validationError is not null)
return EndpointHelpers.BadRequestError(validationError);
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.BadRequest(validationError));
var actor = await db.Players
.AsNoTracking()
@@ -159,17 +160,17 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
var suggestion = await db.Suggestions.FirstOrDefaultAsync(s => s.Id == suggestionId);
if (suggestion == null)
return EndpointHelpers.NotFoundError("Suggestion not found.");
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.NotFound("Suggestion not found."));
var isAdmin = actor.IsAdmin;
if (!isAdmin)
{
if (suggestion.PlayerId != playerId)
return EndpointHelpers.UnauthorizedError();
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.Unauthorized());
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
if (phase == Phase.Results)
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.PhaseMismatch(Phase.Suggest, phase));
if (phase == Phase.Suggest)
{
@@ -177,7 +178,7 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
}
else if (phase != Phase.Vote)
{
return EndpointHelpers.PhaseMismatch(Phase.Suggest, phase);
return ServiceResult<SuggestionUpdatedResponse>.Failure(ServiceError.PhaseMismatch(Phase.Suggest, phase));
}
ApplyEditableFields(suggestion, input);
@@ -190,7 +191,7 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
await db.SaveChangesAsync();
return Results.Ok(new SuggestionUpdatedResponse(
return ServiceResult<SuggestionUpdatedResponse>.Success(new SuggestionUpdatedResponse(
suggestion.Id,
suggestion.Name,
suggestion.Genre,
@@ -203,11 +204,11 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
));
}
public async Task<IResult> GetAllAsync(Guid playerId)
public async Task<ServiceResult<IReadOnlyList<SuggestionAllDto>>> GetAllAsync(Guid playerId)
{
var phase = await EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
if (phase < Phase.Vote)
return EndpointHelpers.PhaseMismatch(Phase.Vote, phase);
return ServiceResult<IReadOnlyList<SuggestionAllDto>>.Failure(ServiceError.PhaseMismatch(Phase.Vote, phase));
var all = await db.Suggestions
.AsNoTracking()
@@ -233,12 +234,11 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
var rootIndex = EndpointHelpers.BuildLinkRoots(all.Select(s => (s.Id, s.ParentSuggestionId)));
var nameLookup = all.ToDictionary(s => s.Id, s => s.Name);
var ordered = all.OrderBy(s => s.CreatedAt).Select(s =>
IReadOnlyList<SuggestionAllDto> ordered = all.OrderBy(s => s.CreatedAt).Select(s =>
{
var linkedIds = EndpointHelpers.LinkedIdsFor(s.Id, rootIndex).Where(id => id != s.Id).ToList();
return new
{
return new SuggestionAllDto(
s.Id,
s.Name,
s.Genre,
@@ -251,12 +251,12 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
s.Author,
s.ParentSuggestionId,
s.IsOwner,
LinkedIds = linkedIds,
LinkedTitles = linkedIds.Where(nameLookup.ContainsKey).Select(id => nameLookup[id]).ToList()
};
});
linkedIds,
linkedIds.Where(nameLookup.ContainsKey).Select(id => nameLookup[id]).ToList()
);
}).ToList();
return Results.Ok(ordered);
return ServiceResult<IReadOnlyList<SuggestionAllDto>>.Success(ordered);
}
private static void ApplyEditableFields(Suggestion suggestion, SuggestionInput input)