Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ services.AddSingleton<IPipelineMiddlewareProvider, MyMiddlewareProvider>();

## Result Mapper

Register an `IResultMapper` to apply shared exception-to-HTTP-status mapping across endpoints.
Subclass `ResultMapper` to apply shared exception-to-HTTP-status mapping across endpoints.
See the [AspNetCore README](../src/Hyperbee.Pipeline.AspNetCore/README.md) for details.

```csharp
services.AddSingleton<IResultMapper, ConflictResultMapper>();
services.AddSingleton<ResultMapper, ConflictResultMapper>();
```
30 changes: 24 additions & 6 deletions docs/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ app.MapPost("/orders", async (Order order, ICommandFunction<Order, OrderResult>
var result = await command.ExecuteAsync(order, cancellationToken);

// Automatically returns:
// - 400 Bad Request for validation failures
// - 422 Unprocessable Entity for validation failures
// - 404 Not Found for NotFoundValidationFailure
// - 401 Unauthorized for UnauthorizedValidationFailure
// - 403 Forbidden for ForbiddenValidationFailure
Expand All @@ -357,23 +357,41 @@ app.MapPost("/orders", async (Order order, ICommandFunction<Order, OrderResult>
});
```

For shared mapping logic across many endpoints, implement `IResultMapper` and register it with DI:
For shared mapping logic across many endpoints, subclass `ResultMapper` and register it with DI:

```csharp
services.AddSingleton<IResultMapper, ConflictResultMapper>();
services.AddSingleton<ResultMapper, ConflictResultMapper>();

app.MapPost("/orders", async (
Order order,
ICommandFunction<Order, OrderResult> command,
IResultMapper resultMapper) =>
ResultMapper mapper) =>
{
var result = await command.ExecuteAsync(order);
return result.ToResult( resultMapper );
return result.ToResult( mapper );
});
```

For one-off customizations without subclassing, use `ResultMapper.Create()` with lambdas:

```csharp
app.MapPost("/orders", async (Order order, ICommandFunction<Order, OrderResult> command) =>
{
var mapper = ResultMapper.Create(
mapException: ex => ex is DbUpdateConcurrencyException
? Results.Conflict( "Version conflict. Reload and retry." )
: null
);

var result = await command.ExecuteAsync(order);
return result.ToResult( x => x?.ToPresentation(), mapper );
});
```

Any lambda not provided falls back to the default behavior.

See the [AspNetCore README](../src/Hyperbee.Pipeline.AspNetCore/README.md) for full details on
`IResultMapper` and all `ToResult` overloads.
`ResultMapper` and all `ToResult` overloads.

## Advanced Scenarios

Expand Down
42 changes: 42 additions & 0 deletions src/Hyperbee.Pipeline.AspNetCore/DecoratedResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Http;

namespace Hyperbee.Pipeline.AspNetCore;

/// <summary>
/// Wraps an <see cref="IResult"/> with a list of actions that decorate the <see cref="HttpContext"/>
/// before the inner result executes. Supports composing multiple decorators via chaining.
/// </summary>
internal sealed class DecoratedResult : IResult
{
internal IResult Inner { get; }
internal List<Action<HttpContext>> Decorators { get; }

internal DecoratedResult( IResult inner, Action<HttpContext> decorator )
{
// Flatten: if inner is already a DecoratedResult, absorb its decorators
if ( inner is DecoratedResult existing )
{
Inner = existing.Inner;
Decorators = [.. existing.Decorators, decorator];
}
else
{
Inner = inner;
Decorators = [decorator];
}
}

internal DecoratedResult( IResult inner, List<Action<HttpContext>> decorators )
{
Inner = inner;
Decorators = decorators;
}

public async Task ExecuteAsync( HttpContext httpContext )
{
foreach ( var decorator in Decorators )
decorator( httpContext );

await Inner.ExecuteAsync( httpContext );
}
}
Loading
Loading