Skip to content

feat: Propagate Result<T> from handlers to endpoints (#81, #83, #85, #87)#92

Merged
mpaulosky merged 2 commits intomainfrom
squad/81-result-t-handlers
Mar 4, 2026
Merged

feat: Propagate Result<T> from handlers to endpoints (#81, #83, #85, #87)#92
mpaulosky merged 2 commits intomainfrom
squad/81-result-t-handlers

Conversation

@mpaulosky
Copy link
Copy Markdown
Owner

Working as Sam (Backend Developer)

Summary

Updated all 12 API handlers to return \Task<Result>\ and all 4 endpoint files to map \Result\ → HTTP responses.

Changes

  • 12 handlers (Get/Delete/Update × 4 domains) now return \Task<Result>\
  • 4 endpoint files map Result error codes → HTTP status codes (404/409/400/200)
  • Error codes: NotFound → 404, Conflict → 409, Validation → 400

Status

⚠️ Do not merge until Gimli updates tests for new Result return types

Closes

Closes #81, #83, #85, #87

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

…oses #81, #83, #85, #87)

- Changed all Get/Update/Delete handlers to return Task<Result<T>>
- Handlers now propagate repository Results instead of unwrapping
- Endpoints map Result to HTTP responses (Ok/NotFound/Conflict/BadRequest)
- Delete handlers return Result<bool> wrapping repository Result
- Validation errors return Result.Fail with ResultErrorCode.Validation
- NotFound errors use ResultErrorCode.NotFound
- Conflict errors use ResultErrorCode.Conflict

BREAKING: Handler return types changed - tests need update by Gimli

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 4, 2026 18:07
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 4, 2026

Test Results Summary

0 files   -   5  0 suites   - 37   0s ⏱️ - 7m 6s
0 tests  - 699  0 ✅  - 694  0 💤 ±0  0 ❌  - 5 
0 runs   - 725  0 ✅  - 720  0 💤 ±0  0 ❌  - 5 

Results for commit 9885078. ± Comparison against base commit d6ed1b2.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the API layer to propagate Result<T> from domain handlers up to minimal API endpoints, enabling centralized HTTP status mapping based on ResultErrorCode.

Changes:

  • Updated Get/Update/Delete handlers across Issues, Statuses, Categories, and Comments to return Task<Result<T>> (or Task<Result<bool>>) instead of throwing/returning null.
  • Updated endpoint route handlers to interpret Result<T> and return appropriate HTTP responses.
  • Added a JetBrains .DotSettings entry (user dictionary word).

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
src/Api/Handlers/Statuses/UpdateStatusHandler.cs Update handler now returns Result<StatusDto> and short-circuits on validation/not-found.
src/Api/Handlers/Statuses/StatusEndpoints.cs Status endpoints now map Result<T> to HTTP responses.
src/Api/Handlers/Statuses/GetStatusHandler.cs Get handler now returns Result<StatusDto> directly from repository.
src/Api/Handlers/Statuses/DeleteStatusHandler.cs Delete handler now returns Result<bool> and avoids exceptions.
src/Api/Handlers/Issues/UpdateIssueStatusHandler.cs Update-status handler now returns Result<IssueDto> and propagates repository update result.
src/Api/Handlers/Issues/UpdateIssueHandler.cs Update handler now returns Result<IssueDto> and avoids exceptions.
src/Api/Handlers/Issues/IssueEndpoints.cs Issue endpoints now map Result<T> error codes to HTTP status codes.
src/Api/Handlers/Issues/GetIssueHandler.cs Get handler now returns Result<IssueDto> directly from repository.
src/Api/Handlers/Issues/DeleteIssueHandler.cs Delete handler now returns Result<bool> and avoids exceptions.
src/Api/Handlers/Comments/UpdateCommentHandler.cs Update handler now returns Result<CommentDto> and short-circuits on validation/not-found.
src/Api/Handlers/Comments/GetCommentHandler.cs Get handler now returns Result<CommentDto> directly from repository.
src/Api/Handlers/Comments/DeleteCommentHandler.cs Delete handler now returns Result<bool> and avoids exceptions.
src/Api/Handlers/Comments/CommentEndpoints.cs Comment endpoints now map Result<T> to HTTP responses.
src/Api/Handlers/Categories/UpdateCategoryHandler.cs Update handler now returns Result<CategoryDto> and avoids exceptions.
src/Api/Handlers/Categories/GetCategoryHandler.cs Get handler now returns Result<CategoryDto> directly from repository.
src/Api/Handlers/Categories/DeleteCategoryHandler.cs Delete handler now returns Result<bool> and avoids exceptions.
src/Api/Handlers/Categories/CategoryEndpoints.cs Category endpoints now map Result<T> to HTTP responses.
IssueManager.sln.DotSettings Adds a JetBrains user dictionary word entry.

You can also share your feedback on Copilot code review. Take the survey.

throw new NotFoundException($"Status with ID '{command.Id}' could not be updated.");

return updateResult.Value!;
return await _repository.UpdateAsync(updatedStatus, cancellationToken);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning _repository.UpdateAsync(...) directly can break the endpoint’s status mapping: StatusRepository.UpdateAsync currently returns Result.Fail(...) with ErrorCode=None on not-found/update-failed, so callers won’t see ResultErrorCode.NotFound and may respond 400 instead of 404. Consider translating a failed update into a NotFound-coded Result here (or update the repository to set ErrorCode.NotFound).

Suggested change
return await _repository.UpdateAsync(updatedStatus, cancellationToken);
var updateResult = await _repository.UpdateAsync(updatedStatus, cancellationToken);
// Ensure that a failed update of a non-existent status is surfaced as NotFound
if (updateResult.Success)
return updateResult;
if (updateResult.ErrorCode is ResultErrorCode.None)
return Result.Fail<StatusDto>($"Status with ID '{command.Id}' was not found.", ResultErrorCode.NotFound);
return updateResult;

Copilot uses AI. Check for mistakes.
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the status as a <see cref="StatusDto"/>, or <see langword="null"/> if not found.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains the result with status as a <see cref="StatusDto"/>, or an error if not found.</returns>
/// <exception cref="ArgumentException">Thrown when the status ID is null or empty.</exception>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs indicate an ArgumentException can be thrown for null/empty IDs, but GetStatusQuery uses a non-nullable ObjectId and this method just forwards to the repository. Update/remove this tag to avoid misleading consumers.

Suggested change
/// <exception cref="ArgumentException">Thrown when the status ID is null or empty.</exception>

Copilot uses AI. Check for mistakes.
Comment on lines 44 to 45
/// <exception cref="ValidationException">Thrown when the command fails validation.</exception>
/// <exception cref="NotFoundException">Thrown when the comment is not found or cannot be updated.</exception>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs still advertise throwing ValidationException/NotFoundException, but this handler now returns Result for those failure cases. Update/remove the tags so the documentation matches the implementation.

Suggested change
/// <exception cref="ValidationException">Thrown when the command fails validation.</exception>
/// <exception cref="NotFoundException">Thrown when the comment is not found or cannot be updated.</exception>

Copilot uses AI. Check for mistakes.
Comment on lines 68 to +70
var command = new DeleteCommentCommand { Id = objectId };
var result = await handler.Handle(command);
return result ? Results.NoContent() : Results.NotFound();
return result.Success ? Results.NoContent() : Results.NotFound();
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint maps any delete failure to 404. DeleteCommentHandler can return ResultErrorCode.Validation (e.g., ObjectId.Empty passes TryParse but fails NotEmpty), which should be 400. Map ResultErrorCode.Validation to BadRequest (and keep NotFound → 404).

Copilot uses AI. Check for mistakes.
var updatedIssue = result.Value with { Status = command.Status };
var updateResult = await _repository.UpdateAsync(updatedIssue, cancellationToken);
return updateResult.Value;
return await _repository.UpdateAsync(updatedIssue, cancellationToken);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IssueRepository.UpdateAsync returns Result.Fail(...) with ErrorCode=None when it can’t update. Returning that directly means callers may map the failure to 400 instead of 404. Consider translating a failed update into a NotFound-coded Result here (or update the repository to emit ResultErrorCode.NotFound on update failures).

Suggested change
return await _repository.UpdateAsync(updatedIssue, cancellationToken);
var updateResult = await _repository.UpdateAsync(updatedIssue, cancellationToken);
if (!updateResult.Success && updateResult.ErrorCode == ResultErrorCode.None)
return Result.Fail<IssueDto>($"Issue with ID '{command.IssueId}' was not found.", ResultErrorCode.NotFound);
return updateResult;

Copilot uses AI. Check for mistakes.
Comment on lines 68 to +70
var command = new DeleteCategoryCommand { Id = objectId };
var result = await handler.Handle(command);
return result ? Results.NoContent() : Results.NotFound();
return result.Success ? Results.NoContent() : Results.NotFound();
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint maps any delete failure to 404. DeleteCategoryHandler can return ResultErrorCode.Validation (e.g., ObjectId.Empty passes TryParse but fails NotEmpty), which should be 400. Map ResultErrorCode.Validation to BadRequest (and keep NotFound → 404).

Copilot uses AI. Check for mistakes.
throw new NotFoundException($"Issue with ID '{command.Id}' could not be updated.");

return updateResult.Value;
return await _repository.UpdateAsync(updatedIssue, cancellationToken);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning _repository.UpdateAsync(...) directly can yield a failed Result with ErrorCode=None (IssueRepository.UpdateAsync doesn’t set NotFound/Conflict codes). That will cause IssueEndpoints to treat update failures as 400 instead of 404/409. Consider mapping a failed update to ResultErrorCode.NotFound here (and/or have the repository set appropriate ErrorCodes).

Suggested change
return await _repository.UpdateAsync(updatedIssue, cancellationToken);
var updateResult = await _repository.UpdateAsync(updatedIssue, cancellationToken);
if (!updateResult.Success && updateResult.ErrorCode == ResultErrorCode.None)
return Result.Fail<IssueDto>($"Issue with ID '{command.Id}' was not found.", ResultErrorCode.NotFound);
return updateResult;

Copilot uses AI. Check for mistakes.
throw new NotFoundException($"Comment with ID '{command.Id}' could not be updated.");

return updateResult.Value!;
return await _repository.UpdateAsync(updatedComment, cancellationToken);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning _repository.UpdateAsync(...) directly can yield ErrorCode=None on not-found/update-failed (CommentRepository.UpdateAsync doesn’t set ResultErrorCode.NotFound). That will make CommentEndpoints return 400 instead of 404 for missing comments. Consider translating failed updates into a NotFound-coded Result here (or update the repository to set codes).

Suggested change
return await _repository.UpdateAsync(updatedComment, cancellationToken);
var updateResult = await _repository.UpdateAsync(updatedComment, cancellationToken);
if (updateResult.Failure || updateResult.Value is null)
return Result.Fail<CommentDto>($"Comment with ID '{command.Id}' was not found or could not be updated.", ResultErrorCode.NotFound);
return updateResult;

Copilot uses AI. Check for mistakes.
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the comment as a <see cref="CommentDto"/>, or <see langword="null"/> if not found.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains the result with comment as a <see cref="CommentDto"/>, or an error if not found.</returns>
/// <exception cref="ArgumentException">Thrown when the comment ID is null or empty.</exception>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs indicate an ArgumentException can be thrown for null/empty IDs, but GetCommentQuery uses a non-nullable ObjectId and this method just forwards to the repository. Update/remove this tag to avoid misleading consumers.

Suggested change
/// <exception cref="ArgumentException">Thrown when the comment ID is null or empty.</exception>

Copilot uses AI. Check for mistakes.
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the category as a <see cref="CategoryDto"/>, or <see langword="null"/> if not found.</returns>
/// <returns>A task that represents the asynchronous operation. The task result contains the result with category as a <see cref="CategoryDto"/>, or an error if not found.</returns>
/// <exception cref="ArgumentException">Thrown when the category ID is null or empty.</exception>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs indicate an ArgumentException can be thrown for null/empty IDs, but GetCategoryQuery uses a non-nullable ObjectId and this method just forwards to the repository. Update/remove this tag to avoid misleading consumers.

Suggested change
/// <exception cref="ArgumentException">Thrown when the category ID is null or empty.</exception>

Copilot uses AI. Check for mistakes.
#86, #88)

Updated 25 test files across Unit.Tests and Integration.Tests to assert on
Result<T> return values. Get/Delete/Update handlers in all 4 domains now
verified via result.Success, result.Value, and result.ErrorCode assertions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mpaulosky
Copy link
Copy Markdown
Owner Author

Tests updated by Gimli - 25 test files fixed across Unit.Tests and Integration.Tests. Build: 0 errors. Pre-push Architecture.Tests: 9/9 passing. Ready for merge.

@mpaulosky mpaulosky merged commit 11f82c9 into main Mar 4, 2026
6 of 8 checks passed
@mpaulosky mpaulosky deleted the squad/81-result-t-handlers branch March 4, 2026 18:46
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 4, 2026

Codecov Report

❌ Patch coverage is 83.87097% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.87%. Comparing base (d6ed1b2) to head (6a17ad5).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/Api/Handlers/Issues/IssueEndpoints.cs 62.50% 3 Missing ⚠️
src/Api/Handlers/Categories/CategoryEndpoints.cs 83.33% 1 Missing ⚠️
...c/Api/Handlers/Categories/DeleteCategoryHandler.cs 75.00% 0 Missing and 1 partial ⚠️
src/Api/Handlers/Comments/CommentEndpoints.cs 83.33% 1 Missing ⚠️
src/Api/Handlers/Comments/DeleteCommentHandler.cs 75.00% 0 Missing and 1 partial ⚠️
src/Api/Handlers/Issues/DeleteIssueHandler.cs 75.00% 0 Missing and 1 partial ⚠️
src/Api/Handlers/Statuses/DeleteStatusHandler.cs 75.00% 0 Missing and 1 partial ⚠️
src/Api/Handlers/Statuses/StatusEndpoints.cs 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #92      +/-   ##
==========================================
- Coverage   82.48%   81.87%   -0.62%     
==========================================
  Files          74       74              
  Lines        1513     1506       -7     
  Branches      113      109       -4     
==========================================
- Hits         1248     1233      -15     
- Misses        217      222       +5     
- Partials       48       51       +3     
Files with missing lines Coverage Δ
src/Api/Handlers/Categories/GetCategoryHandler.cs 100.00% <100.00%> (ø)
...c/Api/Handlers/Categories/UpdateCategoryHandler.cs 100.00% <100.00%> (ø)
src/Api/Handlers/Comments/GetCommentHandler.cs 100.00% <100.00%> (ø)
src/Api/Handlers/Comments/UpdateCommentHandler.cs 100.00% <100.00%> (ø)
src/Api/Handlers/Issues/GetIssueHandler.cs 88.88% <100.00%> (-1.12%) ⬇️
src/Api/Handlers/Issues/UpdateIssueHandler.cs 100.00% <100.00%> (+9.09%) ⬆️
...rc/Api/Handlers/Issues/UpdateIssueStatusHandler.cs 100.00% <100.00%> (ø)
src/Api/Handlers/Statuses/GetStatusHandler.cs 100.00% <100.00%> (ø)
src/Api/Handlers/Statuses/UpdateStatusHandler.cs 100.00% <100.00%> (ø)
src/Api/Handlers/Categories/CategoryEndpoints.cs 95.08% <83.33%> (-1.53%) ⬇️
... and 7 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Sprint] Issues: Refactor handlers to use ObjectId and return Result<T>

2 participants