Header menu logo IcedTasks

Build a cancellable pipeline

This tutorial builds a small request-style pipeline with cancellableTask.

You will:

Use cancellableTask when cancellation is part of the operation's contract. The computation expression represents a function:

CancellationToken -> Task<'T>

That means the work does not start until the caller supplies a token.

#r "../../src/IcedTasks/bin/Release/net9.0/IcedTasks.dll"

open System
open System.Threading
open System.Threading.Tasks
open IcedTasks

Model a small request

The examples below use in-memory stand-ins for database or HTTP calls. The important part is that each operation can receive a CancellationToken.

type Customer = {
    Id: int
    Name: string
    IsActive: bool
}

type OrderSummary = { CustomerId: int; OpenOrders: int }

type Dashboard = {
    CustomerName: string
    OpenOrders: int
    CreditLimit: decimal
}

module Store =
    let loadCustomer customerId =
        cancellableTask {
            let! cancellationToken = CancellableTask.getCancellationToken ()
            do! Task.Delay(10, cancellationToken)

            return {
                Id = customerId
                Name = "Ada"
                IsActive = true
            }
        }

    let loadOpenOrders customerId =
        cancellableTask {
            do! fun cancellationToken -> Task.Delay(10, cancellationToken)

            return {
                CustomerId = customerId
                OpenOrders = 3
            }
        }

    let loadCreditLimit customerId =
        task {
            do! Task.Delay 10
            return if customerId > 0 then 2500.00M else 0.00M
        }

loadCustomer and loadOpenOrders are cancellable operations. They do not need a token argument in their public parameter list because cancellableTask will carry the token once the caller starts the pipeline.

loadCreditLimit returns a normal Task<decimal>. You can still bind it inside cancellableTask; use the cancellable shape for work that needs the ambient token and bind ordinary task-shaped APIs when you need to interoperate with them.

Compose the pipeline

The pipeline first loads a customer. Once that result is available, it can start the independent order and credit-limit lookups together with and!.

let buildDashboard customerId =
    cancellableTask {
        let! customer = Store.loadCustomer customerId

        if not customer.IsActive then
            return Error "The customer is inactive."
        else
            let! orders = Store.loadOpenOrders customer.Id
            and! creditLimit = Store.loadCreditLimit customer.Id

            return
                Ok {
                    CustomerName = customer.Name
                    OpenOrders = orders.OpenOrders
                    CreditLimit = creditLimit
                }
    }

Use and! only when the operands are independent. The order lookup and credit-limit lookup can both start after the customer is loaded, and neither needs the other's result.

If a later step needs the previous result, use another sequential let!.

Start the work at the boundary

A CancellableTask<'T> is started by calling it with a CancellationToken. In an ASP.NET app this token is usually HttpContext.RequestAborted. In a console app or test, create a token source explicitly.

let runTutorial () =
    task {
        use cancellation = new CancellationTokenSource(TimeSpan.FromSeconds 2.0)
        let! result = buildDashboard 42 cancellation.Token

        match result with
        | Ok dashboard ->
            return
                $"%s{dashboard.CustomerName}: %d{dashboard.OpenOrders} open orders, credit limit %M{dashboard.CreditLimit}"
        | Error message -> return message
    }

runTutorial().GetAwaiter().GetResult()

Next steps

For more details, continue with:

namespace System
namespace System.Threading
namespace System.Threading.Tasks
namespace IcedTasks
type Customer = { Id: int Name: string IsActive: bool }
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
Multiple items
val string: value: 'T -> string

--------------------
type string = String
type bool = Boolean
type OrderSummary = { CustomerId: int OpenOrders: int }
type Dashboard = { CustomerName: string OpenOrders: int CreditLimit: decimal }
Multiple items
val decimal: value: 'T -> decimal (requires member op_Explicit)

--------------------
type decimal = Decimal

--------------------
type decimal<'Measure> = decimal
val loadCustomer: customerId: int -> CancellableTask<Customer>
val customerId: int
val cancellableTask: CancellableTaskBuilder
<summary> Builds a cancellableTask using computation expression syntax. </summary>
val cancellationToken: CancellationToken
Multiple items
module CancellableTask from IcedTasks.CancellableTasks.CancellableTasks
<summary> Contains functional helper functions for composing and converting CancellableTask values. </summary>

--------------------
type CancellableTask = CancellationToken -> Task
<summary> CancellationToken -&gt; Task </summary>

--------------------
type CancellableTask<'T> = CancellationToken -> Task<'T>
<summary> CancellationToken -&gt; Task&lt;'T&gt; </summary>
val getCancellationToken: unit -> ct: CancellationToken -> ValueTask<CancellationToken>
<summary>Gets the default cancellation token for executing computations.</summary>
<returns>The default CancellationToken.</returns>
<category index="3">Cancellation and Exceptions</category>
<example id="default-cancellation-token-1"><code lang="F#"> use tokenSource = new CancellationTokenSource() let primes = [ 2; 3; 5; 7; 11 ] for i in primes do let computation = cancellableTask { let! cancellationToken = CancellableTask.getCancellationToken() do! Task.Delay(i * 1000, cancellationToken) printfn $"{i}" } computation tokenSource.Token |&gt; ignore Thread.Sleep(6000) tokenSource.Cancel() printfn "Tasks Finished" </code> This will print "2" 2 seconds from start, "3" 3 seconds from start, "5" 5 seconds from start, cease computation and then followed by "Tasks Finished". </example>
Multiple items
type Task = interface IAsyncResult interface IDisposable new: action: Action -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable + 1 overload member ContinueWith: continuationAction: Action<Task,obj> * state: obj -> Task + 19 overloads member Dispose: unit -> unit member GetAwaiter: unit -> TaskAwaiter member RunSynchronously: unit -> unit + 1 overload member Start: unit -> unit + 1 overload member Wait: unit -> unit + 5 overloads ...
<summary>Represents an asynchronous operation.</summary>

