-
Notifications
You must be signed in to change notification settings - Fork 4
PipelineOperators
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).
PipelineOperators execute in the order they are registered in the Application. Sometimes PipelineOperators are skipped, keep reading.
RegisterPipelineOperator<RoutingPipelineOperator>();
RegisterPipelineOperator<HandlerExecutorPipelineOperator>();
RegisterPipelineOperator<ResultExecutorPipelineOperator>();
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.
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.
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