diff --git a/docs/guide/http/problemdetails.md b/docs/guide/http/problemdetails.md index 2bec4ec7e..a4378f154 100644 --- a/docs/guide/http/problemdetails.md +++ b/docs/guide/http/problemdetails.md @@ -139,3 +139,13 @@ public static ProblemDetails Before(IShipOrder command, Order order) ``` snippet source | anchor + +## Within Message Handlers + +`ProblemDetails` can be used within message handlers as well with similar rules. See this example +from the tests: + +snippet: sample_using_problem_details_in_message_handler + +This functionality was added so that some handlers could be both an endpoint and message handler +without having to duplicate code or delegate to the handler through an endpoint. \ No newline at end of file diff --git a/src/Http/Wolverine.Http.Tests/problem_details_usage_in_http_middleware.cs b/src/Http/Wolverine.Http.Tests/problem_details_usage_in_http_middleware.cs index e4aa50c84..48e1e45fb 100644 --- a/src/Http/Wolverine.Http.Tests/problem_details_usage_in_http_middleware.cs +++ b/src/Http/Wolverine.Http.Tests/problem_details_usage_in_http_middleware.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Shouldly; +using Wolverine.Tracking; using WolverineWebApi; namespace Wolverine.Http.Tests; @@ -40,6 +41,30 @@ public async Task stop_with_problems_if_middleware_trips_off() } #endregion + + [Fact] + public async Task continue_happy_path_as_handler_acting_as_endpoint() + { + // Should be good + await Scenario(x => + { + x.Post.Json(new NumberMessage(3)).ToUrl("/problems2"); + x.StatusCodeShouldBe(204); + }); + } + + [Fact] + public async Task stop_with_problems_if_middleware_trips_off_in_handler_acting_as_endpoint() + { + // This is the "sad path" that should spawn a ProblemDetails + // object + var result = await Scenario(x => + { + x.Post.Json(new NumberMessage(10)).ToUrl("/problems2"); + x.StatusCodeShouldBe(400); + x.ContentTypeShouldBe("application/problem+json"); + }); + } [Fact] public void adds_default_problem_details_to_open_api_metadata() @@ -53,4 +78,28 @@ public void adds_default_problem_details_to_open_api_metadata() produces.StatusCode.ShouldBe(400); produces.ContentTypes.Single().ShouldBe("application/problem+json"); } + + [Fact] + public async Task problem_details_in_message_handlers_positive() + { + NumberMessageHandler.Handled = false; + + var tracked = await Host.InvokeMessageAndWaitAsync(new NumberMessage(3)); + tracked.Executed.SingleMessage() + .Number.ShouldBe(3); + + NumberMessageHandler.Handled.ShouldBeTrue(); + } + + [Fact] + public async Task problem_details_in_message_handlers_negative() + { + NumberMessageHandler.Handled = false; + + var tracked = await Host.InvokeMessageAndWaitAsync(new NumberMessage(10)); + tracked.Executed.SingleMessage() + .Number.ShouldBe(10); + + NumberMessageHandler.Handled.ShouldBeFalse(); + } } \ No newline at end of file diff --git a/src/Http/Wolverine.Http/CodeGen/ProblemDetailsContinuationPolicy.cs b/src/Http/Wolverine.Http/CodeGen/ProblemDetailsContinuationPolicy.cs index 19e062d97..71d39876f 100644 --- a/src/Http/Wolverine.Http/CodeGen/ProblemDetailsContinuationPolicy.cs +++ b/src/Http/Wolverine.Http/CodeGen/ProblemDetailsContinuationPolicy.cs @@ -4,19 +4,36 @@ using JasperFx.Core.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Wolverine.Configuration; using Wolverine.Middleware; namespace Wolverine.Http.CodeGen; -internal class ProblemDetailsContinuationPolicy : IContinuationStrategy +public class ProblemDetailsContinuationPolicy : IContinuationStrategy { - public bool TryFindContinuationHandler(MethodCall call, out Frame? frame) + public static void WriteProblems(ILogger logger, ProblemDetails details) + { + var json = JsonConvert.SerializeObject(details, Formatting.None); + logger.LogInformation("Found problems with this message: {Problems}", json); + } + + public bool TryFindContinuationHandler(IChain chain, MethodCall call, out Frame? frame) { var details = call.Creates.FirstOrDefault(x => x.VariableType == typeof(ProblemDetails)); if (details != null) { - frame = new MaybeEndWithProblemDetailsFrame(details); + if (chain is HttpChain) + { + frame = new MaybeEndWithProblemDetailsFrame(details); + } + else + { + frame = new MaybeEndHandlerWithProblemDetailsFrame(details); + } + return true; } @@ -39,13 +56,13 @@ public MaybeEndWithProblemDetailsFrame(Variable details) uses.Add(details); _details = details; } - + public override IEnumerable FindVariables(IMethodVariables chain) { _context = chain.FindVariable(typeof(HttpContext)); yield return _context; } - + public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) { writer.WriteComment("Evaluate whether the processing should stop if there are any problems"); @@ -55,6 +72,40 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) writer.FinishBlock(); writer.BlankLine(); + Next?.GenerateCode(method, writer); + } +} + +/// +/// Used to potentially stop the execution of an Http request +/// based on whether the IResult is a WolverineContinue or something else +/// +public class MaybeEndHandlerWithProblemDetailsFrame : AsyncFrame +{ + private readonly Variable _details; + private Variable? _logger; + + public MaybeEndHandlerWithProblemDetailsFrame(Variable details) + { + uses.Add(details); + _details = details; + } + + public override IEnumerable FindVariables(IMethodVariables chain) + { + _logger = chain.FindVariable(typeof(ILogger)); + yield return _logger; + } + + public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) + { + writer.WriteComment("Evaluate whether the processing should stop if there are any problems"); + writer.Write($"BLOCK:if (!(ReferenceEquals({_details.Usage}, {typeof(WolverineContinue).FullNameInCode()}.{nameof(WolverineContinue.NoProblems)})))"); + writer.Write($"{typeof(ProblemDetailsContinuationPolicy).FullNameInCode()}.{nameof(ProblemDetailsContinuationPolicy.WriteProblems)}({_logger.Usage}, {_details.Usage});"); + writer.Write("return;"); + writer.FinishBlock(); + writer.BlankLine(); + Next?.GenerateCode(method, writer); } } \ 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 1e0423f57..2604d5823 100644 --- a/src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs +++ b/src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs @@ -3,13 +3,14 @@ using JasperFx.CodeGeneration.Model; using JasperFx.Core.Reflection; using Microsoft.AspNetCore.Http; +using Wolverine.Configuration; using Wolverine.Middleware; namespace Wolverine.Http.CodeGen; internal class ResultContinuationPolicy : IContinuationStrategy { - public bool TryFindContinuationHandler(MethodCall call, out Frame? frame) + public bool TryFindContinuationHandler(IChain chain, MethodCall call, out Frame? frame) { var result = call.Creates.FirstOrDefault(x => x.VariableType.CanBeCastTo()); diff --git a/src/Http/WolverineWebApi/ProblemDetailsUsage.cs b/src/Http/WolverineWebApi/ProblemDetailsUsage.cs index 9e28b66af..7b0d23b8d 100644 --- a/src/Http/WolverineWebApi/ProblemDetailsUsage.cs +++ b/src/Http/WolverineWebApi/ProblemDetailsUsage.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Microsoft.AspNetCore.Mvc; using Wolverine.Http; @@ -31,4 +32,36 @@ public static string Post(NumberMessage message) public record NumberMessage(int Number); -#endregion \ No newline at end of file +#endregion + +public static class NumberMessageHandler +{ + #region sample_using_problem_details_in_message_handler + + public static ProblemDetails Validate(NumberMessage message) + { + if (message.Number > 5) + { + return new ProblemDetails + { + Detail = "Number is bigger than 5", + Status = 400 + }; + } + + // All good, keep on going! + return WolverineContinue.NoProblems; + } + + // Look at this! You can use this as an HTTP endpoint too! + [WolverinePost("/problems2")] + public static void Handle(NumberMessage message) + { + Debug.WriteLine("Handled " + message); + Handled = true; + } + + #endregion + + public static bool Handled { get; set; } +} \ No newline at end of file diff --git a/src/Http/WolverineWebApi/Validation/ValidatedEndpoint.cs b/src/Http/WolverineWebApi/Validation/ValidatedEndpoint.cs index c7fd7d794..225a58fb2 100644 --- a/src/Http/WolverineWebApi/Validation/ValidatedEndpoint.cs +++ b/src/Http/WolverineWebApi/Validation/ValidatedEndpoint.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using FluentValidation; using Wolverine.Http; diff --git a/src/Samples/Diagnostics/DiagnosticsApp/Invoices.cs b/src/Samples/Diagnostics/DiagnosticsApp/Invoices.cs index 56b9309bf..560f6fe08 100644 --- a/src/Samples/Diagnostics/DiagnosticsApp/Invoices.cs +++ b/src/Samples/Diagnostics/DiagnosticsApp/Invoices.cs @@ -1,5 +1,6 @@ using Wolverine; using Wolverine.Attributes; +using Wolverine.Configuration; using Wolverine.ErrorHandling; using Wolverine.Runtime.Handlers; diff --git a/src/Wolverine/Configuration/Chain.cs b/src/Wolverine/Configuration/Chain.cs index d9653c8d6..1036a6795 100644 --- a/src/Wolverine/Configuration/Chain.cs +++ b/src/Wolverine/Configuration/Chain.cs @@ -238,7 +238,7 @@ protected void applyImpliedMiddlewareFromHandlers(GenerationRules generationRule Middleware.Add(frame); // Potentially add handling for IResult or HandlerContinuation - if (generationRules.TryFindContinuationHandler(frame, out var continuation)) + if (generationRules.TryFindContinuationHandler(this, frame, out var continuation)) { Middleware.Add(continuation!); } diff --git a/src/Wolverine/Middleware/ContinuationHandling.cs b/src/Wolverine/Middleware/ContinuationHandling.cs index 3da65f650..4db9d626e 100644 --- a/src/Wolverine/Middleware/ContinuationHandling.cs +++ b/src/Wolverine/Middleware/ContinuationHandling.cs @@ -1,5 +1,6 @@ using JasperFx.CodeGeneration; using JasperFx.CodeGeneration.Frames; +using Wolverine.Configuration; namespace Wolverine.Middleware; @@ -41,11 +42,12 @@ public static List ContinuationStrategies(this Generation } } - public static bool TryFindContinuationHandler(this GenerationRules rules, MethodCall call, out Frame? frame) + public static bool TryFindContinuationHandler(this GenerationRules rules, IChain chain, MethodCall call, + out Frame? frame) { var strategies = rules.ContinuationStrategies(); foreach (var strategy in strategies) - if (strategy.TryFindContinuationHandler(call, out frame)) + if (strategy.TryFindContinuationHandler(chain, call, out frame)) { return true; } @@ -57,12 +59,12 @@ public static bool TryFindContinuationHandler(this GenerationRules rules, Method public interface IContinuationStrategy { - bool TryFindContinuationHandler(MethodCall call, out Frame? frame); + bool TryFindContinuationHandler(IChain chain, MethodCall call, out Frame? frame); } internal class HandlerContinuationPolicy : IContinuationStrategy { - public bool TryFindContinuationHandler(MethodCall call, out Frame? frame) + public bool TryFindContinuationHandler(IChain chain, MethodCall call, out Frame? frame) { if (call.CreatesNewOf()) { diff --git a/src/Wolverine/Middleware/MiddlewarePolicy.cs b/src/Wolverine/Middleware/MiddlewarePolicy.cs index d03a3ac67..c244a1f27 100644 --- a/src/Wolverine/Middleware/MiddlewarePolicy.cs +++ b/src/Wolverine/Middleware/MiddlewarePolicy.cs @@ -143,19 +143,19 @@ public IEnumerable BuildBeforeCalls(IChain chain, GenerationRules rules) foreach (var frame in frames) yield return frame; } - private IEnumerable wrapBeforeFrame(MethodCall call, GenerationRules rules) + private IEnumerable wrapBeforeFrame(IChain chain, MethodCall call, GenerationRules rules) { if (_finals.Length == 0) { yield return call; - if (rules.TryFindContinuationHandler(call, out var frame)) + if (rules.TryFindContinuationHandler(chain, call, out var frame)) { yield return frame!; } } else { - if (rules.TryFindContinuationHandler(call, out var frame)) + if (rules.TryFindContinuationHandler(chain, call, out var frame)) { call.Next = frame; } @@ -209,7 +209,7 @@ private IEnumerable buildBefores(IChain chain, GenerationRules rules) { AssertMethodDoesNotHaveDuplicateReturnValues(call); - foreach (var frame in wrapBeforeFrame(call, rules)) yield return frame; + foreach (var frame in wrapBeforeFrame(chain, call, rules)) yield return frame; } } }