refactor: replace Ardalis.Result with ErrorOr across the app

Ardalis Result<T> carried incoherent success sub-statuses and a clunky
generic converter; ErrorOr models success-or-errors cleanly.

- Application: all commands/queries/handlers now return ErrorOr<T>
  (non-generic Result -> ErrorOr<Success>); errors via Error.NotFound/
  Conflict/Validation/Unauthorized/Failure; success via value or new Success()
- Drop ResultExtensions.As<>(): error propagation is now just return .Errors
- Rewrite HTTP mapping (ErrorOrHttpExtensions) off Error.Type; success maps
  to 200 (value) or 204 (Success); Created still built in the endpoints
- Endpoints/gateway middleware: !IsSuccess -> IsError
- Swap Ardalis.Result package for ErrorOr 2.1.1 (remove unused ref from Worker)

Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 04:05:43 +02:00
co-authored by Claude Opus 4.8
parent a3bf796f17
commit 6e469dbb4a
56 changed files with 259 additions and 283 deletions
@@ -119,7 +119,7 @@ public static class PlayListEndpoints
new CreatePlayListCommand(currentUser.RequiredId, request.Name, request.Description, request.PictureIntentId),
cancellationToken);
if (!result.IsSuccess)
if (result.IsError)
return result.ToHttpResult();
return Results.Created($"/playlists/{result.Value.Id}", result.Value);
@@ -103,7 +103,7 @@ public static class TrackEndpoints
new CreateTrackCommand(currentUser.RequiredId, request.Title, request.PictureIntentId, request.AudioIntentId),
cancellationToken);
if (!result.IsSuccess)
if (result.IsError)
return result.ToHttpResult();
return Results.Created($"/tracks/{result.Value.Id}", result.Value);
@@ -59,7 +59,7 @@ public static class UserEndpoints
new CreateUserCommand(request.Id, request.Name, request.FirstName, request.SecondName),
cancellationToken);
if (!result.IsSuccess)
if (result.IsError)
return result.ToHttpResult();
return Results.Created($"/users/{result.Value.Id}", result.Value);
@@ -0,0 +1,38 @@
using ErrorOr;
using IResult = Microsoft.AspNetCore.Http.IResult;
namespace Musify.Api.Extensions;
public static class ErrorOrHttpExtensions
{
public static IResult ToHttpResult<T>(this ErrorOr<T> result)
{
if (result.IsError)
return Problem(result.Errors);
return result.Value is Success
? Results.NoContent()
: Results.Ok(result.Value);
}
private static IResult Problem(List<Error> errors)
{
if (errors.All(e => e.Type == ErrorType.Validation))
return Results.ValidationProblem(ToValidationDictionary(errors));
var error = errors[0];
return error.Type switch
{
ErrorType.NotFound => Results.NotFound(error.Description),
ErrorType.Unauthorized => Results.Unauthorized(),
ErrorType.Forbidden => Results.StatusCode(StatusCodes.Status403Forbidden),
ErrorType.Conflict => Results.Conflict(error.Description),
_ => Results.Problem(error.Description)
};
}
private static Dictionary<string, string[]> ToValidationDictionary(IEnumerable<Error> errors) =>
errors
.GroupBy(e => string.IsNullOrEmpty(e.Code) ? "general" : e.Code)
.ToDictionary(g => g.Key, g => g.Select(e => e.Description).ToArray());
}
@@ -1,36 +0,0 @@
using Ardalis.Result;
using IResult = Microsoft.AspNetCore.Http.IResult;
namespace Musify.Api.Extensions;
public static class ResultHttpExtensions
{
public static IResult ToHttpResult(this Result result) =>
result.Status switch
{
ResultStatus.Ok or ResultStatus.NoContent => Results.NoContent(),
ResultStatus.NotFound => Results.NotFound(result.Errors.FirstOrDefault()),
ResultStatus.Invalid => Results.ValidationProblem(ToValidationDictionary(result.ValidationErrors)),
ResultStatus.Unauthorized => Results.Unauthorized(),
ResultStatus.Forbidden => Results.StatusCode(StatusCodes.Status403Forbidden),
ResultStatus.Conflict => Results.Conflict(result.Errors.FirstOrDefault()),
_ => Results.Problem(string.Join(", ", result.Errors))
};
public static IResult ToHttpResult<T>(this Result<T> result) =>
result.Status switch
{
ResultStatus.Ok => Results.Ok(result.Value),
ResultStatus.NotFound => Results.NotFound(result.Errors.FirstOrDefault()),
ResultStatus.Invalid => Results.ValidationProblem(ToValidationDictionary(result.ValidationErrors)),
ResultStatus.Unauthorized => Results.Unauthorized(),
ResultStatus.Forbidden => Results.StatusCode(StatusCodes.Status403Forbidden),
ResultStatus.Conflict => Results.Conflict(result.Errors.FirstOrDefault()),
_ => Results.Problem(string.Join(", ", result.Errors))
};
private static Dictionary<string, string[]> ToValidationDictionary(IEnumerable<ValidationError> errors) =>
errors
.GroupBy(e => string.IsNullOrEmpty(e.Identifier) ? "general" : e.Identifier)
.ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
}
+1 -1
View File
@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ardalis.Result" />
<PackageReference Include="ErrorOr" />
<PackageReference Include="AWSSDK.S3" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
<PackageReference Include="MassTransit" />