Jasper 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 middleware
  • MessageHandler 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 matching HandlerChain
  • HandlerGraph is a collection of all the HandlerChain objects for the system
  • IHandlerPolicy provides a facility to allow for user-defined conventions or policies by directly modifying the HandlerGraph model before the MessageHandler 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 operations
  • RouteHandler is the common base class for handling the HTTP routes. These classes are generated based on the configuration of its matching RouteChain
  • RouteGraph is a collection of all the RouteChain objects for the system
  • IRoutePolicy provides a facility to allow for user-defined conventions or policies by directly modifying the RouteGraph model before the RouteHandler 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:

  1. Use custom attributes
  2. Use a custom IRoutePolicy or IHandlerPolicy class
  3. 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());
    }
}