--------------------
type Task<'TResult> = inherit Task new: ``function`` : Func<obj,'TResult> * state: obj -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable<'TResult> + 1 overload member ContinueWith: continuationAction: Action<Task<'TResult>,obj> * state: obj -> Task + 19 overloads member GetAwaiter: unit -> TaskAwaiter<'TResult> member WaitAsync: cancellationToken: CancellationToken -> Task<'TResult> + 4 overloads member Result: 'TResult static member Factory: TaskFactory<'TResult>
<summary>Represents an asynchronous operation that can return a value.</summary>
<typeparam name="TResult">The type of the result produced by this <see cref="T:System.Threading.Tasks.Task`1" />.</typeparam>


--------------------
Task(action: Action) : Task
Task(action: Action, cancellationToken: CancellationToken) : Task
Task(action: Action, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj) : Task
Task(action: Action, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: CancellationToken) : Task
Task(action: Action<obj>, state: obj, creationOptions: TaskCreationOptions) : Task
Task(action: Action<obj>, state: obj, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task

--------------------
Task(``function`` : Func<'TResult>) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: CancellationToken) : Task<'TResult>
Task(``function`` : Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task.Delay(delay: TimeSpan) : Task
Task.Delay(millisecondsDelay: int) : Task
Task.Delay(delay: TimeSpan, timeProvider: TimeProvider) : Task
Task.Delay(delay: TimeSpan, cancellationToken: CancellationToken) : Task
Task.Delay(millisecondsDelay: int, cancellationToken: CancellationToken) : Task
Task.Delay(delay: TimeSpan, timeProvider: TimeProvider, cancellationToken: CancellationToken) : Task
val loadOpenOrders: customerId: int -> CancellableTask<OrderSummary>
val loadCreditLimit: customerId: int -> Task<decimal>
val task: TaskBuilder
val buildDashboard: customerId: int -> CancellableTask<Result<Dashboard,string>>
val customer: Customer
module Store from Build-a-cancellable-pipeline
Customer.IsActive: bool
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val orders: OrderSummary
Customer.Id: int
val creditLimit: decimal
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
Customer.Name: string
OrderSummary.OpenOrders: int
val runTutorial: unit -> Task<string>
val cancellation: CancellationTokenSource
Multiple items
type CancellationTokenSource = interface IDisposable new: unit -> unit + 3 overloads member Cancel: unit -> unit + 1 overload member CancelAfter: millisecondsDelay: int -> unit + 1 overload member CancelAsync: unit -> Task member Dispose: unit -> unit member TryReset: unit -> bool static member CreateLinkedTokenSource: token: CancellationToken -> CancellationTokenSource + 3 overloads member IsCancellationRequested: bool member Token: CancellationToken
<summary>Signals to a <see cref="T:System.Threading.CancellationToken" /> that it should be canceled.</summary>

--------------------
CancellationTokenSource() : CancellationTokenSource
CancellationTokenSource(millisecondsDelay: int) : CancellationTokenSource
CancellationTokenSource(delay: TimeSpan) : CancellationTokenSource
CancellationTokenSource(delay: TimeSpan, timeProvider: TimeProvider) : CancellationTokenSource
Multiple items
[<Struct>] type TimeSpan = new: hours: int * minutes: int * seconds: int -> unit + 4 overloads member Add: ts: TimeSpan -> TimeSpan member CompareTo: value: obj -> int + 1 overload member Divide: divisor: float -> TimeSpan + 1 overload member Duration: unit -> TimeSpan member Equals: value: obj -> bool + 2 overloads member GetHashCode: unit -> int member Multiply: factor: float -> TimeSpan member Negate: unit -> TimeSpan member Subtract: ts: TimeSpan -> TimeSpan ...
<summary>Represents a time interval.</summary>

--------------------
TimeSpan ()
TimeSpan(ticks: int64) : TimeSpan
TimeSpan(hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int, microseconds: int) : TimeSpan
TimeSpan.FromSeconds(seconds: int64) : TimeSpan
TimeSpan.FromSeconds(value: float) : TimeSpan
TimeSpan.FromSeconds(seconds: int64, ?milliseconds: int64, ?microseconds: int64) : TimeSpan
val result: Result<Dashboard,string>
property CancellationTokenSource.Token: CancellationToken with get
<summary>Gets the <see cref="T:System.Threading.CancellationToken" /> associated with this <see cref="T:System.Threading.CancellationTokenSource" />.</summary>
<exception cref="T:System.ObjectDisposedException">The token source has been disposed.</exception>
<returns>The <see cref="T:System.Threading.CancellationToken" /> associated with this <see cref="T:System.Threading.CancellationTokenSource" />.</returns>
val dashboard: Dashboard
Dashboard.CustomerName: string
val message: string

Type something to start searching.