Skip to content

PipelineOperators

Andrew Bullock edited this page Jun 28, 2018 · 1 revision

MustardBlack operates around a pipeline. A request comes in and is passed to the Pipeline where a series of PipelineOperators execute in order to process the request, resulting in a response being sent back to the client.

A per-request data object (PipelineContext) is passed along the Pipeline to each PipelineOperator. This object contains the IRequest, IResponse and a dictionary of arbitrary objectes read/written by your application.

After [Application Routing] has taken place, everything that the Application does with the request is handled by a series of ordered PipelineOperators. Examples of PipelineOperators are the RoutingPipelineOperator, the HandlerExecutorPipelineOperator and the ResultExecutorPipelineOperator. Typically you will want at least these three PipelineOperators registered with your Application (in this order).

Ordering

PipelineOperators execute in the order they are registered in the Application. Sometimes PipelineOperators are skipped, keep reading.

RegisterPipelineOperator<RoutingPipelineOperator>();
RegisterPipelineOperator<HandlerExecutorPipelineOperator>();
RegisterPipelineOperator<ResultExecutorPipelineOperator>();

PipelineOperator Syntax

To implement a PipelineOperator you must implement IPipelineOperator - Important: this is a slight lie told here for simplicity, read the next section for a complete understanding.

IPipelineOperator defines the following method to implement

Task<PipelineContinuation> Operate(PipelineContext context);

PipelineContinuation is an enum with the following values:

Continue,
SkipToResultOperators,
End

Returning PipelineContinuation.Continue from your PipelineOperator will move the Pipeline processing on to the next registered PipelineOperator. Returning PipelineContinuation.End from your PipelineOperator will terminate all further Pipeline processing. You typically don't want to do this unless you have a very special case. Returning PipelineContinuation.SkipToResultOperators from your PipelineOperator will skip to the first registered IResultPresentPipleineOperator and begin executing it. Read the next section for what this means.

PipelineContinuation and the special case of PipelineContext.Result

Whilst the Pipeline itself ignorantly executes each PipelineOperator in order, there is special case for handling the difference between having a Result to turn into a Response and not.*

  • This syntax is an identified area for improvement withing MustardBlack, PRs welcome.

The Pipeline essentially consists of two parts, before a Result has been produced and afterwards.

PipelineOperators which handle stages before a Result has been produced should implement IPreResultPipelineOperator. Those which handle stages after a Result has been produced should implement IPostResultPipelineOperator.

Your Pipeline may have multiple PipelineOperators early on in the execution order which may produce results. Consider the below hypothetical PipelineOperators registered for a particular Application:

RegisterPipelineOperator<LegacyUrlsRedirectPipelineOperator>();	// an IPreResultPipelineOperator
RegisterPipelineOperator<AntiCSRFPipelineOperator>();			// an IPreResultPipelineOperator
RegisterPipelineOperator<AuthenticationPipelineOperator>();		// an IPreResultPipelineOperator
RegisterPipelineOperator<RoutingPipelineOperator>();			// an IPreResultPipelineOperator
RegisterPipelineOperator<HandlerExecutorPipelineOperator>();	// an IPreResultPipelineOperator
RegisterPipelineOperator<ResultExecutorPipelineOperator>();		// an IPostResultPipelineOperator

Its possible that any of the first 3 PipelineOperators could produce a result (and the HandlerExecutorPipelineOperator, but ignore that for now. The LegacyUrlsRedirectPipelineOperator could issue a RedirectResult to a Moved URI. The AntiCSRFPipelineOperator could issue a HttpResult(400) due to an anti-CSRF token mismatch. The AuthenticationPipelineOperator could issue a HttpResult(401) due to an authentication requirement not being met.

Its also possible that none of these set a Result because the URI was not legacy, it had a valid anti-CSRF token and appropriate authentication credentials were provided, in which case you'd want the request to be routed (by the RoutingPipelineOperator as normal and the HandlerExecutorPipelineOperator to do its job executing a [Handler] (producing a Result).

As soon as you have a Result, you probably want to skip to the PipelineOperators which expect there to be a Result on the PipelineContext and not process any more which don't.

Imagine the AntiCSRFPipelineOperator determines the request does not have a valid anti-CSRF token present and so sets an HttpResult(400) onto the PipelineContext. You would want to skip directly to the ResultExecutorPipelineOperator next to execute the result, and NOT bother performing authentication, routing and handler execution. Example pseudocode for and AntiCSRFPipelineOperator implementation is given below.

public sealed class AntiCSRFPipelineOperator : IPreResultPipelineOperator
{
	public async Task<PipelineContinuation> Operate(PipelineContext context)
	{
		// validate token somehoe
		var tokenIsValid = ValidateToken(context.Request.Cookies["csrf-token"]);
		
		// if the token is not valid
		if(!tokenIsValid)
		{
			context.Result = new HttpResult(HttpStatusCode.BadRequest);
			return PipelineContinuation.SkipToResultOperators;
		}
		
		// else the token is valid, so proceed
		return PipelineContinuation.Continue;
	}
}

Note that just setting a Result on the PipelineContext does not cause this behaviour. You must opt-in to it by returning PipelineContinuation.SkipToResultOperators. This gives you completex flexibility when desiging PipelineOperators.

Why not just skip to ResultExecutionPiplineOperator?

This would mean depending on a concrete implementation (ResultExecutionPiplineOperator) rather than an interface (IPostResultPipelineOperator) which is bad, but the stronger reason is to support pre-processing the Result before it is executed.

One example of such a PipelineOperator is in a javascript application which loads pages into an existing shell. When you visit a page within the app by a direct URL you'd want the page to load with the shell. However, when you click a link within your javascript application to load a new page, you would just want to result the page contents from the server without the shell. You might do that like this with Layouts/Master Pages:

RegisterPipelineOperator<RoutingPipelineOperator>();			// an IPreResultPipelineOperator
RegisterPipelineOperator<HandlerExecutorPipelineOperator>();	// an IPreResultPipelineOperator
RegisterPipelineOperator<LayoutModificationPipelineOperator>();		// an IPostResultPipelineOperator
RegisterPipelineOperator<ResultExecutorPipelineOperator>();		// an IPostResultPipelineOperator
public sealed class MasterPageModificationPipelineOperator : IPreResultPipelineOperator
{
	public async Task<PipelineContinuation> Operate(PipelineContext context)
	{
		var viewResult = context.Result as ViewResult;
		
		// if the result is a ViewResult and the request was made via ajax
		if(viewResult != null && context.Request.IsAjaxRequet())
			// Remove the layout/master-page from the view result so only the page contents gets rendered
			viewResult.Layout = null;
		
		return PipelineContinuation.Continue;
	}
}

Hopefully its clear at this point how you would want all the IPostResultPipelineOperators to execute for all results, regardless of which IPreResultPipelineOperator produced the result.

TODO

Clone this wiki locally