Skip to content

Send one problem response per Error everywhere: controllers match minimal APIs, and exceptions that carry an error get its status - #425

Merged
Vulthil merged 1 commit into
mainfrom
refactor/error-problem-module
Oct 1, 2026
Merged

Vulthil merged 1 commit into
mainfrom
refactor/error-problem-module

Conversation

@Vulthil

@Vulthil Vulthil commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Summary

Builds every error response in one module of Vulthil.SharedKernel.Api, so the same Error gets the same response
on every path, and an exception that carries an Error is answered like a failed result with that error.

Before (checked on a real host):

  • Minimal API (ToIResult): the full RFC 7807 body — type, title, status, detail, instance, the code,
    traceId, requestId.
  • MVC (ToActionResult): a raw ObjectResult that skipped the problem-details service, so the body had only
    status, detail and the code. Validation errors went through ValidationProblem() and had no detail.
  • GlobalExceptionHandler: one generic 500 for every exception — including a DomainException that carries an
    Error.Conflict, and FluentValidation's ValidationException thrown for commands that do not return a Result.
  • result-pattern.md claimed both paths give "the same status and detail", which was false for validation errors.

Change

  • Vulthil.Results: new public IHasError (Error Error { get; }), a type that carries an Error.
  • Vulthil.SharedKernel: DomainException implements IHasError.
  • Vulthil.SharedKernel.Application: new public CommandValidationException : FluentValidation.ValidationException, IHasError. The validation pipeline behavior throws it for commands that do not return a Result; it carries the
    same ValidationError a failed result would. Code that catches ValidationException keeps working.
  • Vulthil.SharedKernel.Api:
    • One internal builder (CustomResults.CreateProblemDetails) creates every error's problem body.
    • ToActionResult runs the same ProblemHttpResult as the minimal-API path, through an internal IActionResult
      adapter, so controllers send identical bodies. It no longer writes into ModelState.
    • GlobalExceptionHandler answers an IHasError exception with its error's problem response (a Conflict gives
      409, a validation error gives 400) and logs it at Warning when the status is 4xx. Every other exception keeps the
      generic 500 without its message, logged at Error.
  • Docs: result-pattern.md (fixed claim, new "Exceptions that carry an error" section), domain-modeling.md,
    cqrs-pipeline.md, and the Results, SharedKernel, Application and Api package pages.

Behavior changes for consumers

  • Controller error bodies gain type, title, instance, traceId and requestId; controller validation bodies
    gain detail.
  • An uncaught DomainException gets its error's status instead of 500.
  • A failed validation of a command that does not return a Result gets 400 instead of 500.
  • No public API is removed. The additions are IHasError and CommandValidationException.

Verification

  • Full solution build: 0 warnings, 0 errors.
  • All test projects pass on net10.0 and net9.0, including Vulthil.IntegrationTests (68 tests) and the Aspire
    messaging integration tests (13 tests).
  • New ProblemResponseTests (9 tests) send real requests through a test server. This adds
    Microsoft.AspNetCore.TestHost on the $(FrameworkVersion) train and to Dependabot's ignore list. They pin:
    • a controller sends the same body as a minimal API endpoint, for a not-found and for a validation error;
    • a DomainException gets the same body as a failed result (409);
    • a CommandValidationException gets the same 400 validation body as a failed result;
    • any other exception gets the generic 500 without its message;
    • the log level follows the status.
  • Mutation checks:
    • Restoring the raw ObjectResult fails 40 tests, 3 of them end-to-end.
    • Switching the exception mapping off fails 4 tests.
    • Logging every exception at Error fails 1 test.
  • dotnet pack of the four changed packages passes package validation against the 1.2.0 baseline.

Backport to v1.0: no

…imal APIs, and exceptions that carry an error get its status
@Vulthil
Vulthil merged commit 20edcdc into main Oct 1, 2026
7 checks passed
@Vulthil
Vulthil deleted the refactor/error-problem-module branch October 1, 2026 09:16
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.

1 participant