Step 10: Feed orders from a stream
Until now the program started each order with a call of its own. Orders can also arrive as a stream: messages from a queue, or rows that another system writes. This step puts five orders through an Akka.Streams flow that starts one workflow per element, two at a time. Then it stops the process while two orders are shipping and runs the batch again, to show what resumes and what does not.
Step 10 needs FCQRS.Workflows 7.5.0 or later.
This page changes the program from step 9. The workflow does not change:
Order.fs (Order.cs) and Account.fs (Account.cs) stay as they are. The journal starts empty.
A batch of orders
In Program.fs (Program.cs), add the batch after the registration:
Shared setup
module Account =
open FCQRS.Common
// What the program and the order workflow can ask an account to do.
type AccountCommand =
| Deposit of amount: decimal
| Debit of payment: string * amount: decimal
// What the account replies. Rejected is a reply only: it is never stored.
type AccountEvent =
| Deposited of amount: decimal
| Debited of payment: string * amount: decimal
| Rejected of reason: string
// What the account knows now. Payments holds the payments it has taken, so
// a repeated debit takes the money once.
type AccountState = { Balance: decimal; Payments: Set<string> }
// The state before the account's first event.
let initial = { Balance = 0m; Payments = Set.empty }
// Chooses what to do with a command, based on the current state.
let decide (command: Command<AccountCommand>) (state: AccountState) =
match command.CommandDetails with
| Deposit amount -> PersistEvent(Deposited amount)
// A repeated debit gets the first answer. It is checked before the
// balance, because the first debit has already lowered the balance.
| Debit(payment, amount) when state.Payments.Contains payment ->
DeferEvent(Debited(payment, amount))
| Debit(_, amount) when amount > state.Balance ->
DeferEvent(Rejected $"Insufficient funds: {state.Balance} available")
| Debit(payment, amount) -> PersistEvent(Debited(payment, amount))
// Applies one event. A rejection or a repeated reply changes nothing.
let fold (event: Event<AccountEvent>) (state: AccountState) =
match event.EventDetails with
| Deposited amount -> { state with Balance = state.Balance + amount }
| Debited(payment, _) when state.Payments.Contains payment -> state
| Debited(payment, amount) ->
{ state with
Balance = state.Balance - amount
Payments = state.Payments.Add payment
}
| Rejected _ -> state
module Order =
open System
open System.Threading.Tasks
open FCQRS.Common
open FCQRS.FSharp
open FCQRS.Workflows
open Account
// What an order workflow is started with. FCQRS stores it in the journal.
type OrderRequest = {
Customer: string
Item: string
Quantity: int
UnitPrice: decimal
}
// What the workflow returns. FCQRS stores it too.
type OrderResult = {
Status: string
Reservation: string
Shipment: string
Confirmation: Guid
PlacedAt: DateTime
}
// The warehouse is an external system. The key is the same for every
// attempt of one step, so the warehouse can recognise a repeated call.
type IWarehouse =
abstract Reserve:
key: string * item: string * quantity: int -> Task<string>
abstract Release: key: string * reservation: string -> Task<string>
// The carrier is another external system. It returns a shipment ID.
type ICarrier =
abstract Ship:
key: string * reservation: string * item: string -> Task<string>
// A fraud check is a third external system. It answers whether an order
// may go ahead.
type IFraudCheck =
abstract Check:
key: string * customer: string * total: decimal -> Task<bool>
// Up to five attempts, 200 ms apart.
let reserveRetry = {
Retry.times 5 with
Schedule = FixedInterval(TimeSpan.FromMilliseconds 200.0)
}
// "order" is stored with every instance. It must not change.
let definition
(warehouse: IWarehouse)
(carrier: ICarrier)
(accounts: AggregateHandle<AccountCommand, AccountEvent>)
(fraud: IFraudCheck)
=
Workflow.define "order" (fun (request: OrderRequest) ->
workflow {
try
// A failed attempt is not stored. FCQRS tries again until
// an attempt succeeds or the policy gives up.
let! reservation =
activity "reserve-stock" reserveRetry (fun attempt ->
warehouse.Reserve(
attempt.IdempotencyKey,
request.Item,
request.Quantity))
// The body must give the same answers on every replay, so it
// reads the clock and makes GUIDs only through steps. FCQRS
// stores their values and replays them.
let! placedAt = now
let! confirmation = newGuid
let finish status shipment = {
Status = status
Reservation = reservation
Shipment = shipment
Confirmation = confirmation
PlacedAt = placedAt
}
// Gives the stock back when the order does not ship.
let release =
activity "release-stock" reserveRetry (fun attempt ->
warehouse.Release(attempt.IdempotencyKey, reservation))
let total = request.UnitPrice * decimal request.Quantity
// An order that had passed this point before the fraud check
// was added gets 1 and skips it. A new order gets 2. An order
// that fails the fraud check is not approved.
let! change = version "fraud-check" 2
let check (attempt: AttemptContext) =
fraud.Check(attempt.IdempotencyKey, request.Customer, total)
let! passed =
workflow {
if change < 2 then
return true
else
return! activity "check-fraud" (Retry.times 3) check
}
// An order over 1000 waits for a person to approve it, for at
// most one day. With no answer by then, the wait returns None.
let approvalWait = TimeSpan.FromDays 1.0
let! approved =
workflow {
if not passed then
return false
elif total <= 1000m then
return true
else
let! answer =
waitSignalFor<bool> "approval" approvalWait
return defaultArg answer false
}
if not approved then
let! _ = release
return finish "not approved" ""
else
// The confirmation number identifies the payment, so a
// repeated debit is recognised as the same payment.
let payment = string confirmation
let! reply =
send accounts request.Customer (Debit(payment, total))
match reply with
| Rejected _ ->
let! _ = release
return finish "payment refused" ""
| _ ->
// Packing takes two seconds. The timer's due time
// is stored, so a restart does not start the wait.
do! sleep (TimeSpan.FromSeconds 2.0)
let! shipment =
activity "ship" (Retry.times 3) (fun attempt ->
carrier.Ship(
attempt.IdempotencyKey,
reservation,
request.Item))
return finish "shipped" shipment
// When a step's policy gives up, the step fails, and the body
// sees WorkflowStepFailedException. Running out of stock is an
// answer the order can give; any other failure is not caught.
with :? WorkflowStepFailedException as failed
when failed.StepName = "reserve-stock" ->
return {
Status = "out of stock"
Reservation = ""
Shipment = ""
Confirmation = Guid.Empty
PlacedAt = DateTime.MinValue
}
})
// Unchanged from step 9.
open Order
Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.AccountFCQRSFCQRS.CommonContains common types like Events and Commands Functionality for Write Side.
Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Account.AccountCommandDepositamount: decimaldecimalAn abbreviation for the CLI type . Basic Types
Debitpayment: stringstringAn abbreviation for the CLI type . Basic Types
Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Account.AccountEventDepositedDebitedRejectedreason: stringFcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Account.AccountStateBalance: decimalPayments: Set<string>Microsoft.FSharp.Collections.FSharpSet`1Immutable sets based on binary trees, where elements are ordered by F# generic comparison. By default comparison is the F# structural comparison function or uses implementations of the IComparable interface on element values. See the module for further operations on sets. All members of this class are thread-safe and may be used concurrently from multiple threads.
initial: AccountStateMicrosoft.FSharp.Collections.SetModuleContains operations for working with values of type .
empty: Set<'T>The empty set for the type 'T. Set.empty<int> Evaluates to set [ ].
decide: Command<AccountCommand> -> AccountState -> EventAction<AccountEvent>command: Command<AccountCommand>FCQRS.Common.Command`1Represents a command to be processed by an aggregate actor. <typeparam name="'CommandDetails">The specific type of the command payload.</typeparam>
state: AccountStateCommandDetails: 'CommandDetailsThe specific details or payload of the command.
PersistEventPersist the event to the journal. The actor's state will be updated using the event handler *after* persistence succeeds.
Contains: string -> boolA useful shortcut for Set.contains. See the Set module for further operations on sets. The value to check. True if the set contains value. let set = Set.empty.Add(2).Add(3) printfn $"Does the set contain 1? {set.Contains(1)}" The sample evaluates to the following output: Does the set contain 1? false
DeferEventPublish and fold the event in the live actor without storing it or incrementing the persisted version. A deferred event does not start a saga; running sagas still receive it.
(>): 'T -> 'T -> boolStructural greater-than The first parameter. The second parameter. The result of the comparison. 5 > 1 // Evaluates to true 5 > 5 // Evaluates to false (1, "a") > (1, "z") // Evaluates to false
fold: Event<AccountEvent> -> AccountState -> AccountStateevent: Event<AccountEvent>FCQRS.Common.Event`1Represents an event generated by an aggregate actor as a result of processing a command. <typeparam name="'EventDetails">The specific type of the event payload.</typeparam>
EventDetails: 'EventDetailsThe specific details or payload of the event.
(-): ^T1 -> ^T2 -> ^T3Overloaded subtraction operator The first parameter. The second parameter. The result of the operation. 10 - 2 // Evaluates to 8
Add: string -> Set<string>A useful shortcut for Set.add. Note this operation produces a new set and does not mutate the original set. The new set will share many storage nodes with the original. See the Set module for further operations on sets. The value to add to the set. The result set. let set = Set.empty.Add(1).Add(1).Add(2) printfn $"The new set is: {set}" The sample evaluates to the following output: The new set is: set [1; 2]
Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.OrderSystemThreadingTasksFCQRS.FSharpIdiomatic-F# functional facade for FCQRS. Gives F# consumers the same one-call ergonomics the C# host-builder (HostExtensions.fs) gives C#, but with F# idioms: records-of-functions for the definitions, typed handles for the results, an explicit wiring pipeline, and plain helpers for saga side effects. It is a *pure addition* that wraps only the existing primitives (IActor.InitializeActor / SagaBuilder.initSimple / Projections.startTracked / InitializeSagaStarter / CreateCommandSubscription / Actor.api) and changes nothing in the C# interop layer or the core. open FCQRS.FSharp let api = Fcqrs.actor config loggerFactory (Some (Fcqrs.connect DBType.Sqlite conn)) "Cluster" let documents = Fcqrs.aggregate api { Name="Document"; Initial=...; Decide=...; Fold=... } let slugs = Fcqrs.aggregate api { Name="Slug"; Initial=...; Decide=...; Fold=... } let publication = Fcqrs.saga api (publicationDef documents.Factory slugs.Factory) Fcqrs.wireSagaStarters api [ publication ] let subs = Fcqrs.projection api (Projection.single 0 updateReadModel) // (Projection.multi when you must control which notifications publish) // send a command and await the matching aggregate reply: let! ev = documents.Send (Fcqrs.newCid()) (Fcqrs.aggregateId id) cmd (fun e -> ...)
WorkflowsFcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Order.OrderRequestCustomer: stringItem: stringQuantity: intintAn abbreviation for the CLI type . Basic Types
UnitPrice: decimalFcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Order.OrderResultStatus: stringReservation: stringShipment: stringConfirmation: GuidSystem.GuidRepresents a globally unique identifier (GUID).
PlacedAt: DateTimeSystem.DateTimeRepresents an instant in time, typically expressed as a date and time of day.
Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Order.IWarehouseReserve: IWarehouse -> string * string * int -> Task<string>keyitemquantitySystem.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
Release: IWarehouse -> string * string -> Task<string>reservationFcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Order.ICarrierShip: ICarrier -> string * string * string -> Task<string>Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Order.IFraudCheckCheck: IFraudCheck -> string * string * decimal -> Task<bool>customertotalboolAn abbreviation for the CLI type . Basic Types
reserveRetry: RetryPolicyFCQRS.Workflows.RetryCommon retry policies.
times: int -> RetryPolicyAt most `attempts` attempts, with backoff from one second to one minute.
Schedule: RetryScheduleThe wait between attempts.
FixedIntervalRe-send at a fixed interval.
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. is less than TimeSpan.MinValue or greater than TimeSpan.MaxValue. -or- is . -or- is . is equal to . An object that represents .
definition: IWarehouse -> ICarrier -> AggregateHandle<AccountCommand,AccountEvent> -> IFraudCheck -> WorkflowDefinition<OrderRequest,OrderResult>warehouse: IWarehousecarrier: ICarrieraccounts: AggregateHandle<AccountCommand,AccountEvent>FCQRS.FSharp.AggregateHandle`2What you get back after registering an aggregate.
fraud: IFraudCheckFCQRS.Workflows.WorkflowDefines workflows.
define: string -> ('Input -> Workflow<'Result>) -> WorkflowDefinition<'Input,'Result>A workflow named `name` whose instances run `body` on their input.
request: OrderRequestworkflow: WorkflowBuilderBuilds a workflow: `workflow { let! x = activity ... }`.
reservation: stringactivity: string -> RetryPolicy -> (AttemptContext -> Task<'T>) -> Workflow<'T>Calls an external system. The call runs at least once: a crash after the call and before its result is stored repeats it, so pass `IdempotencyKey` to the external system. `name` is stored and must not change while instances are running.
attempt: AttemptContextReserve: string * string * int -> Task<string>IdempotencyKey: stringThe same for every attempt of this step, across restarts: workflow name, workflow ID, and step path. Pass it to the external system so a repeated call is recognised.
placedAt: DateTimenow: Workflow<DateTime>The current UTC time, stored so that replay returns the same value.
confirmation: GuidnewGuid: Workflow<Guid>A new GUID, stored so that replay returns the same value.
finish: string -> string -> OrderResultstatus: stringshipment: stringrelease: Workflow<string>Release: string * string -> Task<string>total: decimal(*): ^T1 -> ^T2 -> ^T3Overloaded multiplication operator The first parameter. The second parameter. The result of the operation. 8 * 6 // Evaluates to 48
decimal: ^T -> decimalConverts the argument to System.Decimal using a direct conversion for all primitive numeric types. For strings, the input is converted using UInt64.Parse() with InvariantCulture settings. Otherwise the operation requires an appropriate static conversion method on the input type. The input value. The converted decimal. decimal "42.23" // evaluates to 42.23M decimal 0xff // evaluates to 255M decimal -10 // evaluates to -10M
change: intversion: string -> int -> Workflow<int>A value that records which version of the workflow's code an instance runs, for changing a workflow while instances are running: let! v = version "add-fraud-check" 2 if v >= 2 then ... new steps ... else ... the steps as they were ... A new instance stores `latest` and gets it. An instance that had already passed this point when the change was deployed gets 1 and stores nothing. The step takes no place in the step sequence, so adding it does not move the steps after it. `changeId` names the change. The number is stored under the step's position and this name. One name may appear at different positions, such as once per round of a loop, and different names may share a position. The same name twice at one position is not allowed. A later change at the same point keeps the name and raises `latest`. The name is a persistent contract: after a rename, an instance that stored a number finds none under the new name and gets 1.
check: AttemptContext -> Task<bool>FCQRS.Workflows.AttemptContextWhat an activity receives for one attempt.
Check: string * string * decimal -> Task<bool>passed: bool(<): 'T -> 'T -> boolStructural less-than comparison The first parameter. The second parameter. The result of the comparison. 1 < 5 // Evaluates to true 5 < 5 // Evaluates to false (1, "a") < (1, "z") // Evaluates to true
approvalWait: TimeSpanFromDays: float -> TimeSpanReturns a that represents a specified number of days, where the specification is accurate to the nearest millisecond. A number of days, accurate to the nearest millisecond. is less than TimeSpan.MinValue or greater than TimeSpan.MaxValue. -or- is . -or- is . is equal to . An object that represents .
approved: bool``not``: bool -> boolNegate a logical value. Not True equals False and not False equals True The value to negate. The result of the negation. not (2 + 2 = 5) // Evaluates to true // not is a function that can be compose with other functions let fileDoesNotExist = System.IO.File.Exists >> not
(<=): 'T -> 'T -> boolStructural less-than-or-equal comparison The first parameter. The second parameter. The result of the comparison. 5 <= 1 // Evaluates to false 5 <= 5 // Evaluates to true [1; 5] <= [1; 6] // Evaluates to true
answer: bool optionwaitSignalFor: string -> TimeSpan -> Workflow<'T option>Waits for a signal named `name` for at most `timeout`, measured from the stored start of the step. Gives `None` when the time passes first; a signal that arrives later stays stored for the next wait.
defaultArg: 'T option -> 'T -> 'TUsed to specify a default value for an optional argument in the implementation of a function An option representing the argument. The default value of the argument. The argument value. If it is None, the defaultValue is returned. type Vector(x: double, y: double, ?z: double) = let z = defaultArg z 0.0 member this.X = x member this.Y = y member this.Z = z let v1 = Vector(1.0, 2.0) v1.Z // Evaluates to 0. let v2 = Vector(1.0, 2.0, 3.0) v2.Z // Evaluates to 3.0
string: 'T -> stringConverts the argument to a string using ToString. For standard integer and floating point values and any type that implements IFormattable, ToString conversion uses CultureInfo.InvariantCulture. The input value. The converted string. string 'A' // evaluates to "A" string 0xff // evaluates to "255" string -10 // evaluates to "-10"
reply: AccountEventsend: AggregateHandle<'Command,'Event> -> string -> 'Command -> Workflow<'Event>`sendWith` under `Retry.command`: send again when no reply came in time, with backoff, for five minutes.
sleep: TimeSpan -> Workflow<unit>Waits for `duration`. The due time is stored when the step is first reached, so a restart does not postpone it.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. is less than TimeSpan.MinValue or greater than TimeSpan.MaxValue. -or- is . -or- is . is equal to . An object that represents .
Ship: string * string * string -> Task<string>FCQRS.Workflows.WorkflowStepFailedExceptionRaised in a workflow body when a step ended without a value. The body sees this exception both when the step fails and when a stored failure is replayed, never the original exception.
failed: WorkflowStepFailedExceptionStepName: stringThe step's name.
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
Empty: GuidA read-only instance of the structure whose value is all zeros.
MinValue: DateTimeRepresents the smallest possible value of . This field is read-only.
// Five orders from ada, one per item.
let batch =
[ "widget"; "gadget"; "gizmo"; "crate"; "sprocket" ]
|> List.map (fun item -> {
Customer = "ada"
Item = item
Quantity = 2
UnitPrice = 40m
})
// The order ID comes from the element, so an element that arrives again
// reaches the order it started before.
let idOf (request: OrderRequest) = $"o-{request.Item}"
batch: OrderRequest list(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ]
item: stringCustomer: stringItem: stringQuantity: intUnitPrice: decimalidOf: OrderRequest -> stringrequest: OrderRequestFcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Order.OrderRequest// Five orders from ada, one per item.
var batch = new[] { "widget", "gadget", "gizmo", "crate", "sprocket" }
.Select(item => new OrderRequest("ada", item, 2, 40m))
.ToList();
// The order ID comes from the element, so an element that arrives again
// reaches the order it started before.
string IdOf(OrderRequest request) => $"o-{request.Item}";
The five orders are ada's, one per item, and each costs 80. Under the stand-ins, the warehouse has
no gizmo and the carrier does not take a crate, so o-gizmo ends as "out of stock" and
o-crate faults. The other three ship.
idOf (IdOf in C#) computes the order ID from the element. A real element, such as a message
from a queue, carries an order number of its own; here the item stands in for it. The ID must come
from the element, not from a counter or a new GUID, so that an element that arrives again names the
order it started before. Its start then stores nothing.
Run the batch through a flow
The setup opens Akka.Streams.Dsl, which defines Source. The F# program also opens
System.Threading.Tasks for the stand-ins below:
open System
open System.Collections.Generic
open System.IO
open System.Threading
open System.Threading.Tasks
open Akka.Streams.Dsl
open Microsoft.Data.Sqlite
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.Logging
open FCQRS.Actor
open FCQRS.Common
open FCQRS.FSharp
open FCQRS.Workflows
open Account
open Order
let database = Path.Combine(AppContext.BaseDirectory, "orders.db")
// FCQRS logs through Microsoft.Extensions.Logging; this program stays quiet.
let logging = LoggerFactory.Create(fun _ -> ())
// FCQRS reads optional settings from IConfiguration; this program sets none.
let configuration = ConfigurationBuilder().Build()
// The journal is a SQLite file next to the program.
let connection = Fcqrs.connect DBType.Sqlite $"Data Source={database};"
let api = Fcqrs.actor configuration logging (Some connection) "orders"
SystemCollectionsGenericIOThreadingTasksAkkaStreamsDslMicrosoftDataSqliteExtensionsConfigurationLoggingFCQRSFCQRS.ActorFCQRS.CommonContains common types like Events and Commands Functionality for Write Side.
FCQRS.FSharpIdiomatic-F# functional facade for FCQRS. Gives F# consumers the same one-call ergonomics the C# host-builder (HostExtensions.fs) gives C#, but with F# idioms: records-of-functions for the definitions, typed handles for the results, an explicit wiring pipeline, and plain helpers for saga side effects. It is a *pure addition* that wraps only the existing primitives (IActor.InitializeActor / SagaBuilder.initSimple / Projections.startTracked / InitializeSagaStarter / CreateCommandSubscription / Actor.api) and changes nothing in the C# interop layer or the core. open FCQRS.FSharp let api = Fcqrs.actor config loggerFactory (Some (Fcqrs.connect DBType.Sqlite conn)) "Cluster" let documents = Fcqrs.aggregate api { Name="Document"; Initial=...; Decide=...; Fold=... } let slugs = Fcqrs.aggregate api { Name="Slug"; Initial=...; Decide=...; Fold=... } let publication = Fcqrs.saga api (publicationDef documents.Factory slugs.Factory) Fcqrs.wireSagaStarters api [ publication ] let subs = Fcqrs.projection api (Projection.single 0 updateReadModel) // (Projection.multi when you must control which notifications publish) // send a command and await the matching aggregate reply: let! ev = documents.Send (Fcqrs.newCid()) (Fcqrs.aggregateId id) cmd (fun e -> ...)
WorkflowsFcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.AccountFcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Orderdatabase: stringSystem.IO.PathPerforms operations on instances that contain file or directory path information. These operations are performed in a cross-platform manner.
Combine: string * string -> stringCombines two strings into a path. The first path to combine. The second path to combine. .NET Framework and .NET Core versions older than 2.1: or contains one or more of the invalid characters defined in . or is . The combined paths. If one of the specified paths is a zero-length string, this method returns the other path. If contains an absolute path, this method returns .
System.AppContextProvides members for setting and retrieving data about an application's context.
BaseDirectory: stringGets the file path of the base directory that the assembly resolver uses to probe for assemblies. The file path of the base directory that the assembly resolver uses to probe for assemblies.
logging: ILoggerFactoryMicrosoft.Extensions.Logging.LoggerFactoryProduces instances of classes based on the given providers.
Create: Action<ILoggingBuilder> -> ILoggerFactoryCreates new instance of configured using provided delegate. A delegate to configure the . The that was created.
configuration: IConfigurationRoot``.ctor``: unit -> unitBuild: unit -> IConfigurationRootBuilds an with keys and values from the set of providers registered in . An with keys and values from the registered providers.
connection: ConnectionFCQRS.FSharp.Fcqrsconnect: DBType -> string -> ConnectionBuild a SQLite/etc. Connection from a raw connection string of any length.
FCQRS.Actor.DBTypeRepresents the type of database connection
SqliteSQLite using Microsoft.Data.Sqlite provider
api: IActoractor: IConfiguration -> ILoggerFactory -> Connection option -> string -> IActorCreate the actor system from plain values (cluster name as a string).
SomeThe representation of "Value of type 'T" The input value. An option representing the value.
using Akka.Streams.Dsl;
using FCQRS.Workflows;
using FCQRS.Workflows.CSharp;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using static FCQRS.CSharp;
var database = Path.Combine(AppContext.BaseDirectory, "orders.db");
// FCQRS logs through Microsoft.Extensions.Logging; this program stays quiet.
var logging = LoggerFactory.Create(_ => { });
// FCQRS reads optional settings from IConfiguration; this program sets none.
var configuration = new ConfigurationBuilder().Build();
// The journal is a SQLite file next to the program.
var actor = ActorApi.Create(
configuration, logging, $"Data Source={database};", "orders");
After the batch, replace the functions and the lines that placed orders with the stream. The
stream starts the orders and waits for their outcomes, so start and place (Start and Place),
the wait for o-7, and the order o-9 go. Step 10 starts from an empty journal, so nothing waits
for o-7. deposit moves to the registration, shown below:
Shared setup
// Registered further down this page.
module Registered =
let orders =
Unchecked.defaultof<WorkflowHandle<OrderRequest, OrderResult>>
open Registered
Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Registeredorders: WorkflowHandle<OrderRequest,OrderResult>Microsoft.FSharp.Core.Operators.UncheckedThis module contains basic operations which do not apply runtime and/or static checks
defaultof: 'TGenerate a default value for any type. This is null for reference types, For structs, this is struct value where all fields have the default value. This function is unsafe in the sense that some F# values do not have proper null values.
FCQRS.Workflows.WorkflowHandle`2Starts and queries the instances of a registered workflow.
Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Order.OrderRequestFcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Order.OrderResult// Orders that registering resumed are running before the stream starts.
for id in [ "o-widget"; "o-gadget" ] do
match orders.Status id |> Async.RunSynchronously with
| WorkflowStatus.Running pending ->
let steps = String.Join(", ", pending)
printfn $"{id} is running: {steps}"
| _ -> ()
// Prints an outcome as the flow emits it.
let print (struct (id: string, outcome: WorkflowOutcome<OrderResult>)) =
match outcome with
| WorkflowOutcome.Finished result ->
printfn $"{id} finished: {result.Status}"
| WorkflowOutcome.Faulted error -> printfn $"{id} faulted: {error.Message}"
| WorkflowOutcome.Cancelled -> printfn $"{id} was cancelled"
// Counts the orders between entering the flow and leaving it. This
// stream has no async boundary, so its stages run one at a time and
// plain counters are enough.
let mutable inFlow = 0
let mutable most = 0
// Start an order per element, at most two at once, and emit each outcome
// in the batch's order. An order that takes longer than 30 seconds fails
// the stream.
Source
.From(batch)
.Select(fun request ->
inFlow <- inFlow + 1
most <- max most inFlow
request)
.Via(WorkflowFlow.run orders idOf 2 (TimeSpan.FromSeconds 30.0))
.Select(fun outcome ->
inFlow <- inFlow - 1
outcome)
.RunForeach(print, api.Materializer)
.Wait()
printfn $"At most {most} orders were in the stream at once"
id: stringorders: WorkflowHandle<OrderRequest,OrderResult>Status: string -> Async<WorkflowStatus>Where the instance is.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Microsoft.FSharp.Control.FSharpAsyncHolds static members for creating and manipulating asynchronous computations. See also F# Language Guide - Async Workflows. Async Programming
RunSynchronously: Async<'T> * int option * CancellationToken option -> 'TRuns the asynchronous computation and await its result. If an exception occurs in the asynchronous computation then an exception is re-raised by this function. If no cancellation token is provided then the default cancellation token is used. The computation is started on the current thread if is null, has of true, and no timeout is specified. Otherwise the computation is started by queueing a new work item in the thread pool, and the current thread is blocked awaiting the completion of the computation. The timeout parameter is given in milliseconds. A value of -1 is equivalent to . The computation to run. The amount of time in milliseconds to wait for the result of the computation before raising a . If no value is provided for timeout then a default of -1 is used to correspond to . The cancellation token to be associated with the computation. If one is not supplied, the default cancellation token is used. The result of the computation. Starting Async Computations printfn "A" let result = async { printfn "B" do! Async.Sleep(1000) printfn "C" 17 } |> Async.RunSynchronously printfn "D" Prints "A", "B" immediately, then "C", "D" in 1 second. result is set to 17.
FCQRS.Workflows.WorkflowStatusWhere a workflow instance is.
RunningThe body is waiting on these steps, given as "path name".
pending: string liststeps: stringSystem.StringRepresents text as a sequence of UTF-16 code units.
Join: string * string seq -> stringConcatenates the members of a constructed collection of type , using the specified separator between each member. The string to use as a separator. is included in the returned string only if has more than one element. A collection that contains the strings to concatenate. is . The length of the resulting string overflows the maximum allowed length (Int32.MaxValue). A string that consists of the elements of delimited by the string. -or- if has zero elements.
printfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
print: struct (string * WorkflowOutcome<OrderResult>) -> unit_arg1: struct (string * WorkflowOutcome<OrderResult>)outcome: WorkflowOutcome<OrderResult>FCQRS.Workflows.WorkflowOutcome`1How a workflow ended.
FinishedThe body returned this value.
result: OrderResultFaultedThe body threw an exception it did not handle.
error: StepErrorCancelledThe workflow was cancelled.
inFlow: intmost: intAkka.Streams.Dsl.SourceTBD
batch: OrderRequest listrequest: OrderRequest(+): ^T1 -> ^T2 -> ^T3Overloaded addition operator The first parameter. The second parameter. The result of the operation. 2 + 2 // Evaluates to 4 "Hello " + "World" // Evaluates to "Hello World"
max: 'T -> 'T -> 'TMaximum based on generic comparison The first value. The second value. The maximum value. max 1 2 // Evaluates to 2 max [1;2;3] [1;2;4] // Evaluates to [1;2;4] max "zoo" "alpha" // Evaluates to "zoo"
FCQRS.Workflows.WorkflowFlowAkka.Streams stages that start workflows. `run` and `start` take an F# handle; `Run` and `Start` take a C# client. The stream is not durable. If the process stops, the workflows resume and the stream does not, so the source must replay from an offset of its own, for example by committing only after an element's outcome is emitted. Compute the workflow ID from the element: a replayed element then reaches the instance it started before and starts no second one.
run: WorkflowHandle<'Input,'Result> -> ('Input -> string) -> int -> TimeSpan -> Flow<'Input,struct (string * WorkflowOutcome<'Result>),Akka.NotUsed>Starts one workflow per element and emits the ID with the workflow's outcome, in the input's order. A faulted or cancelled workflow is an element like a finished one. At most `parallelism` elements are between their start and their outcome, so the stream keeps at most that many workflows between start and outcome. It does not limit other workflows of the same definition: those that resumed after a restart, and those whose timeout failed the stream, run regardless. An element whose workflow does not end within `timeout` fails the stream with a `TimeoutException`; the workflow itself carries on.
idOf: OrderRequest -> stringSystem.TimeSpanRepresents a time interval.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. is less than TimeSpan.MinValue or greater than TimeSpan.MaxValue. -or- is . -or- is . is equal to . An object that represents .
outcome: struct (string * WorkflowOutcome<OrderResult>)(-): ^T1 -> ^T2 -> ^T3Overloaded subtraction operator The first parameter. The second parameter. The result of the operation. 10 - 2 // Evaluates to 8
api: IActorMaterializer: Akka.Streams.ActorMaterializerGets the Akka Streams materializer.
// Orders that registering resumed are running before the stream starts.
foreach (var id in new[] { "o-widget", "o-gadget" })
{
if (await orders.StatusAsync(id) is WorkflowStatus.Running running)
{
var steps = string.Join(", ", running.pending);
Console.WriteLine($"{id} is running: {steps}");
}
}
// Prints an outcome as the flow emits it.
void Print(WorkflowEnded<OrderResult> ended)
{
if (ended.IsFinished)
Console.WriteLine($"{ended.Id} finished: {ended.Result.Status}");
else if (ended.IsFaulted)
Console.WriteLine($"{ended.Id} faulted: {ended.Error!.Message}");
else
Console.WriteLine($"{ended.Id} was cancelled");
}
// Counts the orders between entering the flow and leaving it. This
// stream has no async boundary, so its stages run one at a time and
// plain counters are enough.
var inFlow = 0;
var most = 0;
// Start an order per element, at most two at once, and emit each outcome
// in the batch's order. An order that takes longer than 30 seconds fails
// the stream.
await Source
.From(batch)
.Select(request =>
{
inFlow++;
most = Math.Max(most, inFlow);
return request;
})
.Via(WorkflowFlow.Run(orders, IdOf, 2, TimeSpan.FromSeconds(30)))
.Select(ended =>
{
inFlow--;
return ended;
})
.RunForeach(Print, actor.Materializer);
Console.WriteLine($"At most {most} orders were in the stream at once");
The program first prints the status of o-widget and o-gadget if they are running. On the first
run nothing has started yet, so it prints nothing.
WorkflowFlow.run orders idOf 2 timeout (WorkflowFlow.Run(orders, IdOf, 2, timeout) in C#) is a
flow from OrderRequest to an outcome. For each element it computes the ID, starts the order, and
waits until the order ends:
- At most two elements are between their start and their outcome, so the stream keeps at most two orders between start and outcome. The third element enters when an earlier one leaves.
- Outcomes leave in the batch's order, even when a later order ends first.
- A faulted order is an element like a finished one:
WorkflowOutcome.Faultedin F#,IsFaultedin C#. It does not fail the stream. - An order that does not end within 30 seconds fails the stream with a
TimeoutException. The order itself carries on.
The Select before the flow counts each element that enters it, and the one after counts each
outcome that leaves. Akka.Streams runs the stages of this stream one at a time, so the counters need
no lock. RunForeach runs the stream with the actor system's materializer and calls print
(Print) for each outcome; the program waits until the stream completes.
At the end, the program prints the journal of o-widget instead of o-7:
// Applications do not read this table. This program reads it only to show
// what FCQRS stored for an order.
let printJournal id =
use connection = new SqliteConnection($"Data Source={database}")
connection.Open()
let command = connection.CreateCommand()
command.CommandText <-
"SELECT sequence_number, manifest FROM journal
WHERE persistence_id = $id
ORDER BY sequence_number"
let persistenceId = $"order/default-shard/{id}"
command.Parameters.AddWithValue("$id", persistenceId) |> ignore
use rows = command.ExecuteReader()
printfn $"Journal of order/default-shard/{id}:"
while rows.Read() do
// The manifest names the stored type, for example
// "FCQRS.Workflows.WorkflowRecord+Started, FCQRS.Workflows".
let manifest = rows.GetString 1
let record = manifest.Split(',').[0].Split('+').[1]
let number = rows.GetInt64 0
printfn $" {number} {record}"
printJournal "o-widget"
api.Stop().Wait()
printJournal: 'a -> unitid: 'aconnection: SqliteConnectionMicrosoft.Data.Sqlite.SqliteConnectionRepresents a connection to a SQLite database. Connection Strings Async Limitations
Open: unit -> unitOpens a connection to the database using the value of . If Mode=ReadWriteCreate is used (the default) the file is created, if it doesn't already exist. A SQLite error occurs while opening the connection.
command: SqliteCommandCreateCommand: unit -> SqliteCommandCreates a new command associated with the connection. The new command. The command's property will also be set to the current transaction.
CommandText: stringGets or sets the SQL to execute against the database. The SQL to execute against the database. Batching
persistenceId: stringAddWithValue: string * obj -> SqliteParameterAdds a parameter to the collection. The name of the parameter. The value of the parameter. Can be null. The parameter that was added. Parameters Data Types
Parameters: SqliteParameterCollectionGets the collection of parameters used by the command. The collection of parameters used by the command. Parameters
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
ignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
rows: SqliteDataReaderExecuteReader: unit -> SqliteDataReaderExecutes the against the database and returns a data reader. The data reader. A SQLite error occurs during execution. Database Errors Batching
printfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
Read: unit -> boolAdvances to the next row in the result set. if there are more rows; otherwise, .
manifest: stringGetString: int -> stringGets the value of the specified column as a . The zero-based column ordinal. The value of the column.
record: stringSplit: char * StringSplitOptions -> string arraynumber: int64GetInt64: int -> int64Gets the value of the specified column as a . The zero-based column ordinal. The value of the column.
api: IActorStop: unit -> TaskStops the actor system gracefully.
Wait: unit -> unitWaits for the to complete execution. The has been disposed. The task was canceled. The collection contains a object. -or- An exception was thrown during the execution of the task. The collection contains information about the exception or exceptions.
// Applications do not read this table. This program reads it only to show
// what FCQRS stored for an order.
void PrintJournal(string id)
{
using var connection = new SqliteConnection($"Data Source={database}");
connection.Open();
var command = connection.CreateCommand();
command.CommandText = """
SELECT sequence_number, manifest FROM journal
WHERE persistence_id = $id
ORDER BY sequence_number
""";
command.Parameters.AddWithValue("$id", $"order/default-shard/{id}");
using var rows = command.ExecuteReader();
Console.WriteLine($"Journal of order/default-shard/{id}:");
while (rows.Read())
{
// The manifest names the stored type, for example
// "FCQRS.Workflows.WorkflowRecord+Started, FCQRS.Workflows".
var record = rows.GetString(1).Split(',')[0].Split('+')[1];
Console.WriteLine($" {rows.GetInt64(0)} {record}");
}
}
PrintJournal("o-widget");
await actor.Stop();
Make the stand-ins quiet
Two orders now call the stand-ins at the same time. The stand-ins stop printing, because the lines of two orders would mix in a different order on every run. They also lock their dictionaries, because two calls can change them at once:
// A stand-in for the warehouse. It has no gizmos, and it is busy for the
// first two calls with a key. It remembers keys, so a repeated call returns
// the first reservation. It prints nothing, and a lock keeps it correct
// when two orders call it at once.
type Warehouse() =
let calls = Dictionary<string, int>()
let reservations = Dictionary<string, string>()
interface IWarehouse with
member _.Reserve(key, item, _) =
lock calls (fun () ->
if item = "gizmo" then
invalidOp "Out of stock"
calls[key] <- (match calls.TryGetValue key with
| true, count -> count + 1
| _ -> 1)
if calls[key] <= 2 then
invalidOp "The warehouse is busy"
if not (reservations.ContainsKey key) then
reservations[key] <- $"r-{reservations.Count + 1}"
Task.FromResult reservations[key])
member _.Release(_, reservation) = Task.FromResult reservation
Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Warehousecalls: Dictionary<string,int>``.ctor``: unit -> unitInitializes a new instance of the class that is empty, has the default initial capacity, and uses the default equality comparer for the key type.
stringAn abbreviation for the CLI type . Basic Types
intAn abbreviation for the CLI type . Basic Types
reservations: Dictionary<string,string>Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Order.IWarehouse_: WarehouseReserve: Warehouse -> string * string * int -> Task<string>key: stringitem: stringlock: 'Lock -> (unit -> 'T) -> 'TExecute the function as a mutual-exclusion region using the input value as a lock. The object to be locked. The action to perform during the lock. The resulting value. open System.Linq /// A counter object, supporting unlocked and locked increment type TestCounter () = let mutable count = 0 /// Increment the counter, unlocked member this.IncrementWithoutLock() = count <- count + 1 /// Increment the counter, locked member this.IncrementWithLock() = lock this (fun () -> count <- count + 1) /// Get the count member this.Count = count let counter = TestCounter() // Create a parallel sequence to that uses all our CPUs (seq {1..100000}).AsParallel() .ForAll(fun _ -> counter.IncrementWithoutLock()) // Evaluates to a number between 1-100000, non-deterministically because there is no locking counter.Count let counter2 = TestCounter() // Create a parallel sequence to that uses all our CPUs (seq {1..100000}).AsParallel() .ForAll(fun _ -> counter2.IncrementWithLock()) // Evaluates to 100000 deterministically because the increment to the counter object is locked counter2.Count
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
invalidOp: string -> 'TThrow a exception The exception message. The result value. type FileReader(fileName: string) = let mutable isOpen = false member this.Open() = if isOpen then invalidOp "File is already open" // ... Here we may open the file ... isOpen <- true let reader = FileReader("journal.txt") reader.Open() // Executes fine reader.Open() // Throws System.InvalidOperationException: File is already open
Item: intGets or sets the value associated with the specified key. The key of the value to get or set. is . The property is retrieved and does not exist in the collection. The value associated with the specified key. If the specified key is not found, a get operation throws a , and a set operation creates a new element with the specified key.
TryGetValue: string * byref<int> -> boolGets the value associated with the specified key. The key of the value to get. When this method returns, contains the value associated with the specified key, if the key is found; otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. is . if the contains an element with the specified key; otherwise, .
count: int(+): ^T1 -> ^T2 -> ^T3Overloaded addition operator The first parameter. The second parameter. The result of the operation. 2 + 2 // Evaluates to 4 "Hello " + "World" // Evaluates to "Hello World"
(<=): 'T -> 'T -> boolStructural less-than-or-equal comparison The first parameter. The second parameter. The result of the comparison. 5 <= 1 // Evaluates to false 5 <= 5 // Evaluates to true [1; 5] <= [1; 6] // Evaluates to true
``not``: bool -> boolNegate a logical value. Not True equals False and not False equals True The value to negate. The result of the negation. not (2 + 2 = 5) // Evaluates to true // not is a function that can be compose with other functions let fileDoesNotExist = System.IO.File.Exists >> not
ContainsKey: string -> boolDetermines whether the contains the specified key. The key to locate in the . is . if the contains an element with the specified key; otherwise, .
Item: stringGets or sets the value associated with the specified key. The key of the value to get or set. is . The property is retrieved and does not exist in the collection. The value associated with the specified key. If the specified key is not found, a get operation throws a , and a set operation creates a new element with the specified key.
System.Threading.Tasks.TaskRepresents an asynchronous operation.
FromResult: 'TResult -> Task<'TResult>Creates a that's completed successfully with the specified result. The result to store into the completed task. The type of the result returned by the task. The successfully completed task.
Release: Warehouse -> string * string -> Task<string>reservation: string// A stand-in for the warehouse. It has no gizmos, and it is busy for the
// first two calls with a key. It remembers keys, so a repeated call returns
// the first reservation. It prints nothing, and a lock keeps it correct
// when two orders call it at once.
public sealed class Warehouse : IWarehouse
{
private readonly Dictionary<string, int> calls = new();
private readonly Dictionary<string, string> reservations = new();
public Task<string> Reserve(string key, string item, int quantity)
{
lock (calls)
{
if (item == "gizmo")
throw new InvalidOperationException("Out of stock");
calls[key] = calls.GetValueOrDefault(key) + 1;
if (calls[key] <= 2)
throw new InvalidOperationException("The warehouse is busy");
if (!reservations.ContainsKey(key))
reservations[key] = $"r-{reservations.Count + 1}";
return Task.FromResult(reservations[key]);
}
}
public Task<string> Release(string key, string reservation) =>
Task.FromResult(reservation);
}
The carrier's crash becomes stop. With stop, the first ship call waits and the second ends
the process. At that moment o-widget and o-gadget have each stored that ship started, and
neither has a result. Because the carrier waits for the second call, this is the state on every
machine. Without stop, a call takes two seconds; Task.Delay replaces Thread.Sleep, so two
calls that wait do not hold two threads:
// A stand-in for the carrier. It does not take crates, and a call takes two
// seconds. With `stop`, the first call waits for a second one, and the
// second stops the process while both orders are inside `ship`.
type Carrier(stop: bool) =
let shipments = Dictionary<string, string>()
let mutable calls = 0
interface ICarrier with
member _.Ship(key, _, item) =
task {
if item = "crate" then
invalidOp "The carrier does not take crates"
if stop then
if Interlocked.Increment &calls = 1 then
do! Task.Delay Timeout.Infinite
printfn
" the process stops while two orders are being shipped"
exit 1
do! Task.Delay 2000
return
lock shipments (fun () ->
if not (shipments.ContainsKey key) then
shipments[key] <- $"s-{shipments.Count + 1}"
shipments[key])
}
Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Carrierstop: boolboolAn abbreviation for the CLI type . Basic Types
shipments: Dictionary<string,string>``.ctor``: unit -> unitInitializes a new instance of the class that is empty, has the default initial capacity, and uses the default equality comparer for the key type.
stringAn abbreviation for the CLI type . Basic Types
calls: intFcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Order.ICarrier_: CarrierShip: Carrier -> string * string * string -> Task<string>key: stringitem: stringtask: TaskBuilderBuilds a task using computation expression syntax.
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
invalidOp: string -> 'TThrow a exception The exception message. The result value. type FileReader(fileName: string) = let mutable isOpen = false member this.Open() = if isOpen then invalidOp "File is already open" // ... Here we may open the file ... isOpen <- true let reader = FileReader("journal.txt") reader.Open() // Executes fine reader.Open() // Throws System.InvalidOperationException: File is already open
System.Threading.InterlockedProvides atomic operations for variables that are shared by multiple threads.
Increment: byref<int> -> intIncrements a specified variable and stores the result, as an atomic operation. The variable whose value is to be incremented. The address of is a pointer. The value of the variable immediately after the increment operation finished.
(~&): 'T -> byref<'T>Address-of. Uses of this value may result in the generation of unverifiable code. The input object. The managed pointer.
System.Threading.Tasks.TaskRepresents an asynchronous operation.
Delay: int -> TaskCreates a task that completes after a specified number of milliseconds. The number of milliseconds to wait before completing the returned task, or -1 to wait indefinitely. The argument is less than -1. A task that represents the time delay.
System.Threading.TimeoutContains constants that specify infinite time-out intervals. This class cannot be inherited.
Infinite: intA constant used to specify an infinite waiting period, for threading methods that accept an parameter.
printfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
exit: int -> 'TExit the current hardware isolated process, if security settings permit, otherwise raise an exception. Calls . The exit code to use. Never returns. [<EntryPoint>] let main argv = if argv.Length = 0 then eprintfn "You must provide arguments" exit(-1) // Causes program to quit with an error code printfn "Argument count: %i" argv.Length 0
lock: 'Lock -> (unit -> 'T) -> 'TExecute the function as a mutual-exclusion region using the input value as a lock. The object to be locked. The action to perform during the lock. The resulting value. open System.Linq /// A counter object, supporting unlocked and locked increment type TestCounter () = let mutable count = 0 /// Increment the counter, unlocked member this.IncrementWithoutLock() = count <- count + 1 /// Increment the counter, locked member this.IncrementWithLock() = lock this (fun () -> count <- count + 1) /// Get the count member this.Count = count let counter = TestCounter() // Create a parallel sequence to that uses all our CPUs (seq {1..100000}).AsParallel() .ForAll(fun _ -> counter.IncrementWithoutLock()) // Evaluates to a number between 1-100000, non-deterministically because there is no locking counter.Count let counter2 = TestCounter() // Create a parallel sequence to that uses all our CPUs (seq {1..100000}).AsParallel() .ForAll(fun _ -> counter2.IncrementWithLock()) // Evaluates to 100000 deterministically because the increment to the counter object is locked counter2.Count
``not``: bool -> boolNegate a logical value. Not True equals False and not False equals True The value to negate. The result of the negation. not (2 + 2 = 5) // Evaluates to true // not is a function that can be compose with other functions let fileDoesNotExist = System.IO.File.Exists >> not
ContainsKey: string -> boolDetermines whether the contains the specified key. The key to locate in the . is . if the contains an element with the specified key; otherwise, .
Item: stringGets or sets the value associated with the specified key. The key of the value to get or set. is . The property is retrieved and does not exist in the collection. The value associated with the specified key. If the specified key is not found, a get operation throws a , and a set operation creates a new element with the specified key.
// A stand-in for the carrier. It does not take crates, and a call takes two
// seconds. With `stop`, the first call waits for a second one, and the
// second stops the process while both orders are inside `ship`.
public sealed class Carrier(bool stop) : ICarrier
{
private readonly Dictionary<string, string> shipments = new();
private int calls;
public async Task<string> Ship(string key, string reservation, string item)
{
if (item == "crate")
throw new InvalidOperationException(
"The carrier does not take crates");
if (stop)
{
if (Interlocked.Increment(ref calls) == 1)
await Task.Delay(Timeout.Infinite);
Console.WriteLine(
" the process stops while two orders are being shipped");
Environment.Exit(1);
}
await Task.Delay(2000);
lock (shipments)
{
if (!shipments.ContainsKey(key))
shipments[key] = $"s-{shipments.Count + 1}";
return shipments[key];
}
}
}
The fraud check passes every order, as before, and prints nothing:
// A stand-in for the fraud check. It passes every order and prints nothing.
type FraudCheck() =
interface IFraudCheck with
member _.Check(_, _, _) = Task.FromResult true
Fcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.FraudCheckFcqrs_350-workflow-tutorial_010-feed-orders-from-a-stream.md_page.Order.IFraudCheck_: FraudCheckCheck: FraudCheck -> string * string * decimal -> Task<bool>System.Threading.Tasks.TaskRepresents an asynchronous operation.
FromResult: 'TResult -> Task<'TResult>Creates a that's completed successfully with the specified result. The result to store into the completed task. The type of the result returned by the task. The successfully completed task.
// A stand-in for the fraud check. It passes every order and prints nothing.
public sealed class FraudCheck : IFraudCheck
{
public Task<bool> Check(string key, string customer, decimal total) =>
Task.FromResult(true);
}
Replace the registration. It reads --stop, and it deposits 500 for ada before it registers the
workflow:
// Register the account rules; the workflow sends commands to them.
let accounts =
Fcqrs.aggregate api {
Name = "Account"
Initial = initial
Decide = decide
Fold = fold
Snapshots = Default
Passivation = PassivationPolicy.Default
}
// Finish the aggregate startup. This program has no sagas.
Fcqrs.wireSagaStarters api []
// Put money in a customer's account.
let deposit customer amount =
accounts.Send
(Fcqrs.newCid ()) (Fcqrs.aggregateId customer) (Deposit amount)
(fun _ -> true)
|> Async.RunSynchronously
|> ignore
printfn $"Deposited {amount} to {customer}"
// Ada pays for the orders. The deposit comes before the workflow is
// registered, so the program reads the status of the resumed orders right
// after registering, while they are still shipping.
deposit "ada" 500m
// Run the program with --stop to stop it while two orders are shipping.
let stop = Environment.GetCommandLineArgs() |> Array.contains "--stop"
// Registering the workflow also resumes the orders that were running when
// the process last stopped.
let orders =
Workflows.register
api (definition (Warehouse()) (Carrier stop) accounts (FraudCheck()))
accounts: AggregateHandle<AccountCommand,AccountEvent>FCQRS.FSharp.Fcqrsaggregate: IActor -> Aggregate<'State,'Command,'Event> -> AggregateHandle<'Command,'Event>Register an aggregate and return its typed handle. Calling this IS the registration (it initializes the sharding region).
api: IActorName: stringInitial: 'Stateinitial: AccountStateDecide: Command<'Command> -> 'State -> EventAction<'Event>handleCommand (decide): command + current state -> what to do.
decide: Command<AccountCommand> -> AccountState -> EventAction<AccountEvent>Fold: Event<'Event> -> 'State -> 'StateapplyEvent (fold): event + current state -> next state (pure).
fold: Event<AccountEvent> -> AccountState -> AccountStateSnapshots: SnapshotPolicySnapshot cadence: Default (config / 30), NoSnapshots, or Every n.
DefaultUse the global config (config:akka:persistence:snapshot-version-count), or 30.
Passivation: PassivationPolicyIdle passivation: PassivationPolicy.Default (configuration, then Akka's 120s), After an idle period, or Never.
FCQRS.Common.PassivationPolicyIdle passivation for an aggregate type, set per entity at registration. Passivation stops an idle actor and releases its in-memory state; the next command recovers it from the journal. Only messages routed through cluster sharding count as activity. Sagas ignore this: their shard regions remember entities, which disables idle passivation in Akka.NET. A saga stops at StopSaga or abort instead.
DefaultUse configuration: `akka.cluster.sharding.<EntityName>.passivate-idle-entity-after`, then `akka.cluster.sharding.passivate-idle-entity-after`, then Akka.NET's 120s.
wireSagaStarters: IActor -> SagaHandle list -> unitWire every registered saga into one saga-starter (or the empty starter if none). Call after the aggregates + sagas are registered.
deposit: string -> decimal -> unitcustomer: stringamount: decimalSend: FCQRS.Model.Data.CID -> FCQRS.Model.Data.AggregateId -> 'Command -> ('Event -> bool) -> Async<Event<'Event>>Send a command and await the first matching aggregate event. Fails with SagaAlreadyStartedException, with nothing stored, when the command would start a saga that its correlation ID already started on this aggregate.
newCid: unit -> FCQRS.Model.Data.CIDA fresh correlation id (UUID v7).
aggregateId: string -> FCQRS.Model.Data.AggregateIdAn aggregate id from a string (e.g. a document/user key). Any non-blank id works: the shard names entity actors Uri.EscapeDataString(entityId), so characters Akka actor names would reject directly (spaces, %) are escaped before they reach an actor path.
Deposit(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
Microsoft.FSharp.Control.FSharpAsyncHolds static members for creating and manipulating asynchronous computations. See also F# Language Guide - Async Workflows. Async Programming
RunSynchronously: Async<'T> * int option * CancellationToken option -> 'TRuns the asynchronous computation and await its result. If an exception occurs in the asynchronous computation then an exception is re-raised by this function. If no cancellation token is provided then the default cancellation token is used. The computation is started on the current thread if is null, has of true, and no timeout is specified. Otherwise the computation is started by queueing a new work item in the thread pool, and the current thread is blocked awaiting the completion of the computation. The timeout parameter is given in milliseconds. A value of -1 is equivalent to . The computation to run. The amount of time in milliseconds to wait for the result of the computation before raising a . If no value is provided for timeout then a default of -1 is used to correspond to . The cancellation token to be associated with the computation. If one is not supplied, the default cancellation token is used. The result of the computation. Starting Async Computations printfn "A" let result = async { printfn "B" do! Async.Sleep(1000) printfn "C" 17 } |> Async.RunSynchronously printfn "D" Prints "A", "B" immediately, then "C", "D" in 1 second. result is set to 17.
ignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
printfn: Printf.TextWriterFormat<'T> -> 'TPrint to stdout using the given format, and add a newline. The formatter. The formatted result. See Printf.printfn (link: ) for examples.
stop: boolSystem.EnvironmentProvides information about, and means to manipulate, the current environment and platform. This class cannot be inherited.
GetCommandLineArgs: unit -> string arrayReturns a string array containing the command-line arguments for the current process. The system does not support command-line arguments. An array of strings where each element contains a command-line argument. The first element is the executable file name, and the following zero or more elements contain the remaining command-line arguments.
Microsoft.FSharp.Collections.ArrayModuleContains operations for working with arrays. See also F# Language Guide - Arrays.
contains: 'T -> 'T array -> boolTests if the array contains the specified element. The value to locate in the input array. The input array. True if the input array contains the specified element; false otherwise. Thrown when the input array is null. [| 1; 2 |] |> Array.contains 2 // evaluates to true [| 1; 2 |] |> Array.contains 5 // evaluates to false
orders: WorkflowHandle<OrderRequest,OrderResult>FCQRS.Workflows.WorkflowsRegisters workflows with an actor system.
register: IActor -> WorkflowDefinition<'Input,'Result> -> WorkflowHandle<'Input,'Result>Registers the workflow and returns its handle. Instances that were running when the process last stopped resume as soon as this is called.
definition: IWarehouse -> ICarrier -> AggregateHandle<AccountCommand,AccountEvent> -> IFraudCheck -> WorkflowDefinition<OrderRequest,OrderResult>``.ctor``: unit -> Warehouse``.ctor``: bool -> Carrier``.ctor``: unit -> FraudCheck// Register the account rules; the workflow sends commands to them.
var accounts = new Account().Init(actor);
// Finish the aggregate startup. This program has no sagas.
ActorWiring.InitSagaStarterEmpty(actor);
// Put money in a customer's account.
async Task Deposit(string customer, decimal amount)
{
await accounts.Handler(_ => true, Values.NewCID(),
Values.CreateAggregateId(customer), new Deposit(amount));
Console.WriteLine($"Deposited {amount} to {customer}");
}
// Ada pays for the orders. The deposit comes before the workflow is
// registered, so the program reads the status of the resumed orders right
// after registering, while they are still shipping.
await Deposit("ada", 500m);
// Run the program with --stop to stop it while two orders are shipping.
var stop = args.Contains("--stop");
// Registering the workflow also resumes the orders that were running when
// the process last stopped.
var orders = WorkflowHost.Register(actor, new OrderWorkflow(
new Warehouse(), new Carrier(stop), accounts.Factory, new FraudCheck()));
The deposit comes before the workflow's registration because of the second run. Registering
resumes the orders that were running, and their ship call takes two seconds. The program asks
for their status right after registering, so it finds them still in ship. A deposit between the
two would delay the question.
Run it with --stop
Delete bin/Debug/net11.0/orders.db, then run the program with --stop:
dotnet run -- --stop
Deposited 500 to ada
the process stops while two orders are being shipped
The stream took widget and gadget, its limit of two. Both orders reserved stock, paid, waited
for packing, and called the carrier, and the second call stopped the process. The program printed
no outcome, because no order had ended.
Run it again
Keep orders.db and run the program without the flag:
dotnet run
Deposited 500 to ada
o-widget is running: 6 ship
o-gadget is running: 6 ship
o-widget finished: shipped
o-gadget finished: shipped
o-gizmo finished: out of stock
o-crate faulted: The carrier does not take crates
o-sprocket finished: shipped
At most 2 orders were in the stream at once
Journal of order/default-shard/o-widget:
1 Started
2 StepScheduled
3 StepCompleted
4 StepCompleted
5 StepCompleted
6 StepCompleted
7 StepScheduled
8 StepCompleted
9 StepScheduled
10 StepCompleted
11 StepScheduled
12 StepCompleted
13 StepScheduled
14 StepCompleted
15 Finished
Registering the workflow resumed o-widget and o-gadget. Both were running at ship, position
6, before the stream handed over any element.
The stream did not resume. Nothing stored which elements it had taken, so the program fed the whole
batch again. For widget and gadget, the start found an order with that ID and stored nothing,
and the flow waited for the resumed order to end. The other three elements started new orders.
The outcomes came in the batch's order. o-crate faulted at the carrier and was emitted like the
others, and the stream went on to o-sprocket. At most two orders were in the stream at once.
The journal of o-widget has one Started record: the element fed again stored nothing. Record 6
is the version step from step 9, which gave the new order 2. Record 13 is ship's start from the
first run, and record 14 its result from the second.
What the stream does not do
The stream is not durable. When the process stops, the orders resume and the stream does not. This program could feed the batch again because the batch is in its code. A real source must replay from an offset of its own: a queue delivers again the messages that were not acknowledged, and a reader of a file stores how far it got. Acknowledge an element, or store the offset, only after the flow emits its outcome.
An element that arrives again reaches its order through the ID. If the order is still running, the flow waits for it; if it has ended, the flow emits the stored outcome. Either way the start stores nothing.
When the source needs only the start stored, not the outcome, WorkflowFlow.start
(WorkflowFlow.Start in C#) emits each element's ID as soon as its start is stored.
What you built
The order workflow now reserves stock with retries, takes its time and confirmation number from steps, waits for an approval signal, charges an aggregate, releases the stock when the order is not approved or not paid, catches the failure it has an answer for, and survives a process that stops in the middle of a step and a change to its own code. A stream feeds it orders, two at a time, and an element that arrives again reaches the order it started.
The workflow concept page covers what the tutorial left out: running steps together, repeating steps, child workflows, cancellation, and the C# API in full. Write a workflow is the same material as a task guide, and Observability shows the traces, metrics, and logs that each order produces.