Version
1.1.2
Next
Reading, Writing, and Versioning Messages, Commands, and Events
Previous
Dead Letter EnvelopesJasper Middleware and Policies
Note! Jasper is the successor to an earlier project called FubuMVC that focused very hard on user-defined conventions and a modular runtime pipeline. Great, but some users overused those features resulting in systems that were unmaintainable. While Jasper definitely still supports user defined conventions and a flexible runtime pipeline, we urge some caution about overusing these features when just some explicit code will do the job.
Jasper's Architectural Model
Internally, the key components of Jasper's architecture relevant to this section are shown below:
Jasper's Configuration Model
Likewise, for the message handling:
HandlerChain
is the configuration-time model for each message handled by a Jasper application, including all the action methods and middlewareMessageHandler
is the common base class for the runtime-generated classes that handle messages at runtime. These classes are generated based on the configuration of its matchingHandlerChain
HandlerGraph
is a collection of all theHandlerChain
objects for the systemIHandlerPolicy
provides a facility to allow for user-defined conventions or policies by directly modifying theHandlerGraph
model before theMessageHandler
classes are generated
Jasper's internal middleware is the Frame model from the closely related Lamar project. During application bootstrapping, Jasper builds up both the RouteGraph
and HandlerGraph
models that essentially model a collection of Lamar Frame
objects that get compiled into the RouteHandler
and MessageHandler
classes at runtime.
To apply middleware to either HTTP routes or message handling, you'll be working with the common IChain
interface shown below:
public interface IChain
{
IList<Frame> Middleware { get; }
IList<Frame> Postprocessors { get; }
string Description { get; }
bool ShouldFlushOutgoingMessages();
MethodCall[] HandlerCalls();
IEnumerable<Type> ServiceDependencies(IContainer container);
}
For the HTTP routes in Jasper.Http
:
RouteChain
is the configuration-time model for the Url pattern, the endpoint action method that executes the route, and any configured middleware or post processing operationsRouteHandler
is the common base class for handling the HTTP routes. These classes are generated based on the configuration of its matchingRouteChain
RouteGraph
is a collection of all theRouteChain
objects for the systemIRoutePolicy
provides a facility to allow for user-defined conventions or policies by directly modifying theRouteGraph
model before theRouteHandler
classes are generated
Authoring Middleware
Note! This whole code compilation model is pretty new and there aren't enough examples yet. Feel very free to ask questions in the Gitter room linked in the top bar of this page.
Jasper supports the "Russian Doll" model of middleware, similar in concept to ASP.Net Core but very different in implementation. Jasper's middleware uses runtime code generation and compilation with LamarCompiler. What this means is that "middleware" in Jasper is code that is woven right into the message and route handlers.
As an example, let's say you want to build some custom middleware that is a simple performance timing of either HTTP route execution or message execution. In essence, you want to inject code like this:
var stopwatch = new Stopwatch();
stopwatch.Start();
try
{
// execute the HTTP request
// or message
}
finally
{
stopwatch.Stop();
logger.LogInformation("Ran something in " + stopwatch.ElapsedMilliseconds);
}
Alright, the first step is to create a LamarCompiler Frame
class that generates that code around the inner message or HTTP handler:
public class StopwatchFrame : SyncFrame
{
private readonly IChain _chain;
private readonly Variable _stopwatch;
private Variable _logger;
public StopwatchFrame(IChain chain)
{
_chain = chain;
// This frame creates a Stopwatch, so we
// expose that fact to the rest of the generated method
// just in case someone else wants that
_stopwatch = new Variable(typeof(Stopwatch), "stopwatch", this);
}
public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
{
writer.Write($"var stopwatch = new {typeof(Stopwatch).FullNameInCode()}();");
writer.Write("stopwatch.Start();");
writer.Write("BLOCK:try");
Next?.GenerateCode(method, writer);
writer.FinishBlock();
// Write a finally block where you record the stopwatch
writer.Write("BLOCK:finally");
writer.Write("stopwatch.Stop();");
writer.Write(
$"{_logger.Usage}.Log(Microsoft.Extensions.Logging.LogLevel.Information, \"{_chain.Description} ran in \" + {_stopwatch.Usage}.{nameof(Stopwatch.ElapsedMilliseconds)});)");
writer.FinishBlock();
}
public override IEnumerable<Variable> FindVariables(IMethodVariables chain)
{
// This in effect turns into "I need ILogger<IChain> injected into the
// compiled class"
_logger = chain.FindVariable(typeof(ILogger<IChain>));
yield return _logger;
}
}
Applying Middleware
Okay, great, but the next question is "how do I stick this middleware on routes or message handlers?". You've got three options:
- Use custom attributes
- Use a custom
IRoutePolicy
orIHandlerPolicy
class - Expose a static
Configure(chain)
method on handler classes
Even though one of the original design goals of FubuMVC and now Jasper was to eliminate or at least reduce the number of attributes users had to spew out into their application code, let's start with using an attribute.
Custom Attributes
To attach our StopwatchFrame
as middleware to any route or message handler, we can write a custom attribute based on Jasper's
ModifyChainAttribute
class as shown below:
public class StopwatchAttribute : ModifyChainAttribute
{
public override void Modify(IChain chain, GenerationRules rules, IContainer container)
{
chain.Middleware.Add(new StopwatchFrame(chain));
}
}
This attribute can now be placed either on a specific HTTP route endpoint method or message handler method to only apply to
that specific action, or it can be placed on a Handler
or Endpoint
class to apply to all methods exported by that type.
Here's an example:
public class ClockedEndpoint
{
[Stopwatch]
public string get_clocked()
{
return "how fast";
}
}
Now, when the application is bootstrapped, this is the code that would be generated to handle the "GET /clocked" route:
public class Jasper_Testing_Samples_ClockedEndpoint_get_clocked : Jasper.Http.Model.RouteHandler
{
private readonly Microsoft.Extensions.Logging.ILogger<Jasper.Configuration.IChain> _logger;
public Jasper_Testing_Samples_ClockedEndpoint_get_clocked(Microsoft.Extensions.Logging.ILogger<Jasper.Configuration.IChain> logger)
{
_logger = logger;
}
public override Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext, System.String[] segments)
{
var clockedEndpoint = new Jasper.Testing.Samples.ClockedEndpoint();
var stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
try
{
var result_of_get_clocked = clockedEndpoint.get_clocked();
return WriteText(result_of_get_clocked, httpContext.Response);
}
finally
{
stopwatch.Stop();
_logger.Log(Microsoft.Extensions.Logging.LogLevel.Information, "Route 'GET: clocked' ran in " + stopwatch.ElapsedMilliseconds);)
}
}
}
ModifyChainAttribute
is a generic way to add middleware or post processing frames, but if you need to configure things specific to routes or message handlers, you can also use ModifyHandlerChainAttribute
for message handlers or ModifyRouteAttribute
for http routes.
Policies
Note! Again, please go easy with this feature and try not to shoot yourself in the foot by getting too aggressive with custom policies
You can register user-defined policies that apply to all chains or some subset of chains. For message handlers, implement this interface:
public interface IHandlerPolicy
{
void Apply(HandlerGraph graph, GenerationRules rules, IContainer container);
}
Here's a simple sample that registers middleware on each handler chain:
public class WrapWithSimple : IHandlerPolicy
{
public void Apply(HandlerGraph graph, GenerationRules rules, IContainer container)
{
foreach (var chain in graph.Chains)
{
chain.Middleware.Add(new SimpleWrapper());
}
}
}
Then register your custom IHandlerPolicy
with a Jasper application like this:
public class AppWithHandlerPolicy : JasperOptions
{
public AppWithHandlerPolicy()
{
Handlers.GlobalPolicy<WrapWithSimple>();
}
}
Using Configure(chain) Methods
Note! This feature is experimental, but is meant to provide an easy way to apply middleware or other configuration to specific HTTP endpoints or message handlers without writing custom policies or having to resort to all new attributes.
There's one last option for configuring chains by a naming convention. If you want to configure the chains from just one handler or endpoint class, you can implement a method with one of these signatures:
public static void Configure(IChain)
{
// gets called for each endpoint or message handling method
// on just this class
}
public static void Configure(RouteChain chain)`
{
// gets called for each endpoint method on this class
}
public static void Configure(HandlerChain chain)
{
// gets called for each message handling method
// on just this class
}
Here's an example of this being used from Jasper's test suite:
public class CustomizedHandler
{
public void Handle(SpecialMessage message)
{
// actually handle the SpecialMessage
}
public static void Configure(HandlerChain chain)
{
chain.Middleware.Add(new CustomFrame());
}
}