Skip to content
Open
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 CSharpEssentials.AspNetCore/Readme.MD
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ app.UseVersionableSwagger();
### Result Endpoint Filter

```csharp
builder.Services.AddScoped<ResultEndpointFilter>();
builder.Services.AddSingleton<IResultErrorMapper, DomainErrorMapper>(); // Optional

app.MapGet("/users/{id}", (int id) => GetUser(id))
.AddEndpointFilter<ResultEndpointFilter>();

// Returns 200 with value on success, 400 with errors on failure
// Returns 200 with value on success, mapped error result on failure
```

### Structured Error Response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
using CSharpEssentials.Errors;
using CSharpEssentials.ResultPattern;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace CSharpEssentials.AspNetCore;

public sealed class ResultEndpointFilter(IResultErrorMapper? mapper = null) : IEndpointFilter
public sealed class ResultEndpointFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
object? result = await next(context);
if (result is null)
return result;

IResultErrorMapper? mapper = context.HttpContext.RequestServices?.GetService<IResultErrorMapper>();
Type resultType = result.GetType();
if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Result<>))
{
Expand Down
44 changes: 42 additions & 2 deletions CSharpEssentials.Tests/AspNetCore/ResultEndpointFilterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.DependencyInjection;

namespace CSharpEssentials.Tests.AspNetCore;

Expand All @@ -25,7 +26,7 @@ public async Task InvokeAsync_WithSuccessResultT_Should_Return_Ok()
public async Task InvokeAsync_WithFailureResultT_Should_Return_BadRequest()
{
var filter = new ResultEndpointFilter();
var context = new DefaultEndpointFilterInvocationContext(new DefaultHttpContext());
var context = CreateContext();

object result = (await filter.InvokeAsync(context, _ => new ValueTask<object?>(Result<int>.Failure(Error.NotFound("X", "Missing")))))!;

Expand All @@ -48,7 +49,7 @@ public async Task InvokeAsync_WithSuccessResult_Should_Return_Ok()
public async Task InvokeAsync_WithFailureResult_Should_Return_BadRequest()
{
var filter = new ResultEndpointFilter();
var context = new DefaultEndpointFilterInvocationContext(new DefaultHttpContext());
var context = CreateContext();

object result = (await filter.InvokeAsync(context, _ => new ValueTask<object?>(Result.Failure(Error.Validation("V", "Invalid")))))!;

Expand Down Expand Up @@ -77,4 +78,43 @@ public async Task InvokeAsync_WithPlainObject_Should_Return_Object()

result.Should().Be("hello");
}

[Fact]
public async Task InvokeAsync_WithFailureResultTAndRegisteredMapper_Should_ResolveMapperFromRequestServices()
{
var filter = new ResultEndpointFilter();
var context = CreateContext(new ServiceCollection()
.AddSingleton<IResultErrorMapper, TestResultErrorMapper>()
.BuildServiceProvider());

object result = (await filter.InvokeAsync(context, _ => new ValueTask<object?>(Result<int>.Failure(Error.NotFound("X", "Missing")))))!;

var notFound = (NotFound<Error[]>)result;
notFound.Value![0].Type.Should().Be(ErrorType.NotFound);
}

[Fact]
public async Task InvokeAsync_WithFailureResultAndRegisteredMapper_Should_ResolveMapperFromRequestServices()
{
var filter = new ResultEndpointFilter();
var context = CreateContext(new ServiceCollection()
.AddSingleton<IResultErrorMapper, TestResultErrorMapper>()
.BuildServiceProvider());

object result = (await filter.InvokeAsync(context, _ => new ValueTask<object?>(Result.Failure(Error.Validation("V", "Invalid")))))!;

var notFound = (NotFound<Error[]>)result;
notFound.Value![0].Type.Should().Be(ErrorType.Validation);
}

private static DefaultEndpointFilterInvocationContext CreateContext(IServiceProvider? services = null)
=> new(new DefaultHttpContext
{
RequestServices = services ?? new ServiceCollection().BuildServiceProvider()
});

private sealed class TestResultErrorMapper : IResultErrorMapper
{
public Microsoft.AspNetCore.Http.IResult Map(Error[] errors) => TypedResults.NotFound(errors);
}
}
Loading