Skip to content

Commit bb6cc04

Browse files
committed
Ability to limit the scope of Before/After methods on combo handler/http endpoints. Closes GH-1601
1 parent 2146389 commit bb6cc04

File tree

7 files changed

+115
-20
lines changed

7 files changed

+115
-20
lines changed

docs/guide/http/endpoints.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,5 +336,34 @@ public static string GetNow(DateTimeOffset now) // using the custom parameter st
336336
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/CustomParameterEndpoint.cs#L7-L15' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_http_endpoint_receiving_now' title='Start of snippet'>anchor</a></sup>
337337
<!-- endSnippet -->
338338

339+
## Http Endpoint / Message Handler Combo
340+
341+
Here's a common scenario that has come up from Wolverine users. Let's say that you have some kind of logical command message that
342+
your system needs to handle that might come in from the outside from either HTTP clients or from asynchronous messaging.
343+
Folks have frequently asked about how to reuse code between the message handling invocation and the HTTP endpoint. You've
344+
got a handful of options:
345+
346+
1. Build a message handler and have the HTTP endpoint just delegate to `IMessageBus.InvokeAsync()` with the message
347+
2. Have both the message handler and HTTP endpoint delegate to shared code, whether that be a shared service, just a static method somewhere, or even
348+
have the HTTP endpoint code directly call the concrete message handler
349+
3. Use a hybrid Message Handler / HTTP Endpoint because Wolverine can do that!
350+
351+
To make a single class and method be both a message handler and HTTP endpoint, just add a `[Wolverine{HttpVerb}]` attribute
352+
with the route directly on your message handler. As long as that method follows Wolverine's normal naming rules for message
353+
discovery, Wolverine will treat it as both a message handler and as an HTTP endpoint. Here's an example from our tests:
354+
355+
snippet: sample_using_problem_details_in_message_handler
356+
357+
If you are using Wolverine.HTTP in your application, Wolverine is able to treat `ProblemDetails` similar to the built in
358+
`HandlerContinuation` when running inside of message handlers.
359+
360+
If you have some middleware methods that should only apply specifically when running as a handler or when running as an HTTP endpoint,
361+
you can utilize `MiddlewareScoping` directives with `[WolverineBefore]`, `[WolverineAfter]`, or `[WolverineFinally]` attributes to
362+
limit the applicability of individual middleware methods.
363+
364+
::: info
365+
There is no runtime filtering here because the `MiddlewareScoping` impacts the generated code around your hybrid message handler /
366+
HTTP endpoint method, and Wolverine already generates code separately for the two use cases.
367+
:::
339368

340369

src/Http/Wolverine.Http.Tests/combo_handler_and_endpoint.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,42 @@ public combo_handler_and_endpoint(AppFixture fixture) : base(fixture)
1414
[Fact]
1515
public async Task use_combo_with_problem_details_as_endpoint()
1616
{
17+
// cleaning up expected change
18+
NumberMessageHandler.CalledBeforeOnlyOnHttpEndpoints = false;
19+
NumberMessageHandler.CalledBeforeOnlyOnMessageHandlers = false;
20+
1721
// Should be good
1822
await Host.Scenario(x =>
1923
{
20-
x.Post.Json(new WolverineWebApi.NumberMessage(3)).ToUrl("/problems");
24+
x.Post.Json(new WolverineWebApi.NumberMessage(3)).ToUrl("/problems2");
25+
x.StatusCodeShouldBe(204);
2126
});
2227

28+
NumberMessageHandler.CalledBeforeOnlyOnHttpEndpoints.ShouldBeTrue();
29+
NumberMessageHandler.CalledBeforeOnlyOnMessageHandlers.ShouldBeFalse();
30+
2331
// should return problem details because the number > 5
2432
// Should be good
2533
var result = await Host.Scenario(x =>
2634
{
27-
x.Post.Json(new WolverineWebApi.NumberMessage(10)).ToUrl("/problems");
35+
x.Post.Json(new WolverineWebApi.NumberMessage(10)).ToUrl("/problems2");
2836
x.ContentTypeShouldBe("application/problem+json");
2937
x.StatusCodeShouldBe(400);
3038
});
3139

3240
var details = result.ReadAsJson<ProblemDetails>();
3341

3442
details.Detail.ShouldBe("Number is bigger than 5");
43+
44+
3545
}
3646

