Step 1: Place an order
This tutorial builds an order workflow one step at a time. An order reserves stock at a warehouse, waits for a person to approve a large total, charges the customer, and ships. In this step the workflow does one thing: it reserves the stock.
Each page shows every line of the program. Start in an empty folder, add what the page shows, and run it. The CQRS tutorial, which starts at Open an account, is not required before this one.
What a workflow is
Placing an order takes several calls to other systems, and the process can stop between any two of them. A workflow is a function that FCQRS runs for you and records as it goes. After each step, FCQRS stores the step's result in a journal. When the process starts again, FCQRS runs the function from the beginning, but takes each finished step's result from the journal instead of running the step again.
Each order is one instance of the workflow, identified by an ID such as o-1.
Create the project
Create a folder named Orders and put these files in it. The project file names the packages:
FCQRS.Workflows, which brings in FCQRS, and SQLite, where the journal is stored.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Order.fs" />
<Compile Include="Program.fs" />
<PackageReference Include="FCQRS.Workflows" Version="7.7.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3"
Version="3.0.2" />
<PackageReference Include="SourceGear.sqlite3" Version="3.50.4.5" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FCQRS.Workflows" Version="7.7.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3"
Version="3.0.2" />
<PackageReference Include="SourceGear.sqlite3" Version="3.50.4.5" />
</ItemGroup>
</Project>
The file is Orders.fsproj in F# and Orders.csproj in C#. The programs target .NET 11, and the
C# program uses C# 15 from step 6 on. This global.json selects the
.NET 11 SDK:
{
"sdk": {
"version": "11.0.100",
"rollForward": "latestFeature"
}
}
The program's code goes in two files: Order.fs (Order.cs in C#) for the workflow, and
Program.fs (Program.cs) for the program that runs it.
Name the input and the result
module Order
open System.Threading.Tasks
open FCQRS.Workflows
// 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 = { Reservation: string }
Fcqrs_350-workflow-tutorial_001-place-an-order.md_page.OrderSystemThreadingTasksFCQRSWorkflowsFcqrs_350-workflow-tutorial_001-place-an-order.md_page.Order.OrderRequestCustomer: stringstringAn abbreviation for the CLI type . Basic Types
Item: stringQuantity: intintAn abbreviation for the CLI type . Basic Types
UnitPrice: decimaldecimalAn abbreviation for the CLI type . Basic Types
Fcqrs_350-workflow-tutorial_001-place-an-order.md_page.Order.OrderResultReservation: stringusing FCQRS.Workflows;
using FCQRS.Workflows.CSharp;
// What an order workflow is started with. FCQRS stores it in the journal.
public sealed record OrderRequest(
string Customer, string Item, int Quantity, decimal UnitPrice);
// What the workflow returns. FCQRS stores it too.
public sealed record OrderResult(string Reservation);
A workflow takes one input and returns one result. FCQRS stores both as JSON, so their field names are part of what is stored. After a field is renamed, the new code does not find that field in the orders already stored.
Describe the warehouse
// 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>
Fcqrs_350-workflow-tutorial_001-place-an-order.md_page.Order.IWarehouseReserve: IWarehouse -> string * string * int -> Task<string>keystringAn abbreviation for the CLI type . Basic Types
itemquantityintAn abbreviation for the CLI type . Basic Types
System.Threading.Tasks.Task`1Represents an asynchronous operation that can return a value. The type of the result produced by this .
// 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.
public interface IWarehouse
{
Task<string> Reserve(string key, string item, int quantity);
}
The workflow reaches the warehouse through this interface. Reserve returns a reservation ID.
The key is explained with the stand-in warehouse below.
Write the workflow
// "order" is stored with every instance. It must not change.
let definition (warehouse: IWarehouse) =
Workflow.define "order" (fun (request: OrderRequest) ->
workflow {
// An activity is the only place the body calls another system.
// FCQRS stores its result. "reserve-stock" is stored too and
// must not change while orders are running.
let! reservation =
activity "reserve-stock" Retry.none (fun attempt ->
warehouse.Reserve(
attempt.IdempotencyKey, request.Item, request.Quantity))
return { Reservation = reservation }
})
definition: IWarehouse -> WorkflowDefinition<OrderRequest,OrderResult>warehouse: IWarehouseFcqrs_350-workflow-tutorial_001-place-an-order.md_page.Order.IWarehouseFCQRS.Workflows.WorkflowDefines workflows.
define: string -> ('Input -> Workflow<'Result>) -> WorkflowDefinition<'Input,'Result>A workflow named `name` whose instances run `body` on their input.
request: OrderRequestFcqrs_350-workflow-tutorial_001-place-an-order.md_page.Order.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.
FCQRS.Workflows.RetryCommon retry policies.
none: RetryPolicyOne attempt.
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.
Item: stringQuantity: intReservation: string// "order" is stored with every instance. It must not change.
public sealed class OrderWorkflow(IWarehouse warehouse)
: Workflow<OrderRequest, OrderResult>("order")
{
public override async WorkflowTask<OrderResult> Run(
WorkflowContext ctx, OrderRequest request)
{
// An activity is the only place the body calls another system.
// FCQRS stores its result. "reserve-stock" is stored too and
// must not change while orders are running.
var reservation = await ctx.Activity("reserve-stock", Retry.none,
attempt => warehouse.Reserve(
attempt.IdempotencyKey, request.Item, request.Quantity));
return new OrderResult(reservation);
}
}
The function that receives the request is the workflow's body. Its one step is an
activity: a call to another system, named reserve-stock. Retry.none makes one attempt; step
2 adds retries.
In F#, the body is a workflow { } block, and let! waits for a step. In C#, the body is the
Run method, and await on a ctx method waits for a step.
The workflow's name, order, and the step's name, reserve-stock, are stored with every order.
They identify the stored records when FCQRS reads them back, so changing either name breaks the
orders that are already stored. Step 8 shows what happens when the steps
change.
Start FCQRS
open System
open System.Collections.Generic
open System.IO
open Microsoft.Data.Sqlite
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.Logging
open FCQRS.Actor
open FCQRS.FSharp
open FCQRS.Workflows
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"
SystemCollectionsGenericIOMicrosoftDataSqliteExtensionsConfigurationLoggingFCQRSFCQRS.ActorFCQRS.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_001-place-an-order.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: FCQRS.Common.IActoractor: IConfiguration -> ILoggerFactory -> Connection option -> string -> FCQRS.Common.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 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");
This is the top of Program.fs (Program.cs). It starts FCQRS with its journal in orders.db,
next to the compiled program.
Add a stand-in warehouse
// A stand-in for the warehouse. It remembers keys, so a repeated call
// returns the first reservation instead of reserving again.
type Warehouse() =
let reservations = Dictionary<string, string>()
interface IWarehouse with
member _.Reserve(key, item, quantity) =
task {
if not (reservations.ContainsKey key) then
reservations[key] <- $"r-{reservations.Count + 1}"
printfn
$" warehouse reserved {quantity} {item} (key {key})"
return reservations[key]
}
Fcqrs_350-workflow-tutorial_001-place-an-order.md_page.Warehousereservations: 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
Fcqrs_350-workflow-tutorial_001-place-an-order.md_page.Order.IWarehouse_: WarehouseReserve: Warehouse -> string * string * int -> Threading.Tasks.Task<string>key: stringitem: stringquantity: inttask: TaskBuilderBuilds a task using computation expression syntax.
``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.
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.
// A stand-in for the warehouse. It remembers keys, so a repeated call
// returns the first reservation instead of reserving again.
public sealed class Warehouse : IWarehouse
{
private readonly Dictionary<string, string> reservations = new();
public Task<string> Reserve(string key, string item, int quantity)
{
if (!reservations.ContainsKey(key))
{
reservations[key] = $"r-{reservations.Count + 1}";
Console.WriteLine(
$" warehouse reserved {quantity} {item} (key {key})");
}
return Task.FromResult(reservations[key]);
}
}
In F#, this type follows the startup code. In C#, it goes at the end of Program.cs, because a
file with top-level statements declares its types after them.
The program has no real warehouse, so this class stands in for one and prints each reservation.
It reserves once per key. FCQRS gives every attempt of one step the same key, the
idempotency key, such as order/o-1/0: the workflow's name, the order's ID, and the step's
position. A real warehouse that remembers keys in the same way can tell a repeated call from a new
one.
A repeated call can happen. FCQRS stores a step's result after the call returns, so a process that stops between the two runs the step again when it starts. An activity runs at least once.
Register the workflow
// Registering the workflow also resumes the orders that were running when
// the process last stopped.
let orders = Workflows.register api (definition (Warehouse()))
orders: WorkflowHandle<OrderRequest,OrderResult>FCQRS.Workflows.WorkflowsRegisters workflows with an actor system.
register: FCQRS.Common.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.
api: FCQRS.Common.IActordefinition: IWarehouse -> WorkflowDefinition<OrderRequest,OrderResult>``.ctor``: unit -> Warehouse// Registering the workflow also resumes the orders that were running when
// the process last stopped.
var orders = WorkflowHost.Register(actor, new OrderWorkflow(new Warehouse()));
orders starts orders and reads their state.
Start an order
let request = {
Customer = "ada"
Item = "widget"
Quantity = 2
UnitPrice = 40m
}
// Start an order. A start with an ID that is already in the journal stores
// nothing and returns the order's status.
let start id =
match orders.Start id request |> Async.RunSynchronously with
| StartResult.Started -> printfn $"Started {id}"
| StartResult.AlreadyStarted status ->
printfn $"{id} is already started ({status})"
start "o-1"
// Wait for the order to end. The workflow does not depend on this wait.
let outcome =
orders.Result "o-1" (TimeSpan.FromSeconds 30.0) |> Async.RunSynchronously
match outcome with
| WorkflowOutcome.Finished result ->
printfn $"o-1 finished: reservation {result.Reservation}"
| other -> printfn $"o-1 ended: {other}"
start "o-1"
request: OrderRequestCustomer: stringItem: stringQuantity: intUnitPrice: decimalstart: string -> unitid: stringorders: WorkflowHandle<OrderRequest,OrderResult>Start: string -> 'Input -> Async<StartResult>Starts the workflow with this ID. Returns once the start is stored. A second start with the same ID stores nothing and reports the existing instance.
(|>): '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 * Threading.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.StartResultThe answer to a start request.
StartedThis request started the workflow.
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.
AlreadyStartedA workflow with this ID already exists. Nothing was stored.
status: WorkflowStatusoutcome: WorkflowOutcome<OrderResult>Result: string -> TimeSpan -> Async<WorkflowOutcome<'Result>>Waits up to the given time for the instance to end. The wait is in memory: the workflow continues when the caller stops waiting. Raises `TimeoutException` when the time passes, and `InvalidOperationException` when no workflow with this ID was started.
System.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 .
FCQRS.Workflows.WorkflowOutcome`1How a workflow ended.
FinishedThe body returned this value.
result: OrderResultother: WorkflowOutcome<OrderResult>var request = new OrderRequest("ada", "widget", 2, 40m);
// Start an order. A start with an ID that is already in the journal stores
// nothing and returns false.
async Task Start(string id)
{
if (await orders.StartAsync(id, request))
Console.WriteLine($"Started {id}");
else
Console.WriteLine(
$"{id} is already started ({await orders.StatusAsync(id)})");
}
await Start("o-1");
// Wait for the order to end. The workflow does not depend on this wait.
var result = await orders.ResultAsync("o-1", TimeSpan.FromSeconds(30));
Console.WriteLine($"o-1 finished: reservation {result.Reservation}");
await Start("o-1");
Starting o-1 stores the request and returns. The order runs on its own; waiting for its result
is optional. The program starts o-1 a second time to show that an ID starts one order only.
Look at the journal
// Applications do not read this table. This program reads it only to show
// what FCQRS stored for o-1.
let printJournal () =
use connection = new SqliteConnection($"Data Source={database}")
connection.Open()
let command = connection.CreateCommand()
command.CommandText <-
"SELECT sequence_number, manifest FROM journal
WHERE persistence_id = 'order/default-shard/o-1'
ORDER BY sequence_number"
use rows = command.ExecuteReader()
printfn "Journal of order/default-shard/o-1:"
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 ()
api.Stop().Wait()
printJournal: unit -> unitconnection: 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
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: FCQRS.Common.IActorStop: unit -> Threading.Tasks.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 o-1.
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 = 'order/default-shard/o-1'
ORDER BY sequence_number
""";
using var rows = command.ExecuteReader();
Console.WriteLine("Journal of order/default-shard/o-1:");
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}");
}
}
await actor.Stop();
This is the end of the program's statements. It prints the kind of each record FCQRS stored for
o-1, and stops FCQRS.
Run it
From the Orders folder:
dotnet run
Started o-1
warehouse reserved 2 widget (key order/o-1/0)
o-1 finished: reservation r-1
o-1 is already started (Completed)
Journal of order/default-shard/o-1:
1 Started
2 StepScheduled
3 StepCompleted
4 Finished
The order ran its one step and finished. The second start found o-1 in the journal, stored
nothing, and returned its status, Completed.
order/default-shard/o-1 identifies the order in the journal. Its four records are:
Started: the request, stored before the start returned.StepScheduled:reserve-stockbegan, at position 0. A step with this record and no result was running when the process stopped, so FCQRS runs it again.StepCompleted: the step's result,r-1.Finished: the workflow's result.
Run it again
o-1 is already started (Completed)
o-1 finished: reservation r-1
o-1 is already started (Completed)
Journal of order/default-shard/o-1:
1 Started
2 StepScheduled
3 StepCompleted
4 Finished
The journal in orders.db still holds o-1, so the first start stores nothing, and the result
comes from the journal. The warehouse prints nothing: it is not called again. Each step from here
on starts new orders, so delete bin/Debug/net11.0/orders.db before you run the next step's
program.
Next
Step 2: Retry the warehouse makes the warehouse fail some calls and gives the step a retry policy.