diff --git a/CSharpEssentials.AspNetCore/Readme.MD b/CSharpEssentials.AspNetCore/Readme.MD index 5dafb03..f31c03f 100644 --- a/CSharpEssentials.AspNetCore/Readme.MD +++ b/CSharpEssentials.AspNetCore/Readme.MD @@ -50,12 +50,12 @@ app.UseVersionableSwagger(); ### Result Endpoint Filter ```csharp -builder.Services.AddScoped(); +builder.Services.AddSingleton(); // Optional app.MapGet("/users/{id}", (int id) => GetUser(id)) .AddEndpointFilter(); -// 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 diff --git a/CSharpEssentials.AspNetCore/ResultEndpointFilter/ResultEndpointFilter.cs b/CSharpEssentials.AspNetCore/ResultEndpointFilter/ResultEndpointFilter.cs index d7bd6ab..df6f4e3 100644 --- a/CSharpEssentials.AspNetCore/ResultEndpointFilter/ResultEndpointFilter.cs +++ b/CSharpEssentials.AspNetCore/ResultEndpointFilter/ResultEndpointFilter.cs @@ -2,10 +2,11 @@ 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 InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { @@ -13,6 +14,7 @@ public sealed class ResultEndpointFilter(IResultErrorMapper? mapper = null) : IE if (result is null) return result; + IResultErrorMapper? mapper = context.HttpContext.RequestServices?.GetService(); Type resultType = result.GetType(); if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Result<>)) { diff --git a/CSharpEssentials.Tests/AspNetCore/ResultEndpointFilterTests.cs b/CSharpEssentials.Tests/AspNetCore/ResultEndpointFilterTests.cs index f90be46..76e2251 100644 --- a/CSharpEssentials.Tests/AspNetCore/ResultEndpointFilterTests.cs +++ b/CSharpEssentials.Tests/AspNetCore/ResultEndpointFilterTests.cs @@ -4,6 +4,7 @@ using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.Extensions.DependencyInjection; namespace CSharpEssentials.Tests.AspNetCore; @@ -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(Result.Failure(Error.NotFound("X", "Missing")))))!; @@ -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(Result.Failure(Error.Validation("V", "Invalid")))))!; @@ -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() + .BuildServiceProvider()); + + object result = (await filter.InvokeAsync(context, _ => new ValueTask(Result.Failure(Error.NotFound("X", "Missing")))))!; + + var notFound = (NotFound)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() + .BuildServiceProvider()); + + object result = (await filter.InvokeAsync(context, _ => new ValueTask(Result.Failure(Error.Validation("V", "Invalid")))))!; + + var notFound = (NotFound)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); + } }