Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions docsSrc/How-To-Guides/Cancellable-Task-In-Console-App.fsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
(**
---
title: Use CancellableTask in a console app
category: How To Guides
categoryindex: 4
index: 1
---


# How to use CancellableTask in a console app

[See Console App docs](https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/console-teleprompter)

To use a cancellableTask with a console app, we'll need to create a CancellationTokenSource and pass the CancellationToken to the cancellableTask.

In this example, we'll tie the CancellationTokenSource to the console app's cancellation token so that when the user presses Ctrl+C, the cancellableTask will be cancelled.

*)

#r "nuget: IcedTasks"

open IcedTasks

/// Set of Task based helpers
module Task =
open System.Threading
open System.Threading.Tasks

/// <summary>Queues the specified work to run on the thread pool. Helper for Task.Run</summary>
let runOnThreadpool (cancellationToken: CancellationToken) (func: unit -> Task<'b>) =
Task.Run<'b>(func, cancellationToken)

/// <summary>Helper for t.GetAwaiter().GetResult()</summary>
let runSynchounously (t: Task<'b>) = t.GetAwaiter().GetResult()

module MainTask =
open System
open System.Threading
open System.Threading.Tasks

// This is a helper to cancel the CancellationTokenSource if it hasn't already been cancelled or disposed.
let inline tryCancel (cts: CancellationTokenSource) =
try
cts.Cancel()
with :? ObjectDisposedException ->
// if CTS is disposed we're probably exiting cleanly
()

// This will set up the cancellation token source to be cancelled when the user presses Ctrl+C or when the app is unloaded
let setupCloseSignalers (cts: CancellationTokenSource) =
Console.CancelKeyPress.Add(fun _ ->
printfn "CancelKeyPress"
tryCancel cts
)

System.Runtime.Loader.AssemblyLoadContext.Default.add_Unloading (fun _ ->
printfn "AssemblyLoadContext unload"
tryCancel cts
)

AppDomain.CurrentDomain.ProcessExit.Add(fun _ ->
printfn "ProcessExit"
tryCancel cts
)

let mainAsync (argv: string array) =
cancellableTask {
let! ctoken = CancellableTask.getCancellationToken ()
printfn "Doing work!"
do! Task.Delay(1000, ctoken)
printfn "Work done"
return 0
}

[<EntryPoint>]
let main argv =
use cts = new CancellationTokenSource()
//
setupCloseSignalers cts

Task.runOnThreadpool cts.Token (fun () -> (mainAsync argv cts.Token))
// This will block until the cancellableTask is done or cancelled
// This should only be called once at the start of your app
|> Task.runSynchounously
2 changes: 1 addition & 1 deletion docsSrc/How-To-Guides/Cancellable-Task-In-Falco.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ We'll start off with a slightly longer version but then make a helper for this.
*)
// This just the dance to get Falco to compile for this example.
// Really you'd set up these references in an fsproj
#load "../../runtime-scripts/Microsoft.AspNetCore.App-latest-7.fsx"
#load "../../runtime-scripts/Microsoft.AspNetCore.App-latest-8.fsx"
#r "nuget: Falco"
#r "nuget: IcedTasks"

Expand Down
2 changes: 1 addition & 1 deletion docsSrc/How-To-Guides/Cancellable-Task-In-Giraffe.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ We'll start off with a slightly longer version but then make a helper for this.
*)
// This just the dance to get Giraffe to compile for this example.
// Really you'd set up these references in an fsproj
#load "../../runtime-scripts/Microsoft.AspNetCore.App-latest-7.fsx"
#load "../../runtime-scripts/Microsoft.AspNetCore.App-latest-8.fsx"
#r "nuget: Giraffe"
#r "nuget: IcedTasks"

