Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/guide/http/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,5 +336,34 @@ public static string GetNow(DateTimeOffset now) // using the custom parameter st
<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>
<!-- endSnippet -->

## 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.
:::


20 changes: 18 additions & 2 deletions src/Http/Wolverine.Http.Tests/combo_handler_and_endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,42 @@ 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);
});

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

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
Expand All @@ -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();
}
}
2 changes: 2 additions & 0 deletions src/Http/Wolverine.Http/HttpChain.ApiDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 26 additions & 5 deletions src/Http/WolverineWebApi/ProblemDetailsUsage.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Wolverine.Attributes;
using Wolverine.Http;

namespace WolverineWebApi;
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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; }
}
public static bool CalledBeforeOnlyOnMessageHandlers { get; set; }
public static bool CalledBeforeOnlyOnHttpEndpoints { get; set; }
}
#endregion
65 changes: 62 additions & 3 deletions src/Wolverine/Attributes/HandlerMethodAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,82 @@
namespace Wolverine.Attributes;

public enum MiddlewareScoping
{
/// <summary>
/// This middleware always applies
/// </summary>
Anywhere,

/// <summary>
/// This middleware should only be applied when used for message handling
/// </summary>
MessageHandlers,

/// <summary>
/// This middleware should only be applied when running in an HTTP endpoint
/// </summary>
HttpEndpoints
}

public abstract class ScopedMiddlewareAttribute : Attribute
{
public MiddlewareScoping Scoping { get; set; } = MiddlewareScoping.Anywhere;

public ScopedMiddlewareAttribute(MiddlewareScoping scoping)
{
Scoping = scoping;
}

protected ScopedMiddlewareAttribute()
{
}
}

/// <summary>
/// Marks a method on middleware types or handler types as a method
/// that should be called before the actual handler
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class WolverineBeforeAttribute : Attribute;
public class WolverineBeforeAttribute : ScopedMiddlewareAttribute
{
public WolverineBeforeAttribute(MiddlewareScoping scoping) : base(scoping)
{
}

public WolverineBeforeAttribute()
{
}
}

/// <summary>
/// Marks a method on middleware types or handler types as a method
/// that should be called after the actual handler
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class WolverineAfterAttribute : Attribute;
public class WolverineAfterAttribute : ScopedMiddlewareAttribute
{
public WolverineAfterAttribute(MiddlewareScoping scoping) : base(scoping)
{
}

public WolverineAfterAttribute()
{
}
}

/// <summary>
/// Marks a method on middleware types or handler types as a method
/// that should be called after the actual handler in the finally block of
/// a try/finally block around the message handlers
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class WolverineFinallyAttribute : Attribute;
public class WolverineFinallyAttribute : ScopedMiddlewareAttribute
{
public WolverineFinallyAttribute(MiddlewareScoping scoping) : base(scoping)
{
}

public WolverineFinallyAttribute()
{
}
}
8 changes: 7 additions & 1 deletion src/Wolverine/Attributes/MiddlewareAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ public MiddlewareAttribute(params Type[] frameTypes)
_frameTypes = frameTypes;
}

/// <summary>
/// Use this to optionally restrict the application of this middleware to only message handlers
/// or HTTP endpoints. Default is Anywhere
/// </summary>
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);
}
}
6 changes: 4 additions & 2 deletions src/Wolverine/Configuration/Chain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public abstract class Chain<TChain, TModifyAttribute> : IChain
public abstract void ApplyParameterMatching(MethodCall call);
public List<Frame> Middleware { get; } = [];

public abstract MiddlewareScoping Scoping { get; }

public List<Frame> Postprocessors { get; } = [];

[IgnoreDescription]
Expand Down Expand Up @@ -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<WolverineBeforeAttribute>(handlerType.GetMethods(),
var befores = MiddlewarePolicy.FilterMethods<WolverineBeforeAttribute>(this, handlerType.GetMethods(),
MiddlewarePolicy.BeforeMethodNames);

foreach (var before in befores)
Expand All @@ -299,7 +301,7 @@ public void ApplyImpliedMiddlewareFromHandlers(GenerationRules generationRules)
}
}

var afters = MiddlewarePolicy.FilterMethods<WolverineAfterAttribute>(handlerType.GetMethods(),
var afters = MiddlewarePolicy.FilterMethods<WolverineAfterAttribute>(this, handlerType.GetMethods(),
MiddlewarePolicy.AfterMethodNames).ToArray();

if (afters.Any())
Expand Down
20 changes: 20 additions & 0 deletions src/Wolverine/Configuration/IChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScopedMiddlewareAttribute>(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

/// <summary>
Expand All @@ -18,6 +36,8 @@ namespace Wolverine.Configuration;
/// </summary>
public interface IChain
{
MiddlewareScoping Scoping { get; }

void ApplyParameterMatching(MethodCall call);

/// <summary>
Expand Down
25 changes: 15 additions & 10 deletions src/Wolverine/Middleware/MiddlewarePolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,33 +74,38 @@ internal static void ApplyToChain(List<Application> applications, GenerationRule
public Application AddType(Type middlewareType, Func<IChain, bool>? 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<MethodInfo> FilterMethods<T>(IEnumerable<MethodInfo> methods, string[] validNames)
where T : Attribute
public static IEnumerable<MethodInfo> FilterMethods<T>(IChain? chain, IEnumerable<MethodInfo> methods,
string[] validNames)
where T : ScopedMiddlewareAttribute
{
// MatchesScope watches out for null chain
return methods
.Where(x => !x.HasAttribute<WolverineIgnoreAttribute>())
.Where(x => !x.HasAttribute<WolverineIgnoreAttribute>() && chain!.MatchesScope(x))
.Where(x => validNames.Contains(x.Name) || x.HasAttribute<T>());
}

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<IChain, bool> filter)
public Application(IChain? chain, Type middlewareType, Func<IChain, bool> filter)
{
if (!middlewareType.IsPublic && !middlewareType.IsVisible)
{
throw new InvalidWolverineMiddlewareException(middlewareType);
}

_chain = chain;

if (!middlewareType.IsStatic())
{
var constructors = middlewareType.GetConstructors();
Expand All @@ -115,11 +120,11 @@ public Application(Type middlewareType, Func<IChain, bool> filter)
MiddlewareType = middlewareType;
Filter = filter;

var methods = middlewareType.GetMethods().ToArray();
var methods = middlewareType.GetMethods().Where(x => x.DeclaringType != typeof(object)).ToArray();

_befores = FilterMethods<WolverineBeforeAttribute>(methods, BeforeMethodNames).ToArray();
_afters = FilterMethods<WolverineAfterAttribute>(methods, AfterMethodNames).ToArray();
_finals = FilterMethods<WolverineFinallyAttribute>(methods, FinallyMethodNames).ToArray();
_befores = FilterMethods<WolverineBeforeAttribute>(chain, methods, BeforeMethodNames).ToArray();
_afters = FilterMethods<WolverineAfterAttribute>(chain, methods, AfterMethodNames).ToArray();
_finals = FilterMethods<WolverineFinallyAttribute>(chain, methods, FinallyMethodNames).ToArray();

if (_befores.Length == 0 &&
_afters.Length == 0 &&
Expand Down
2 changes: 2 additions & 0 deletions src/Wolverine/Runtime/Handlers/HandlerChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ public LogLevel ExecutionLogLevel
/// </summary>
public bool TelemetryEnabled { get; set; } = true;

public override MiddlewareScoping Scoping => MiddlewareScoping.MessageHandlers;

public override void ApplyParameterMatching(MethodCall call)
{
// Nothing
Expand Down
Loading