Skip to content

Commit

Permalink
Can use ProblemDetails with message handlers as well. Can use message…
Browse files Browse the repository at this point in the history
… handlers as HTTP endpoints. Closes GH-1069
  • Loading branch information
jeremydmiller committed Oct 15, 2024
1 parent afdb370 commit 8e8a096
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 16 deletions.
10 changes: 10 additions & 0 deletions docs/guide/http/problemdetails.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,13 @@ public static ProblemDetails Before(IShipOrder command, Order order)
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Marten/Orders.cs#L87-L101' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_before_on_http_aggregate' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Within Message Handlers <Badge type="tip" text="3.0" />

`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.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Wolverine.Tracking;
using WolverineWebApi;

namespace Wolverine.Http.Tests;
Expand Down Expand Up @@ -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()
Expand All @@ -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<NumberMessage>()
.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<NumberMessage>()
.Number.ShouldBe(10);

NumberMessageHandler.Handled.ShouldBeFalse();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -39,13 +56,13 @@ public MaybeEndWithProblemDetailsFrame(Variable details)
uses.Add(details);
_details = details;
}

public override IEnumerable<Variable> 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");
Expand All @@ -55,6 +72,40 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
writer.FinishBlock();
writer.BlankLine();

Next?.GenerateCode(method, writer);
}
}

/// <summary>
/// Used to potentially stop the execution of an Http request
/// based on whether the IResult is a WolverineContinue or something else
/// </summary>
public class MaybeEndHandlerWithProblemDetailsFrame : AsyncFrame
{
private readonly Variable _details;
private Variable? _logger;

public MaybeEndHandlerWithProblemDetailsFrame(Variable details)
{
uses.Add(details);
_details = details;
}

public override IEnumerable<Variable> 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);
}
}
3 changes: 2 additions & 1 deletion src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IResult>());

Expand Down
35 changes: 34 additions & 1 deletion src/Http/WolverineWebApi/ProblemDetailsUsage.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Wolverine.Http;

Expand Down Expand Up @@ -31,4 +32,36 @@ public static string Post(NumberMessage message)

public record NumberMessage(int Number);

#endregion
#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; }
}
1 change: 1 addition & 0 deletions src/Http/WolverineWebApi/Validation/ValidatedEndpoint.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using FluentValidation;
using Wolverine.Http;

Expand Down
1 change: 1 addition & 0 deletions src/Samples/Diagnostics/DiagnosticsApp/Invoices.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Wolverine;
using Wolverine.Attributes;
using Wolverine.Configuration;
using Wolverine.ErrorHandling;
using Wolverine.Runtime.Handlers;

Expand Down
2 changes: 1 addition & 1 deletion src/Wolverine/Configuration/Chain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!);
}
Expand Down
10 changes: 6 additions & 4 deletions src/Wolverine/Middleware/ContinuationHandling.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using Wolverine.Configuration;

namespace Wolverine.Middleware;

Expand Down Expand Up @@ -41,11 +42,12 @@ public static List<IContinuationStrategy> 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;
}
Expand All @@ -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<HandlerContinuation>())
{
Expand Down
8 changes: 4 additions & 4 deletions src/Wolverine/Middleware/MiddlewarePolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,19 +143,19 @@ public IEnumerable<Frame> BuildBeforeCalls(IChain chain, GenerationRules rules)
foreach (var frame in frames) yield return frame;
}

private IEnumerable<Frame> wrapBeforeFrame(MethodCall call, GenerationRules rules)
private IEnumerable<Frame> 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;
}
Expand Down Expand Up @@ -209,7 +209,7 @@ private IEnumerable<Frame> 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;
}
}
}
Expand Down

0 comments on commit 8e8a096

Please sign in to comment.