From f6c5bfcac3746f76c7a7469909150bb7955a8200 Mon Sep 17 00:00:00 2001 From: Erdem Date: Sun, 31 May 2026 16:40:28 +0300 Subject: [PATCH] fix(aspnetcore): resolve result error mapper per request --- CSharpEssentials.AspNetCore/Readme.MD | 4 +- .../ResultEndpointFilter.cs | 4 +- .../AspNetCore/ResultEndpointFilterTests.cs | 44 ++++++++++++++++++- README.MD | 2 +- 4 files changed, 48 insertions(+), 6 deletions(-) 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); + } } diff --git a/README.MD b/README.MD index fc55651..0bf7c84 100644 --- a/README.MD +++ b/README.MD @@ -6,7 +6,7 @@ [![Build](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml/badge.svg)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) -[![Tests](https://img.shields.io/badge/tests-2951%20passing-brightgreen)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) +[![Tests](https://img.shields.io/badge/tests-2953%20passing-brightgreen)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.svg)](https://www.nuget.org/packages/CSharpEssentials) [![Downloads](https://img.shields.io/nuget/dt/CSharpEssentials.svg)](https://www.nuget.org/packages/CSharpEssentials) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/senrecep/CSharpEssentials/blob/main/LICENCE)