Expand Down
95 changes: 95 additions & 0 deletions docsSrc/How-To-Guides/Cancellable-Task-in-Minimal-Api.fsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
(**
---
title: Use CancellableTask in ASP.NET Minimal API
category: How To Guides
categoryindex: 3
index: 1
---


# How to use CancellableTask in ASP.NET Minimal API

[See Minimal API docs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-9.0#routing)

To use a cancellableTask with Minimal APIs, we'll need to get the [RequestAborted](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted?view=aspnetcore-7.0) property off the HttpContext.


We'll start off with a slightly longer version but then make a helper for this.

*)
#load "../../runtime-scripts/Microsoft.AspNetCore.App-latest-8.fsx"
#r "nuget: IcedTasks"


open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Hosting
open System.Threading
open System.Threading.Tasks
open IcedTasks

// This is a stand-in for some real database call like Npgsql where it would take a CancellationToken
type Database =
static member Get(query, queryParams, cancellationToken: CancellationToken) =
task { do! Task.Delay(10, cancellationToken) }

module ExampleVerbose =

// Some function that's doing the real handler's work
let myRealWork (ctx: HttpContext) =
cancellableTask {
// Use a lambda to get the cancellableTask's current CancellationToken
let! result =
fun ct -> Database.Get("SELECT foo FROM bar where baz = @0", [ "@0", "buzz" ], ct)

ctx.Response.ContentType <- "application/json"
do! ctx.Response.WriteAsJsonAsync(result)
}

// A helper to get the context's RequestAborted CancellationToken and pass it to the cancellableTask
let myCustomHandler (ctx: HttpContext) =
task {
let cancellationToken = ctx.RequestAborted
return! myRealWork ctx cancellationToken
}

// Minimal API app
let app =
let builder = WebApplication.CreateBuilder()
let app = builder.Build()

// MapGet requires a RequestDelegate, so we need wrap it since there's no implicit conversion
app.MapGet("/", RequestDelegate(fun ctx -> myCustomHandler ctx))
|> ignore

app

module ExampleRefactor1 =
open System

// A helper to get the context's RequestAborted CancellationToken and pass it to any cancellableTask
// Remember a CancellableTask is a function with the signature of CancellationToken -> Task<'T>
let inline cancellableHandler (cancellableHandler: HttpContext -> CancellableTask<unit>) =
//ASP.NET MapGet requires a RequestDelegate, so we need wrap it since there's no implicit conversion
RequestDelegate(fun ctx -> cancellableHandler ctx ctx.RequestAborted)

// Some function that's doing the real handler's work
let myRealWork (ctx: HttpContext) =
cancellableTask {
// Use a lambda to get the cancellableTask's current CancellationToken
let! result =
fun ct -> Database.Get("SELECT foo FROM bar where baz = @0", [ "@0", "buzz" ], ct)

ctx.Response.ContentType <- "application/json"
do! ctx.Response.WriteAsJsonAsync(result)
}

// Minimal API app
let app =
let builder = WebApplication.CreateBuilder()
let app = builder.Build()

app.MapGet("/", cancellableHandler myRealWork)
|> ignore

app
49 changes: 42 additions & 7 deletions generate-sdk-references.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ open System.Text.RegularExpressions
// It will also generate a .fsx file for the latest major version of the .NET SDK to make referencing less brittle.

#r "nuget: semver"

open Semver
open System.Linq

type Runtime = {
Name: string
Expand All @@ -18,11 +20,11 @@ type Runtime = {

let getRuntimeList () =
// You can see which versions of the .NET runtime are currently installed with the following command.

let psi =
ProcessStartInfo("dotnet", "--list-runtimes", RedirectStandardOutput = true)

let proc = Process.Start(psi)
proc.WaitForExit()
use proc = Process.Start(psi)

let output =
seq {
Expand All @@ -45,9 +47,13 @@ let getRuntimeList () =
Path = DirectoryInfo(Path.Join(matches.Groups[3].Value, version))
}
)
|> Seq.toArray

proc.WaitForExit(3000)
|> ignore

runtimes
|> Seq.toArray


module Seq =
let filterOut predicate =
Expand All @@ -63,6 +69,30 @@ module Seq =
|> Seq.exists (fun f -> f x)
)

module Array =
open System.Collections.Generic

[<CompiledName("MaxBy")>]
let inline maxByC (comparer: IComparer<'U>) (projection: 'T -> 'U) (source: seq<'T>) : 'T =
// checkNonNull "source" source
use e = source.GetEnumerator()

if not (e.MoveNext()) then
invalidArg "source" ""

let first = e.Current
let mutable acc = projection first
let mutable accv = first

while e.MoveNext() do
let currv = e.Current
let curr = projection currv

if comparer.Compare(acc, curr) > 0 then
acc <- curr
accv <- currv

accv

let createRuntimeLoadScript blockedDlls (r: Runtime) =
let dir = r.Path
Expand Down Expand Up @@ -92,9 +122,12 @@ let writeReferencesToFile outputPath outputFileName referenceContents =
Directory.CreateDirectory(outputPath)
|> ignore

File.WriteAllLines(Path.Join(outputPath, outputFileName), referenceContents)
let outputPath = Path.Join(outputPath, outputFileName)
printfn "Writing to %s" outputPath

let runtimeOuputNameByVersion r = $"{r.Name}-{r.Version.ToString()}.fsx"
File.WriteAllLines(outputPath, referenceContents)

let runtimeOutputNameByVersion r = $"{r.Name}-{r.Version.ToString()}.fsx"

let runtimeOuputNameByMajorVersion r =
$"{r.Name}-latest-{r.Version.Major}.fsx"
Expand All @@ -119,6 +152,7 @@ let blockedDlls = [
contains "System.IO.Compression.Native"
]


let runTimeLoadScripts =
getRuntimeList ()
|> Array.map (fun runtime -> runtime, createRuntimeLoadScript blockedDlls runtime)
Expand All @@ -128,15 +162,16 @@ let outputFolder = "runtime-scripts"
// print all by version
runTimeLoadScripts
|> Seq.iter (fun (r, referenceContents) ->
writeReferencesToFile outputFolder (runtimeOuputNameByVersion r) referenceContents
writeReferencesToFile outputFolder (runtimeOutputNameByVersion r) referenceContents
)


// print all by major version
runTimeLoadScripts
|> Array.groupBy (fun (r, _) -> r.Name, r.Version.Major)
|> Array.map (fun (_, values) ->
values
|> Array.maxBy (fun (r, _) -> r.Version)
|> Array.maxByC SemVersion.SortOrderComparer (fun (r, _) -> r.Version)
)
|> Array.iter (fun (r, referenceContents) ->
writeReferencesToFile outputFolder (runtimeOuputNameByMajorVersion r) referenceContents
Expand Down