diff --git a/docs/guide/http/endpoints.md b/docs/guide/http/endpoints.md
index f923866bb..bc143ff3b 100644
--- a/docs/guide/http/endpoints.md
+++ b/docs/guide/http/endpoints.md
@@ -336,5 +336,34 @@ public static string GetNow(DateTimeOffset now) // using the custom parameter st
snippet source | anchor
+## Http Endpoint / Message Handler Combo
+
+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
+your system needs to handle that might come in from the outside from either HTTP clients or from asynchronous messaging.
+Folks have frequently asked about how to reuse code between the message handling invocation and the HTTP endpoint. You've
+got a handful of options:
+
+1. Build a message handler and have the HTTP endpoint just delegate to `IMessageBus.InvokeAsync()` with the message
+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
+ have the HTTP endpoint code directly call the concrete message handler
+3. Use a hybrid Message Handler / HTTP Endpoint because Wolverine can do that!
+
+To make a single class and method be both a message handler and HTTP endpoint, just add a `[Wolverine{HttpVerb}]` attribute
+with the route directly on your message handler. As long as that method follows Wolverine's normal naming rules for message
+discovery, Wolverine will treat it as both a message handler and as an HTTP endpoint. Here's an example from our tests:
+
+snippet: sample_using_problem_details_in_message_handler
+
+If you are using Wolverine.HTTP in your application, Wolverine is able to treat `ProblemDetails` similar to the built in
+`HandlerContinuation` when running inside of message handlers.
+
+If you have some middleware methods that should only apply specifically when running as a handler or when running as an HTTP endpoint,
+you can utilize `MiddlewareScoping` directives with `[WolverineBefore]`, `[WolverineAfter]`, or `[WolverineFinally]` attributes to
+limit the applicability of individual middleware methods.
+
+::: info
+There is no runtime filtering here because the `MiddlewareScoping` impacts the generated code around your hybrid message handler /
+HTTP endpoint method, and Wolverine already generates code separately for the two use cases.
+:::
diff --git a/src/Http/Wolverine.Http.Tests/combo_handler_and_endpoint.cs b/src/Http/Wolverine.Http.Tests/combo_handler_and_endpoint.cs
index 17a896b86..856fb2dc2 100644
--- a/src/Http/Wolverine.Http.Tests/combo_handler_and_endpoint.cs
+++ b/src/Http/Wolverine.Http.Tests/combo_handler_and_endpoint.cs
@@ -14,17 +14,25 @@ public combo_handler_and_endpoint(AppFixture fixture) : base(fixture)
[Fact]
public async Task use_combo_with_problem_details_as_endpoint()
{
+ // cleaning up expected change
+ NumberMessageHandler.CalledBeforeOnlyOnHttpEndpoints = false;
+ NumberMessageHandler.CalledBeforeOnlyOnMessageHandlers = false;
+
// Should be good
await Host.Scenario(x =>
{
- x.Post.Json(new WolverineWebApi.NumberMessage(3)).ToUrl("/problems");
+ x.Post.Json(new WolverineWebApi.NumberMessage(3)).ToUrl("/problems2");
+ x.StatusCodeShouldBe(204);
});
+ NumberMessageHandler.CalledBeforeOnlyOnHttpEndpoints.ShouldBeTrue();
+ NumberMessageHandler.CalledBeforeOnlyOnMessageHandlers.ShouldBeFalse();
+
// should return problem details because the number > 5
// Should be good
var result = await Host.Scenario(x =>
{
- x.Post.Json(new WolverineWebApi.NumberMessage(10)).ToUrl("/problems");
+ x.Post.Json(new WolverineWebApi.NumberMessage(10)).ToUrl("/problems2");
x.ContentTypeShouldBe("application/problem+json");
x.StatusCodeShouldBe(400);
});
@@ -32,11 +40,16 @@ await Host.Scenario(x =>
var details = result.ReadAsJson();
details.Detail.ShouldBe("Number is bigger than 5");
+
+
}
[Fact]
public async Task use_combo_as_handler_see_problem_details_catch()
{
+ // cleaning up expected change
+ NumberMessageHandler.CalledBeforeOnlyOnHttpEndpoints = false;
+ NumberMessageHandler.CalledBeforeOnlyOnMessageHandlers = false;
NumberMessageHandler.Handled = false;
// This should be invalid and stop
@@ -46,5 +59,8 @@ public async Task use_combo_as_handler_see_problem_details_catch()
// should be good because we're < 5
await Host.InvokeAsync(new NumberMessage(3));
NumberMessageHandler.Handled.ShouldBeTrue();
+
+ NumberMessageHandler.CalledBeforeOnlyOnHttpEndpoints.ShouldBeFalse();
+ NumberMessageHandler.CalledBeforeOnlyOnMessageHandlers.ShouldBeTrue();
}
}
\ No newline at end of file
diff --git a/src/Http/Wolverine.Http/HttpChain.ApiDescription.cs b/src/Http/Wolverine.Http/HttpChain.ApiDescription.cs
index 93740aff9..c7aa8ee9f 100644
--- a/src/Http/Wolverine.Http/HttpChain.ApiDescription.cs
+++ b/src/Http/Wolverine.Http/HttpChain.ApiDescription.cs
@@ -126,6 +126,8 @@ public ApiDescription CreateApiDescription(string httpMethod)
return apiDescription;
}
+ public override MiddlewareScoping Scoping => MiddlewareScoping.HttpEndpoints;
+
public override void UseForResponse(MethodCall methodCall)
{
if (methodCall.ReturnVariable == null)
diff --git a/src/Http/WolverineWebApi/ProblemDetailsUsage.cs b/src/Http/WolverineWebApi/ProblemDetailsUsage.cs
index 7b0d23b8d..cae661d8c 100644
--- a/src/Http/WolverineWebApi/ProblemDetailsUsage.cs
+++ b/src/Http/WolverineWebApi/ProblemDetailsUsage.cs
@@ -1,5 +1,6 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using Wolverine.Attributes;
using Wolverine.Http;
namespace WolverineWebApi;
@@ -34,10 +35,10 @@ public record NumberMessage(int Number);
#endregion
+#region sample_using_problem_details_in_message_handler
+
public static class NumberMessageHandler
{
- #region sample_using_problem_details_in_message_handler
-
public static ProblemDetails Validate(NumberMessage message)
{
if (message.Number > 5)
@@ -52,7 +53,24 @@ public static ProblemDetails Validate(NumberMessage message)
// All good, keep on going!
return WolverineContinue.NoProblems;
}
-
+
+ // This "Before" method would only be utilized as
+ // an HTTP endpoint
+ [WolverineBefore(MiddlewareScoping.HttpEndpoints)]
+ public static void BeforeButOnlyOnHttp(HttpContext context)
+ {
+ Debug.WriteLine("Got an HTTP request for " + context.TraceIdentifier);
+ CalledBeforeOnlyOnHttpEndpoints = true;
+ }
+
+ // This "Before" method would only be utilized as
+ // a message handler
+ [WolverineBefore(MiddlewareScoping.MessageHandlers)]
+ public static void BeforeButOnlyOnMessageHandlers()
+ {
+ CalledBeforeOnlyOnMessageHandlers = true;
+ }
+
// Look at this! You can use this as an HTTP endpoint too!
[WolverinePost("/problems2")]
public static void Handle(NumberMessage message)
@@ -61,7 +79,10 @@ public static void Handle(NumberMessage message)
Handled = true;
}
- #endregion
+ // These properties are just a cheap trick in Wolverine internal tests
public static bool Handled { get; set; }
-}
\ No newline at end of file
+ public static bool CalledBeforeOnlyOnMessageHandlers { get; set; }
+ public static bool CalledBeforeOnlyOnHttpEndpoints { get; set; }
+}
+#endregion
\ No newline at end of file
diff --git a/src/Wolverine/Attributes/HandlerMethodAttributes.cs b/src/Wolverine/Attributes/HandlerMethodAttributes.cs
index 00e5d7159..66e289d55 100644
--- a/src/Wolverine/Attributes/HandlerMethodAttributes.cs
+++ b/src/Wolverine/Attributes/HandlerMethodAttributes.cs
@@ -1,18 +1,68 @@
namespace Wolverine.Attributes;
+public enum MiddlewareScoping
+{
+ ///
+ /// This middleware always applies
+ ///
+ Anywhere,
+
+ ///
+ /// This middleware should only be applied when used for message handling
+ ///
+ MessageHandlers,
+
+ ///
+ /// This middleware should only be applied when running in an HTTP endpoint
+ ///
+ HttpEndpoints
+}
+
+public abstract class ScopedMiddlewareAttribute : Attribute
+{
+ public MiddlewareScoping Scoping { get; set; } = MiddlewareScoping.Anywhere;
+
+ public ScopedMiddlewareAttribute(MiddlewareScoping scoping)
+ {
+ Scoping = scoping;
+ }
+
+ protected ScopedMiddlewareAttribute()
+ {
+ }
+}
+
///
/// Marks a method on middleware types or handler types as a method
/// that should be called before the actual handler
///
[AttributeUsage(AttributeTargets.Method)]
-public class WolverineBeforeAttribute : Attribute;
+public class WolverineBeforeAttribute : ScopedMiddlewareAttribute
+{
+ public WolverineBeforeAttribute(MiddlewareScoping scoping) : base(scoping)
+ {
+ }
+
+ public WolverineBeforeAttribute()
+ {
+ }
+}
///
/// Marks a method on middleware types or handler types as a method
/// that should be called after the actual handler
///
[AttributeUsage(AttributeTargets.Method)]
-public class WolverineAfterAttribute : Attribute;
+public class WolverineAfterAttribute : ScopedMiddlewareAttribute
+{
+ public WolverineAfterAttribute(MiddlewareScoping scoping) : base(scoping)
+ {
+ }
+
+ public WolverineAfterAttribute()
+ {
+ }
+}
///
/// Marks a method on middleware types or handler types as a method
@@ -20,4 +70,13 @@ public class WolverineAfterAttribute : Attribute;
/// a try/finally block around the message handlers
///
[AttributeUsage(AttributeTargets.Method)]
-public class WolverineFinallyAttribute : Attribute;
\ No newline at end of file
+public class WolverineFinallyAttribute : ScopedMiddlewareAttribute
+{
+ public WolverineFinallyAttribute(MiddlewareScoping scoping) : base(scoping)
+ {
+ }
+
+ public WolverineFinallyAttribute()
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Wolverine/Attributes/MiddlewareAttribute.cs b/src/Wolverine/Attributes/MiddlewareAttribute.cs
index ee8ec7140..e69ac942e 100644
--- a/src/Wolverine/Attributes/MiddlewareAttribute.cs
+++ b/src/Wolverine/Attributes/MiddlewareAttribute.cs
@@ -17,9 +17,15 @@ public MiddlewareAttribute(params Type[] frameTypes)
_frameTypes = frameTypes;
}
+ ///
+ /// Use this to optionally restrict the application of this middleware to only message handlers
+ /// or HTTP endpoints. Default is Anywhere
+ ///
+ public MiddlewareScoping Scoping { get; set; } = MiddlewareScoping.Anywhere;
+
public override void Modify(IChain chain, GenerationRules rules, IServiceContainer container)
{
- var applications = _frameTypes.Select(type => new MiddlewarePolicy.Application(type, _ => true)).ToList();
+ var applications = _frameTypes.Select(type => new MiddlewarePolicy.Application(chain, type, _ => true)).ToList();
MiddlewarePolicy.ApplyToChain(applications, rules, chain);
}
}
\ No newline at end of file
diff --git a/src/Wolverine/Configuration/Chain.cs b/src/Wolverine/Configuration/Chain.cs
index bdd99be97..af24ef4fb 100644
--- a/src/Wolverine/Configuration/Chain.cs
+++ b/src/Wolverine/Configuration/Chain.cs
@@ -28,6 +28,8 @@ public abstract class Chain : IChain
public abstract void ApplyParameterMatching(MethodCall call);
public List Middleware { get; } = [];
+ public abstract MiddlewareScoping Scoping { get; }
+
public List Postprocessors { get; } = [];
[IgnoreDescription]
@@ -272,7 +274,7 @@ public void ApplyImpliedMiddlewareFromHandlers(GenerationRules generationRules)
var handlerTypes = HandlerCalls().Select(x => x.HandlerType).Distinct();
foreach (var handlerType in handlerTypes)
{
- var befores = MiddlewarePolicy.FilterMethods(handlerType.GetMethods(),
+ var befores = MiddlewarePolicy.FilterMethods(this, handlerType.GetMethods(),
MiddlewarePolicy.BeforeMethodNames);
foreach (var before in befores)
@@ -299,7 +301,7 @@ public void ApplyImpliedMiddlewareFromHandlers(GenerationRules generationRules)
}
}
- var afters = MiddlewarePolicy.FilterMethods(handlerType.GetMethods(),
+ var afters = MiddlewarePolicy.FilterMethods(this, handlerType.GetMethods(),
MiddlewarePolicy.AfterMethodNames).ToArray();
if (afters.Any())
diff --git a/src/Wolverine/Configuration/IChain.cs b/src/Wolverine/Configuration/IChain.cs
index f9d1419a6..53d5f7d3c 100644
--- a/src/Wolverine/Configuration/IChain.cs
+++ b/src/Wolverine/Configuration/IChain.cs
@@ -10,6 +10,24 @@
namespace Wolverine.Configuration;
+internal static class ChainExtensions
+{
+ public static bool MatchesScope(this IChain chain, MethodInfo method)
+ {
+ if (chain == null) return true;
+
+ if (method.TryGetAttribute(out var att))
+ {
+ if (att.Scoping == MiddlewareScoping.Anywhere) return true;
+
+ return att.Scoping == chain.Scoping;
+ }
+
+ // All good if no attribute
+ return true;
+ }
+}
+
#region sample_IChain
///
@@ -18,6 +36,8 @@ namespace Wolverine.Configuration;
///
public interface IChain
{
+ MiddlewareScoping Scoping { get; }
+
void ApplyParameterMatching(MethodCall call);
///
diff --git a/src/Wolverine/Middleware/MiddlewarePolicy.cs b/src/Wolverine/Middleware/MiddlewarePolicy.cs
index 75c70240e..7f62b3b26 100644
--- a/src/Wolverine/Middleware/MiddlewarePolicy.cs
+++ b/src/Wolverine/Middleware/MiddlewarePolicy.cs
@@ -74,33 +74,38 @@ internal static void ApplyToChain(List applications, GenerationRule
public Application AddType(Type middlewareType, Func? filter = null)
{
filter ??= _ => true;
- var application = new Application(middlewareType, filter);
+ var application = new Application(null, middlewareType, filter);
_applications.Add(application);
return application;
}
- public static IEnumerable FilterMethods(IEnumerable methods, string[] validNames)
- where T : Attribute
+ public static IEnumerable FilterMethods(IChain? chain, IEnumerable methods,
+ string[] validNames)
+ where T : ScopedMiddlewareAttribute
{
+ // MatchesScope watches out for null chain
return methods
- .Where(x => !x.HasAttribute())
+ .Where(x => !x.HasAttribute() && chain!.MatchesScope(x))
.Where(x => validNames.Contains(x.Name) || x.HasAttribute());
}
public class Application
{
+ private readonly IChain? _chain;
private readonly MethodInfo[] _afters;
private readonly MethodInfo[] _befores;
private readonly ConstructorInfo? _constructor;
private readonly MethodInfo[] _finals;
-
- public Application(Type middlewareType, Func filter)
+
+ public Application(IChain? chain, Type middlewareType, Func filter)
{
if (!middlewareType.IsPublic && !middlewareType.IsVisible)
{
throw new InvalidWolverineMiddlewareException(middlewareType);
}
+ _chain = chain;
+
if (!middlewareType.IsStatic())
{
var constructors = middlewareType.GetConstructors();
@@ -115,11 +120,11 @@ public Application(Type middlewareType, Func filter)
MiddlewareType = middlewareType;
Filter = filter;
- var methods = middlewareType.GetMethods().ToArray();
+ var methods = middlewareType.GetMethods().Where(x => x.DeclaringType != typeof(object)).ToArray();
- _befores = FilterMethods(methods, BeforeMethodNames).ToArray();
- _afters = FilterMethods(methods, AfterMethodNames).ToArray();
- _finals = FilterMethods(methods, FinallyMethodNames).ToArray();
+ _befores = FilterMethods(chain, methods, BeforeMethodNames).ToArray();
+ _afters = FilterMethods(chain, methods, AfterMethodNames).ToArray();
+ _finals = FilterMethods(chain, methods, FinallyMethodNames).ToArray();
if (_befores.Length == 0 &&
_afters.Length == 0 &&
diff --git a/src/Wolverine/Runtime/Handlers/HandlerChain.cs b/src/Wolverine/Runtime/Handlers/HandlerChain.cs
index 6261e8afa..aaae79c7d 100644
--- a/src/Wolverine/Runtime/Handlers/HandlerChain.cs
+++ b/src/Wolverine/Runtime/Handlers/HandlerChain.cs
@@ -164,6 +164,8 @@ public LogLevel ExecutionLogLevel
///
public bool TelemetryEnabled { get; set; } = true;
+ public override MiddlewareScoping Scoping => MiddlewareScoping.MessageHandlers;
+
public override void ApplyParameterMatching(MethodCall call)
{
// Nothing