3747
[Fact]
3848
public async Task use_combo_as_handler_see_problem_details_catch()
3949
{
50+
// cleaning up expected change
51+
NumberMessageHandler.CalledBeforeOnlyOnHttpEndpoints = false;
52+
NumberMessageHandler.CalledBeforeOnlyOnMessageHandlers = false;
4053
NumberMessageHandler.Handled = false;
4154

4255
// This should be invalid and stop
@@ -46,5 +59,8 @@ public async Task use_combo_as_handler_see_problem_details_catch()
4659
// should be good because we're < 5
4760
await Host.InvokeAsync(new NumberMessage(3));
4861
NumberMessageHandler.Handled.ShouldBeTrue();
62+
63+
NumberMessageHandler.CalledBeforeOnlyOnHttpEndpoints.ShouldBeFalse();
64+
NumberMessageHandler.CalledBeforeOnlyOnMessageHandlers.ShouldBeTrue();
4965
}
5066
}

src/Http/WolverineWebApi/ProblemDetailsUsage.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Diagnostics;
22
using Microsoft.AspNetCore.Mvc;
3+
using Wolverine.Attributes;
34
using Wolverine.Http;
45

56
namespace WolverineWebApi;
@@ -34,10 +35,10 @@ public record NumberMessage(int Number);
3435

3536
#endregion
3637

38+
#region sample_using_problem_details_in_message_handler
39+
3740
public static class NumberMessageHandler
3841
{
39-
#region sample_using_problem_details_in_message_handler
40-
4142
public static ProblemDetails Validate(NumberMessage message)
4243
{
4344
if (message.Number > 5)
@@ -52,7 +53,24 @@ public static ProblemDetails Validate(NumberMessage message)
5253
// All good, keep on going!
5354
return WolverineContinue.NoProblems;
5455
}
55-
56+
57+
// This "Before" method would only be utilized as
58+
// an HTTP endpoint
59+
[WolverineBefore(MiddlewareScoping.HttpEndpoints)]
60+
public static void BeforeButOnlyOnHttp(HttpContext context)
61+
{
62+
Debug.WriteLine("Got an HTTP request for " + context.TraceIdentifier);
63+
CalledBeforeOnlyOnHttpEndpoints = true;
64+
}
65+
66+
// This "Before" method would only be utilized as
67+
// a message handler
68+
[WolverineBefore(MiddlewareScoping.MessageHandlers)]
69+
public static void BeforeButOnlyOnMessageHandlers()
70+
{
71+
CalledBeforeOnlyOnMessageHandlers = true;
72+
}
73+
5674
// Look at this! You can use this as an HTTP endpoint too!
5775
[WolverinePost("/problems2")]
5876
public static void Handle(NumberMessage message)
@@ -61,7 +79,10 @@ public static void Handle(NumberMessage message)
6179
Handled = true;
6280
}
6381

64-
#endregion
6582

83+
// These properties are just a cheap trick in Wolverine internal tests
6684
public static bool Handled { get; set; }
67-
}
85+
public static bool CalledBeforeOnlyOnMessageHandlers { get; set; }
86+
public static bool CalledBeforeOnlyOnHttpEndpoints { get; set; }
87+
}
88+
#endregion

src/Wolverine/Attributes/MiddlewareAttribute.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@ public MiddlewareAttribute(params Type[] frameTypes)
1717
_frameTypes = frameTypes;
1818
}
1919

20+
/// <summary>
21+
/// Use this to optionally restrict the application of this middleware to only message handlers
22+
/// or HTTP endpoints. Default is Anywhere
23+
/// </summary>
24+
public MiddlewareScoping Scoping { get; set; } = MiddlewareScoping.Anywhere;
25+
2026
public override void Modify(IChain chain, GenerationRules rules, IServiceContainer container)
2127
{
22-
var applications = _frameTypes.Select(type => new MiddlewarePolicy.Application(type, _ => true)).ToList();
28+
var applications = _frameTypes.Select(type => new MiddlewarePolicy.Application(chain, type, _ => true)).ToList();
2329
MiddlewarePolicy.ApplyToChain(applications, rules, chain);
2430
}
2531
}

src/Wolverine/Configuration/Chain.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ public void ApplyImpliedMiddlewareFromHandlers(GenerationRules generationRules)
274274
var handlerTypes = HandlerCalls().Select(x => x.HandlerType).Distinct();
275275
foreach (var handlerType in handlerTypes)
276276
{
277-
var befores = MiddlewarePolicy.FilterMethods<WolverineBeforeAttribute>(handlerType.GetMethods(),
277+
var befores = MiddlewarePolicy.FilterMethods<WolverineBeforeAttribute>(this, handlerType.GetMethods(),
278278
MiddlewarePolicy.BeforeMethodNames);
279279

280280
foreach (var before in befores)
@@ -301,7 +301,7 @@ public void ApplyImpliedMiddlewareFromHandlers(GenerationRules generationRules)
301301
}
302302
}
303303

