Async Operations¶
Operations are where the logic of the pipeline reside. Here we will be discussing Async Operations. You should create this type of Operation to implement logic that merits asynchronous calls such as
Long-running calculations
File System IO
Network Communication (e.g. REST API calls, RPC, print jobs, etc.)
Reads/Writes from/to a database
etc.
Note
Not everything should be an asynchronous operation. If it truly doesn’t merit the added complexity of an asynchronous call (e.g. simple data validation) then create a regular Operation instead.
To create an async Operation follow the steps below.
Steps to Create an Operation¶
Pre-Requisite
You must have created the Pipeline Context object that your Operation will use as a state object.
The Pipeline Coordinator will take care of setting the state object in the Operation’s Context
property. Unlike non-Async Operations, where the PipelineContext
is injected into the method call,
for async Operations the PipelineContext
is planted in the Context
property.
Step 1: Add New Class¶
1 2 3 4 5 6 7 8 9 10 | using System;
using System.Collections.Generic;
using System.Text;
namespace MyApplication.Operations
{
public class MyOperationAsync
{
}
}
|
Note
It is recommended that you suffix the class with “OperationAsync” as a naming convention or at least “Async” for self-documenting code.
Step 2: Create Marker Interface¶
Create a marker interface that inherits from IPipelineOperationAsync<TContext>
and specify the type of the application’s
Pipeline Context state object that this Operation will handle as its TContext
.
1 2 3 4 5 6 7 8 9 10 11 | using using KnightMoves.Pipelines.Interfaces;
namespace MyApplication.Operations
{
// Marker Interface
public interface IMyOperationAsync : IPipelineOperationAsync<MyApplicationContext> { }
public class MyOperationAsync
{
}
}
|
Step 3: Inherit and Implement¶
Inherit from BasePipelineOperationAsync
and implement the IMyOperationAsync
marker interface.
1 2 3 4 5 6 7 8 9 10 11 12 | using KnightMoves.Pipelines;
using KnightMoves.Pipelines.Interfaces;
namespace MyApplication.Operations
{
// Marker Interface
public interface IMyOperationAsync : IPipelineOperationAsync<MyApplicationContext> { }
public class MyOperationAsync : BasePipelineOperationAsync<MyApplicationContext>, IMyOperationAsync
{
}
}
|
Step 4: Implement Operation Logic¶
Implementing an async Operation will require overriding the implementation of the following two methods.
Task ExecuteAsync()
and
void CompletedTaskCallback(object task)
as shown bleow
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | using KnightMoves.Pipelines;
using KnightMoves.Pipelines.Interfaces;
namespace MyApplication.Operations
{
// Marker Interface
public interface IMyOperationAsync : IPipelineOperationAsync<MyApplicationContext> { }
public class MyOperationAsync : BasePipelineOperationAsync<MyApplicationContext>, IMyOperationAsync
{
private readonly ISomeApiClient _someApiClient;
// Constructor
public MyOperationAsync(ISomeApiClient someApiClient)
{
_someApiClient = someApiClient;
}
// No need to use async/await ... the returned Task is awaited for you
public override Task ExecuteAsync()
{
// Test for previous operations' success/failure if necessary
if (!Context.Successful)
return Task.FromResult(false);
// Implement async operation logic here using the context as needed
return _someApiClient.GetStuffAsync(Context.SomeId);
}
public override void CompletedTaskCallback(object task)
{
// Good practice to check for proper casting of the task
var t = task as Task<IEnumerable<Stuff>>;
if (t == null)
return;
IEnumerable<Stuff> stuff = t.Result;
Context.ListOfStuff = stuff;
Context.ResultMessages.Add("[MyOperationAsync] Successfully retrieved stuff");
}
}
}
|
Note
For Async Operations, the PipelineContext
is planted in the Context
property of the Operation itself.
With non-Async Operations the PipelineContext
is passed to the Execute(TContext context)
method.
Warning
If your Operation requires that another Operation be executed before it in the pipeline, then this is an
Operation-to-Operation dependency and you should add those dependencies to the Dependencies
collection
in the Operation’s constructor.
Using the Pipeline Context¶
Successful¶
The Pipeline Context object contains a boolean property called Successful
documented in the Pipeline Context page.
You can examine this property to make a decision on whether or not to do something.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // removed outer code blocks for brevity
public override Task ExecuteAsync()
{
if(!Context.Successful)
{
// Do nothing
return Task.FromResult(false);
}
// Logic goes here
return Task.FromResult(true);
}
public override void CompletedTaskCallback(object task)
{
// Maybe something went wrong in the logic here but
// it doesn't require terminating the whole pipeline
Context.Successful = false;
}
|
EndProcessing¶
You can cancel the execution of the rest of the pipeline by setting the EndProcessing
property to true. The
Pipeline Coordinator will not execute any Operation in the pipeline after this if it is set to true.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // removed outer code blocks for brevity
public override Task ExecuteAsync()
{
// Logic here resulted in some critical failure so we terminate
// the execution of all other Operations after this
Context.EndProcessing = true;
return Task.FromResult(false);
}
public override void CompletedTaskCallback(object task)
{
// Maybe something went wrong in the logic here
Context.EndProcessing = true;
}
|
ResultMessages¶
You can (and should) report the result of the Operation’s execution by putting a message in the ResultMessages
collection.
It can then be used at the end of the pipeline execution for logging and debugging.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // removed outer code blocks for brevity
public override Task ExecuteAsync()
{
var okay = true;
// Logic goes here and sets okay to false if something went wrong
if(!okay)
{
Context.ResultMessages("MyOperationAsync Failed!");
return Task.FromResult(false);
}
return Task.FromResult(true);
}
public override void CompletedTaskCallback(object task)
{
// Used the completed task to do stuff here
// Then we report the result
Context.ResultMessages("[MyOperationAsync] Successfully executed!");
}
|
Later the Pipeline Context can be used for logging and debugging.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public static void Main(string[] args)
{
// ...
_pipelineCoordinator
.ExecuteAsync<IMyOperation>()
.ExecuteAsync<ISaveResults>()
;
LogOperationResults(_pipelineCoordinator.Context.ResultMessages);
// ...
}
private static void LogOperationResults(IList<string> results)
{
// Log results here
}
|
Exceptions¶
If exceptions are caught in the Operation’s logic and you want to gracefully handle them in a try/catch block, then you can
plant the exception in the Exceptions
collection of the Pipeline Context for logging and debugging later.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // removed outer code blocks for brevity
public override Task ExecuteAsync()
{
try
{
// Some logic goes here
}
catch(Exception ex)
{
// Doh! Exception!
Context.Exceptions.Add(ex);
Context.EndProcessing = true;
Context.ResultMessages.Add("MyOperationAsync Exception: " + ex.Message);
return;
}
// Rest of Logic goes here
context.ResultMessages.Add("MyOperationAsync Successfully executed!");
}
|
Later the Pipeline Context can be used for logging and debugging.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public static void Main(string[] args)
{
// ...
_pipelineCoordinator
.ExecuteAsync<IMyOperation>()
.ExecuteAsync<ISaveResults>()
;
LogExceptions(_pipelineCoordinator.Context.Exceptions);
// ...
}
private static void LogExceptions(IList<string> results)
{
// Log results here
}
|