Middleware

Pipelines support custom middleware. Custom middleware can be created by implementing an extension method that uses a Hook or Wrap builder.

Middleware Syntax

Method Description
Hook Applies middleware to each step in the pipeline.
Wrap Wraps the middleware around the preceeding steps.

Hook

Hooks are middleware that surround individual pipeline actions. The Hook and HookAsync methods allow you to add a hook that is called for every statement in the pipeline. This hook takes the current context, the current argument, and a delegate to the next part of the pipeline. It can manipulate the argument before and after calling the next part of the pipeline.

Here’s an example of how to use HookAsync with an inline lambda:

var command = PipelineFactory
    .Start<string>()
    .HookAsync( async ( ctx, arg, next ) => await next( ctx, arg + "[" ) + "]" )
    .Pipe( ( ctx, arg ) => arg + "1" )
    .Pipe( ( ctx, arg ) => arg + "2" )
    .Build();

var result = await command( new PipelineContext() );

Assert.AreEqual( "[1][2]", result );

Example

Example of a hook middleware that surrounds each step. Hooks must be constrained to only be available at the start of the pipeline. This is accomplished be extending IPipelineStartBuilder<TInput, TOutput>.

Definition:

public static class PipelineMiddleware
{
    public static IPipelineStartBuilder<TInput, TOutput> WithLogging<TInput, TOutput>( this IPipelineStartBuilder<TInput, TOutput> builder )
    {
        return builder.HookAsync( async ( context, argument, next ) =>
        {
            Console.WriteLine( $"[{context.Id:D2}] begin with (arg = {argument})" );

            var result = await next( context, argument );

            Console.WriteLine( $"[{context.Id:D2}] end with (result = {result})" );

            return result;
        } );
    }
}

Usage:

var command = PipelineFactory
    .Start<string>()
    .WithLogging()
    .Pipe((ctx, arg) => $"hello {arg}" )
    .Pipe((ctx, arg) => $"{arg}, again!")
    .Build();

var result = await command( new PipelineContext(), "hook" );

output:

[02] begin (arg = hook)
[02] end (result = hello hook)
[03] begin (arg = hello hook)
[03] end (result = hello hook, again!)

The WithLogging hooked into the beginning and end of each pipeline step with the next method being the individual action(s).

Wraps

Wraps are middleware that surround a group of pipeline actions. The Wrap and WrapAsync method allows you to wrap a part of the pipeline. This is useful when you want to apply a transformation to only a part of the pipeline.

Here’s an example of how to use WrapAsync:

var command = PipelineFactory
    .Start<string>()
    .Pipe( ( ctx, arg ) => arg + "1" )
    .Pipe( ( ctx, arg ) => arg + "2" )
    .WrapAsync( async ( ctx, arg, next ) => await next( ctx, arg + "{" ) + "}" )
    .Pipe( ( ctx, arg ) => arg + "3" )
    .Build();

var result = await command( new PipelineContext() );

Assert.AreEqual( "{12}3", result );

Example

Example of a wrap middleware that surrounds a block of steps. Create Wrap middleware by extending IPipelineBuilder<TInput, TOutput>.

public static class PipelineMiddleware
{
    public static IPipelineBuilder<TInput, TOutput> WithTransaction<TInput, TOutput>( this IPipelineBuilder<TInput, TOutput> builder )
    {
        return builder.WrapAsync( async ( context, argument, next ) =>
        {
            Console.WriteLine( $"[{context.Id:D2}] begin transaction (name = '{context.Name}')" );

            var result = await next( context, argument );

            Console.WriteLine( $"[{context.Id:D2}] end transaction (name = '{context.Name}')" );

            return result;
        }, "T" );
    }
}

Usage:

var command = PipelineFactory
    .Start<string>()
    .Pipe((ctx, arg) => $"hello {arg}")
    .Pipe((ctx, arg) => $"{arg}, again!")
    .WithTransaction()
    .Build();

var result = await command(new PipelineContext(), "wrap");

Assert.AreEqual("hello wrap, again!", result);

output:

[02] begin transaction (name = 'T')
[02] end transaction (name = 'T')

The WithTransaction wrapped all the pipeline steps and was only executed at the beginning and and of the command with the next method being the entire group of actions.

Composition

Because of the way pipeline are composed it is possible for middleware to be additive or appear to override previous middleware.

Example

In this example of a hook the pipeline logs the current user.

public static class PipelineMiddleware
{
    public static IPipelineStartBuilder<TInput, TOutput> WithUser<TInput, TOutput>( this IPipelineStartBuilder<TInput, TOutput> builder, string user )
    {
        return builder.HookAsync( async ( context, argument, next ) =>
        {
            Console.WriteLine( $"[{context.Id:D2}] begin (user = '{user}') (arg = {argument})" );

            var result = await next( context, argument );

            Console.WriteLine( $"[{context.Id:D2}] end (result = {result})" );

            return result;
        } );
    }
}

Usage:

var command = PipelineFactory
    .Start<int>()
    .WithUser("bob")
    .WithUser("jim")
    .Pipe((ctx, arg) => $"input: {++arg}")
    .Build();

var result = await command(new PipelineContext(), 1);

output:

[02] begin (user = 'jim') (arg = 1)
[02] begin (user = 'bob') (arg = 1)
[02] end (result = input: 1)
[02] end (result = input: 1)

:warning: In this case both hooks are executed from the inside out (which appears as reverse order) before the main pipe step.

Usage with child pipeline:

var command = PipelineFactory
    .Start<int>()
    .WithUser("bob")
    .Pipe((ctx, arg) => ++arg)
    .PipeIf((ctx, arg) => arg == 2, 
        inheritMiddleware: false, 
        builder => builder
            .WithUser("jim")
            .Pipe((ctx, arg) => $"hello {++arg}")
    )
    .Pipe((ctx, arg) => $"{arg}, again!")
    .Build();

var result = await command(new PipelineContext(), 1);

output:

[02] begin (user = 'bob') (arg = 1)
[02] end (result = 2)
[03] begin (user = 'jim') (arg = 2)
[03] end (result = hello 3)
[04] begin (user = 'bob') (arg = hello 3)
[04] end (result = hello 3, again!)

:warning: In this case because the child pipeline does not inherit the parent middleware only the one “jim” hook wraps the $"hello {++arg}" step.


© Stillpoint Software.