304-
var afters = MiddlewarePolicy.FilterMethods<WolverineAfterAttribute>(handlerType.GetMethods(),
304+
var afters = MiddlewarePolicy.FilterMethods<WolverineAfterAttribute>(this, handlerType.GetMethods(),
305305
MiddlewarePolicy.AfterMethodNames).ToArray();
306306

307307
if (afters.Any())

src/Wolverine/Configuration/IChain.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@
1010

1111
namespace Wolverine.Configuration;
1212

13+
internal static class ChainExtensions
14+
{
15+
public static bool MatchesScope(this IChain chain, MethodInfo method)
16+
{
17+
if (chain == null) return true;
18+
19+
if (method.TryGetAttribute<ScopedMiddlewareAttribute>(out var att))
20+
{
21+
if (att.Scoping == MiddlewareScoping.Anywhere) return true;
22+
23+
return att.Scoping == chain.Scoping;
24+
}
25+
26+
// All good if no attribute
27+
return true;
28+
}
29+
}
30+
1331
#region sample_IChain
1432

1533
/// <summary>

src/Wolverine/Middleware/MiddlewarePolicy.cs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,33 +74,38 @@ internal static void ApplyToChain(List<Application> applications, GenerationRule
7474
public Application AddType(Type middlewareType, Func<IChain, bool>? filter = null)
7575
{
7676
filter ??= _ => true;
77-
var application = new Application(middlewareType, filter);
77+
var application = new Application(null, middlewareType, filter);
7878
_applications.Add(application);
7979
return application;
8080
}
8181

82-
public static IEnumerable<MethodInfo> FilterMethods<T>(IEnumerable<MethodInfo> methods, string[] validNames)
83-
where T : Attribute
82+
public static IEnumerable<MethodInfo> FilterMethods<T>(IChain? chain, IEnumerable<MethodInfo> methods,
83+
string[] validNames)
84+
where T : ScopedMiddlewareAttribute
8485
{
86+
// MatchesScope watches out for null chain
8587
return methods
86-
.Where(x => !x.HasAttribute<WolverineIgnoreAttribute>())
88+
.Where(x => !x.HasAttribute<WolverineIgnoreAttribute>() && chain!.MatchesScope(x))
8789
.Where(x => validNames.Contains(x.Name) || x.HasAttribute<T>());
8890
}
8991

9092
public class Application
9193
{
94+
private readonly IChain? _chain;
9295
private readonly MethodInfo[] _afters;
9396
private readonly MethodInfo[] _befores;
9497
private readonly ConstructorInfo? _constructor;
9598
private readonly MethodInfo[] _finals;
96-
97-
public Application(Type middlewareType, Func<IChain, bool> filter)
99+
100+
public Application(IChain? chain, Type middlewareType, Func<IChain, bool> filter)
98101
{
99102
if (!middlewareType.IsPublic && !middlewareType.IsVisible)
100103
{
101104
throw new InvalidWolverineMiddlewareException(middlewareType);
102105
}
103106

107+
_chain = chain;
108+
104109
if (!middlewareType.IsStatic())
105110
{
106111
var constructors = middlewareType.GetConstructors();
@@ -115,11 +120,11 @@ public Application(Type middlewareType, Func<IChain, bool> filter)
115120
MiddlewareType = middlewareType;
116121
Filter = filter;
117122

118-
var methods = middlewareType.GetMethods().ToArray();
123+
var methods = middlewareType.GetMethods().Where(x => x.DeclaringType != typeof(object)).ToArray();
119124

120-
_befores = FilterMethods<WolverineBeforeAttribute>(methods, BeforeMethodNames).ToArray();
121-
_afters = FilterMethods<WolverineAfterAttribute>(methods, AfterMethodNames).ToArray();
122-
_finals = FilterMethods<WolverineFinallyAttribute>(methods, FinallyMethodNames).ToArray();
125+
_befores = FilterMethods<WolverineBeforeAttribute>(chain, methods, BeforeMethodNames).ToArray();
126+
_afters = FilterMethods<WolverineAfterAttribute>(chain, methods, AfterMethodNames).ToArray();
127+
_finals = FilterMethods<WolverineFinallyAttribute>(chain, methods, FinallyMethodNames).ToArray();
123128

124129
if (_befores.Length == 0 &&
125130
_afters.Length == 0 &&

0 commit comments

Comments
 (0)