diff --git a/docs/guide/http/metadata.md b/docs/guide/http/metadata.md index 4f5e9cf08..c00fa5764 100644 --- a/docs/guide/http/metadata.md +++ b/docs/guide/http/metadata.md @@ -168,3 +168,9 @@ Any endpoint that returns `CreationResponse` or a sub class will automatically e processing to denote resource creation instead of the generic `200`. Same goes for the built-in `AcceptResponse` type, but returning `202` status. Your own custom implementations of the `IHttpAware` interface would apply the metadata declarations at configuration time so that those customizations would be part of the exported Swashbuckle documentation of the system. + +As of Wolverine 3.4, Wolverine will also apply OpenAPI metadata from any value created by compound handler middleware +or other middleware that implements the `IEndpointMetadataProvider` interface -- which many `IResult` implementations +from within ASP.Net Core middleware do. Consider this example from the tests: + +snippet: sample_using_optional_iresult_with_openapi_metadata diff --git a/docs/guide/http/middleware.md b/docs/guide/http/middleware.md index c5740c4d4..130770066 100644 --- a/docs/guide/http/middleware.md +++ b/docs/guide/http/middleware.md @@ -82,6 +82,11 @@ public static async Task ExecuteOne(IValidator validator, IProble snippet source | anchor +Likewise, you can also just return a `null` from middleware for `IResult` and Wolverine will interpret that as +"just continue" as shown in this sample: + +snippet: sample_using_optional_iresult_with_openapi_metadata + ## Using Configure(chain) Methods You can make explicit modifications to HTTP processing for middleware or OpenAPI metadata for a single endpoint (really all diff --git a/src/Http/Wolverine.Http.Tests/using_IResult_in_endpoints.cs b/src/Http/Wolverine.Http.Tests/using_IResult_in_endpoints.cs index adef2046d..0559c3e58 100644 --- a/src/Http/Wolverine.Http.Tests/using_IResult_in_endpoints.cs +++ b/src/Http/Wolverine.Http.Tests/using_IResult_in_endpoints.cs @@ -1,4 +1,5 @@ using Shouldly; +using WolverineWebApi.Validation; namespace Wolverine.Http.Tests; @@ -31,4 +32,26 @@ public async Task use_as_return_value_async() result.ReadAsText().ShouldBe("Hello from async result"); } + + [Fact] + public async Task using_optional_result_in_middleware_when_result_is_null() + { + var result = await Scenario(x => + { + x.Delete.Json(new BlockUser2("one")).ToUrl("/optional/result"); + x.Header("content-type").SingleValueShouldEqual("text/plain"); + }); + + result.ReadAsText().ShouldBe("Ok - user blocked"); + } + + [Fact] + public async Task using_optional_result_in_middleware_when_result_is_not_null() + { + var result = await Scenario(x => + { + x.Delete.Json(new BlockUser2(null)).ToUrl("/optional/result"); + x.StatusCodeShouldBe(404); + }); + } } \ No newline at end of file diff --git a/src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs b/src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs index 2604d5823..858987d8f 100644 --- a/src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs +++ b/src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs @@ -49,7 +49,7 @@ public override IEnumerable FindVariables(IMethodVariables chain) public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) { writer.WriteComment("Evaluate whether or not the execution should be stopped based on the IResult value"); - writer.Write($"BLOCK:if (!({_result.Usage} is {typeof(WolverineContinue).FullNameInCode()}))"); + writer.Write($"BLOCK:if ({_result.Usage} != null && !({_result.Usage} is {typeof(WolverineContinue).FullNameInCode()}))"); writer.Write($"await {_result.Usage}.{nameof(IResult.ExecuteAsync)}({_context!.Usage}).ConfigureAwait(false);"); writer.Write("return;"); writer.FinishBlock(); diff --git a/src/Http/Wolverine.Http/HttpChain.EndpointBuilder.cs b/src/Http/Wolverine.Http/HttpChain.EndpointBuilder.cs index a05b294aa..f11718946 100644 --- a/src/Http/Wolverine.Http/HttpChain.EndpointBuilder.cs +++ b/src/Http/Wolverine.Http/HttpChain.EndpointBuilder.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Reflection; using JasperFx.CodeGeneration; using JasperFx.Core; @@ -80,6 +81,11 @@ public RouteEndpoint BuildEndpoint() tryApplyAsEndpointMetadataProvider(parameter.ParameterType, builder); } + foreach (var created in Middleware.SelectMany(x => x.Creates)) + { + tryApplyAsEndpointMetadataProvider(created.VariableType, builder); + } + // Set up OpenAPI data for ProblemDetails with status code 400 if not already exists if (Middleware.SelectMany(x => x.Creates).Any(x => x.VariableType == typeof(ProblemDetails))) { diff --git a/src/Http/WolverineWebApi/ResultEndpoints.cs b/src/Http/WolverineWebApi/ResultEndpoints.cs index 674533b8b..68de7289a 100644 --- a/src/Http/WolverineWebApi/ResultEndpoints.cs +++ b/src/Http/WolverineWebApi/ResultEndpoints.cs @@ -16,4 +16,6 @@ public static Task GetAsyncResult() var result = Microsoft.AspNetCore.Http.Results.Content("Hello from async result", "text/plain"); return Task.FromResult(result); } + + } \ No newline at end of file diff --git a/src/Http/WolverineWebApi/Validation/ValidatedCompoundEndpoint.cs b/src/Http/WolverineWebApi/Validation/ValidatedCompoundEndpoint.cs index b0f436f5a..85042bd52 100644 --- a/src/Http/WolverineWebApi/Validation/ValidatedCompoundEndpoint.cs +++ b/src/Http/WolverineWebApi/Validation/ValidatedCompoundEndpoint.cs @@ -1,4 +1,6 @@ using FluentValidation; +using JasperFx.Core; +using Microsoft.AspNetCore.Http.HttpResults; using Wolverine.Http; namespace WolverineWebApi.Validation; @@ -28,7 +30,38 @@ public static string Handle(BlockUser cmd, User user) } } +#region sample_using_optional_iresult_with_openapi_metadata + +public class ValidatedCompoundEndpoint2 +{ + public static User? Load(BlockUser2 cmd) + { + return cmd.UserId.IsNotEmpty() ? new User(cmd.UserId) : null; + } + + // This method would be called, and if the NotFound value is + // not null, will stop the rest of the processing + // Likewise, Wolverine will use the NotFound type to add + // OpenAPI metadata + public static NotFound? Validate(User? user) + { + if (user == null) + return (NotFound?)Results.NotFound(user); + + return null; + } + + [WolverineDelete("/optional/result")] + public static string Handle(BlockUser2 cmd, User user) + { + return "Ok - user blocked"; + } +} + +#endregion + public record BlockUser(string? UserId); +public record BlockUser2(string? UserId); public class BlockUserValidator : AbstractValidator {