Logo Orsak

Testing

An effect doesn't do anything until it is run, and everything it needs comes from the environment it is run with. Testing an effect is therefore a matter of running it with an environment of fakes, and checking its result. There is no container to configure and nothing to mock by reflection: a test environment is an ordinary type, or an object expression, implementing the provider interfaces the effect asks for.

This page tests a small feature that uses Orsak's built-in effects for ids, randomness and time. It is a script: it runs with dotnet fsi docs/testing.fsx once Orsak is built, and the output shown is from such a run. The tests are plain functions here; in a test project, they would be the bodies of tests in the framework of your choice.

The feature

A shop gives its customers discount vouchers, each with a unique code, a random discount of 5 to 15 percent, and an expiry 30 days out. A customer can hold at most three. The vouchers are kept in a store, described in the usual way:

open System
open System.Collections.Generic
open System.Threading.Tasks
open Microsoft.Extensions.Time.Testing
open Orsak

type Voucher = {
    Code: Guid
    Customer: string
    Percent: int
    Expires: DateTimeOffset
}

type VoucherError =
    | TooManyVouchers
    | StoreUnavailable

type IVoucherStore =
    abstract member CountFor: customer: string -> Task<int>
    abstract member Save: Voucher -> Task<Result<unit, VoucherError>>

type IVoucherStoreProvider =
    abstract member Vouchers: IVoucherStore

module Vouchers =
    let countFor customer =
        Effect.Create(fun (p: #IVoucherStoreProvider) -> p.Vouchers.CountFor customer)

    let save voucher =
        Effect.Create(fun (p: #IVoucherStoreProvider) -> p.Vouchers.Save voucher)

Issuing a voucher uses the store, and three of Orsak's built-in effects: GuidGenerator.genGuid, Random.next and Time.utcNow (.NET 8 and later):

let issueVoucher customer = eff {
    let! existing = Vouchers.countFor customer

    if existing >= 3 then
        return! Error TooManyVouchers
    else
        let! code = GuidGenerator.genGuid ()
        let! percent = Random.next 5 16
        let! now = Time.utcNow ()

        let voucher = {
            Code = code
            Customer = customer
            Percent = percent
            Expires = now.AddDays 30.
        }

        do! Vouchers.save voucher
        return voucher
}

let isValid voucher = eff {
    let! now = Time.utcNow ()
    return now < voucher.Expires
}

The type of issueVoucher is inferred as

string -> Effect<#IVoucherStoreProvider & #IGuidGenProvider & #IRandomProvider & #ITimeProvider, Voucher, VoucherError>

which is a complete list of what a test has to provide, checked by the compiler: an environment that misses one of them is a compile error, not a failure at runtime. isValid needs only a clock.

A test environment

The fakes make every source of variation fixed. The store keeps its vouchers in a list, so tests can look at what was saved. The id generator hands out known ids, the random numbers are always the lowest possible, and the clock is a FakeTimeProvider, from the Microsoft.Extensions.TimeProvider.Testing package, which only moves when told to:

type InMemoryStore() =
    let saved = List<Voucher>()
    member _.Saved = List.ofSeq saved

    interface IVoucherStore with
        member _.CountFor customer =
            Task.FromResult(saved |> Seq.filter (fun v -> v.Customer = customer) |> Seq.length)

        member _.Save voucher =
            saved.Add voucher
            Task.FromResult(Ok())

type SequentialGuids() =
    let mutable next = 0

    interface IGuidGenerator with
        member _.GenGuid() =
            next <- next + 1
            Guid($"00000000-0000-0000-0000-%012d{next}")

type LowestRandom() =
    interface IRandomGenerator with
        member _.Next((minValue, _)) = minValue
        member _.NextDouble() = 0.0

type TestEnv(store: IVoucherStore) =
    let guids = SequentialGuids()
    let random = LowestRandom()
    let clock = FakeTimeProvider(DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero))
    member _.Clock = clock

    interface IVoucherStoreProvider with
        member _.Vouchers = store

    interface IGuidGenProvider with
        member _.GuidGenerator = guids

    interface IRandomProvider with
        member _.Effect = random

    interface ITimeProvider with
        member _.Clock = clock

Each test creates its own environment, so tests don't share state and can run in parallel.

Testing the result

Effect.run returns the result as a value, to be matched on. The tests use a small helper to fail with a message:

let expect condition message =
    if not condition then
        failwith message

let ``issues a voucher expiring in 30 days`` () = task {
    let store = InMemoryStore()
    let! result = issueVoucher "ada" |> Effect.run (TestEnv store)

    match result with
    | Ok voucher ->
        expect (voucher.Code = Guid "00000000-0000-0000-0000-000000000001") "the first id"
        expect (voucher.Percent = 5) "the lowest discount"
        expect (voucher.Expires = DateTimeOffset(2026, 1, 31, 12, 0, 0, TimeSpan.Zero)) "30 days out"
        expect (store.Saved = [ voucher ]) "the voucher is saved"
    | Error e -> failwith $"failed with {e}"
}

When a test only cares about success, Effect.runOrFail is shorter: it returns the value, and throws when the effect fails, which fails the test with the error in its message:

let ``a fourth voucher is refused`` () = task {
    let env = TestEnv(InMemoryStore())

    for _ in 1..3 do
        let! _ = issueVoucher "ada" |> Effect.runOrFail env
        ()

    let! fourth = issueVoucher "ada" |> Effect.run env
    expect (fourth = Error TooManyVouchers) "the fourth is refused"
}

Testing failures

Failures of dependencies are faked like anything else. Here the store is down, and an object expression is enough to say so:

let ``a store failure is the result`` () = task {
    let downStore =
        { new IVoucherStore with
            member _.CountFor _ = Task.FromResult 0
            member _.Save _ = Task.FromResult(Error StoreUnavailable)
        }

    let! result = issueVoucher "ada" |> Effect.run (TestEnv downStore)
    expect (result = Error StoreUnavailable) "the store's error"
}

Testing time

Since the clock is part of the environment, a test can move it. A voucher is valid until it expires:

let ``a voucher expires after 30 days`` () = task {
    let env = TestEnv(InMemoryStore())
    let! voucher = issueVoucher "ada" |> Effect.runOrFail env

    env.Clock.Advance(TimeSpan.FromDays 29.)
    let! validAfter29 = isValid voucher |> Effect.runOrFail env
    env.Clock.Advance(TimeSpan.FromDays 2.)
    let! validAfter31 = isValid voucher |> Effect.runOrFail env

    expect validAfter29 "valid after 29 days"
    expect (not validAfter31) "expired after 31 days"
}

isValid only needs a clock, so it can also be tested with an environment that only has one:

let ``a voucher is valid before it expires`` () = task {
    let clockOnly =
        { new ITimeProvider with
            member _.Clock = FakeTimeProvider(DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero))
        }

    let voucher = {
        Code = Guid.Empty
        Customer = "ada"
        Percent = 5
        Expires = DateTimeOffset(2026, 2, 1, 0, 0, 0, TimeSpan.Zero)
    }

    let! valid = isValid voucher |> Effect.runOrFail clockOnly
    expect valid "valid in January"
}

Running them all:

for name, test in
    [ nameof ``issues a voucher expiring in 30 days``, ``issues a voucher expiring in 30 days``
      nameof ``a fourth voucher is refused``, ``a fourth voucher is refused``
      nameof ``a store failure is the result``, ``a store failure is the result``
      nameof ``a voucher expires after 30 days``, ``a voucher expires after 30 days``
      nameof ``a voucher is valid before it expires``, ``a voucher is valid before it expires`` ] do
    test().Wait()
    printfn $"passed: {name}"
passed: issues a voucher expiring in 30 days
passed: a fourth voucher is refused
passed: a store failure is the result
passed: a voucher expires after 30 days
passed: a voucher is valid before it expires

Less boilerplate

A test environment type per test module is short, but it adds up. With Orsak.Myriad, [<GenEnvironment>] on an interface inheriting the providers generates a create function taking an anonymous record of effects, so a test can build its environment in place, and run and runOrFail functions that create it and run an effect in one go.

namespace System
namespace System.Collections
namespace System.Collections.Generic
namespace System.Threading
namespace System.Threading.Tasks
namespace Microsoft
namespace Microsoft.Extensions
namespace Microsoft.Extensions.Time
namespace Microsoft.Extensions.Time.Testing
namespace Orsak
type Voucher = { Code: Guid Customer: string Percent: int Expires: DateTimeOffset }
Multiple items
type Guid = new: b: byte array -> unit + 6 overloads member CompareTo: value: Guid -> int + 1 overload member Equals: g: Guid -> bool + 1 overload member GetHashCode: unit -> int member ToByteArray: unit -> byte array + 1 overload member ToString: unit -> string + 2 overloads member TryFormat: utf8Destination: Span<byte> * bytesWritten: byref<int> * ?format: ReadOnlySpan<char> -> bool + 1 overload member TryWriteBytes: destination: Span<byte> -> bool + 1 overload static member (<) : left: Guid * right: Guid -> bool static member (<=) : left: Guid * right: Guid -> bool ...
<summary>Represents a globally unique identifier (GUID).</summary>

--------------------
Guid ()
Guid(b: byte array) : Guid
Guid(b: ReadOnlySpan<byte>) : Guid
Guid(g: string) : Guid
Guid(b: ReadOnlySpan<byte>, bigEndian: bool) : Guid
Guid(a: int, b: int16, c: int16, d: byte array) : Guid
Guid(a: int, b: int16, c: int16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
Guid(a: uint32, b: uint16, c: uint16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
Multiple items
val string: value: 'T -> string

--------------------
type string = String
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

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

--------------------
type int<'Measure> = int
Multiple items
type DateTimeOffset = new: date: DateOnly * time: TimeOnly * offset: TimeSpan -> unit + 8 overloads member Add: timeSpan: TimeSpan -> DateTimeOffset member AddDays: days: float -> DateTimeOffset member AddHours: hours: float -> DateTimeOffset member AddMicroseconds: microseconds: float -> DateTimeOffset member AddMilliseconds: milliseconds: float -> DateTimeOffset member AddMinutes: minutes: float -> DateTimeOffset member AddMonths: months: int -> DateTimeOffset member AddSeconds: seconds: float -> DateTimeOffset member AddTicks: ticks: int64 -> DateTimeOffset ...
<summary>Represents a point in time, typically expressed as a date and time of day, relative to Coordinated Universal Time (UTC).</summary>

--------------------
DateTimeOffset ()
DateTimeOffset(dateTime: DateTime) : DateTimeOffset
DateTimeOffset(dateTime: DateTime, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(ticks: int64, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(date: DateOnly, time: TimeOnly, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, calendar: Globalization.Calendar, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, microsecond: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, microsecond: int, calendar: Globalization.Calendar, offset: TimeSpan) : DateTimeOffset
type VoucherError = | TooManyVouchers | StoreUnavailable
type IVoucherStore = abstract CountFor: customer: string -> Task<int> abstract Save: Voucher -> Task<Result<unit,VoucherError>>
Multiple items
type Task = interface IAsyncResult interface IDisposable new: action: Action -> unit + 7 overloads member ConfigureAwait: continueOnCapturedContext: bool -> ConfiguredTaskAwaitable + 1 overload member ContinueWith: continuationAction: Action<Task,obj> * state: obj -> Task + 19 overloads member Dispose: unit -> unit member GetAwaiter: unit -> TaskAwaiter member RunSynchronously: unit -> unit + 1 overload member Start: unit -> unit + 1 overload member Wait: unit -> unit + 5 overloads ...
<summary>Represents an asynchronous operation.</summary>

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


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

--------------------
Task(``function`` : Func<'TResult>) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(``function`` : Func<'TResult>, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<'TResult>, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Task(``function`` : Func<obj,'TResult>, state: obj, cancellationToken: Threading.CancellationToken, creationOptions: TaskCreationOptions) : Task<'TResult>
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
type unit = Unit
type IVoucherStoreProvider = abstract Vouchers: IVoucherStore
val countFor: customer: string -> Effect<#IVoucherStoreProvider,int,'b>
val customer: string
Multiple items
union case Effect.Effect: EffectDelegate<'r,'a,'e> -> Effect<'r,'a,'e>

--------------------
module Effect from Orsak
<summary> Functions for running, combining, recovering and repeating effects. </summary>

--------------------
type Effect = static member Create: f: ('a -> Task<Result<'b,'e>>) -> Effect<'a,'b,'e> + 2 overloads static member Error: error: 'e -> Effect<'a,'b,'e>
<summary> Creates effects. <c>Effect.Create</c> turns a function of an environment into an effect, and is how effects are made from the interfaces that describe side effects. The function may return a plain value, a <c>Result</c>, a <c>Task</c>, a <c>ValueTask</c> or an <c>Async</c>, of a value or of a <c>Result</c>; returning a <c>Result</c> lets the effect fail. </summary>
<example> The usual pattern: an interface describing the side effect, a provider interface exposing it, and a function creating the effect, which requires any environment that implements the provider (<c>#IConsoleProvider</c>). <code lang="fsharp"> type IConsole = abstract ReadLine: unit -&gt; string abstract WriteLine: string -&gt; unit type IConsoleProvider = abstract Console: IConsole module Console = let readLine () = Effect.Create(fun (p: #IConsoleProvider) -&gt; p.Console.ReadLine()) let writeLine line = Effect.Create(fun (p: #IConsoleProvider) -&gt; p.Console.WriteLine line) </code> The Orsak.Myriad generators can write the functions of such a module from the interface. </example>


--------------------
type Effect<'r,'a,'e> = | Effect of EffectDelegate<'r,'a,'e> member Run: r: 'r -> AsyncResult<'a,'e> member RunOrFail: r: 'r -> Task<'a> static member (<*>)<'b,'a (requires member (+))> : f: Effect<'r,('b -> 'a),'a0> * e: Effect<'r,'b,'a0> -> Effect<'r,'a,'a0> (requires member (+)) static member (>>=) : h: Effect<'r,'a,'e> * f: ('a -> Effect<'r,'b,'e>) -> Effect<'r,'b,'e> static member Map: h: Effect<'r,'a,'e> * f: ('a -> 'b) -> Effect<'r,'b,'e> static member Return<'a,'b,'c> : a: 'a0 -> Effect<'b,'a0,'c> static member ap<'r,'a,'b,'e> : applicative: Effect<'r,('b -> 'a),'e> -> e: Effect<'r,'b,'e> -> Effect<'r,'a,'e>
<summary> Describes an effect that can either succeed with <typeparamref name="'a" />, or fail with <typeparamref name="'e" />. The effect is 'cold', and only starts when run with an <typeparamref name="'r" />. </summary>
<remarks> Effects are usually written with the <c>eff</c> computation expression, and created from interfaces that describe side effects with <c>Effect.Create</c>. The environment <typeparamref name="'r" /> is inferred from what the effect uses, and supplied when the effect is run. </remarks>
<example><code lang="fsharp"> let greet () = eff { let! name = Console.readLine () do! Console.writeLine $"Hello {name}" } // at the composition root, with an environment providing the console effects: let! result = greet () |&gt; Effect.run env </code></example>
<typeparam name="'r"> The environment required to run the effect </typeparam>
<typeparam name="'a"> The resulting type when the effect runs successfully </typeparam>
<typeparam name="'e"> The resulting type when the effect fails</typeparam>
static member Effect.Create: f: ('a -> 'b) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Async<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> Result<'b,'e>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Task<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> ValueTask<'b>) -> Effect<'a,'b,'a0>
static member Effect.Create: f: ('a -> Async<Result<'b,'e>>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> ValueTask<Result<'b,'e>>) -> Effect<'a,'b,'e>
static member Effect.Create: f: ('a -> Task<Result<'b,'e>>) -> Effect<'a,'b,'e>
val p: #IVoucherStoreProvider
property IVoucherStoreProvider.Vouchers: IVoucherStore with get
abstract IVoucherStore.CountFor: customer: string -> Task<int>
val save: voucher: Voucher -> Effect<#IVoucherStoreProvider,unit,VoucherError>
val voucher: Voucher
abstract IVoucherStore.Save: Voucher -> Task<Result<unit,VoucherError>>
val issueVoucher: customer: string -> Effect<'a,Voucher,VoucherError> (requires 'a :> IVoucherStoreProvider and 'a :> ITimeProvider and 'a :> IRandomProvider and 'a :> IGuidGenProvider)
val eff: EffBuilder
<summary> The computation expression for writing effects. Binding (<c>let!</c>, <c>do!</c>, <c>return!</c>) works with other effects, and with <c>Task</c>, <c>ValueTask</c>, <c>Async</c> and <c>Result</c>, so .NET APIs can be used directly. It supports <c>for</c>, <c>while</c>, <c>use</c>, <c>try/with</c> and <c>try/finally</c>, and <c>and!</c> runs effects concurrently. A failed effect stops the rest of the expression, with its error. </summary>
<example><code lang="fsharp"> let placeOrder (order: Order) = eff { let! id = GuidGenerator.genGuid () let! now = Time.utcNow () do! Orders.save { order with Id = id; Placed = now } let! customer = Customers.load order.CustomerId and! stock = Stock.reserve order.Lines return id } </code></example>
val existing: int
module Vouchers from Testing
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
union case VoucherError.TooManyVouchers: VoucherError
val code: Guid
module GuidGenerator from Orsak
<summary> Effects for generating <see cref="T:System.Guid" />s through an <see cref="T:Orsak.IGuidGenProvider" />. </summary>
val genGuid: unit -> Effect<#IGuidGenProvider,Guid,'b>
<summary> An effect that generates a new <see cref="T:System.Guid" /> with the environment's generator. </summary>
<example><code lang="fsharp"> let createOrder () = eff { let! id = GuidGenerator.genGuid () return { Id = id; Lines = [] } } </code></example>
val percent: int
Multiple items
type Random = new: unit -> unit + 1 overload member GetHexString: stringLength: int * ?lowercase: bool -> string + 1 overload member GetItems<'T> : choices: ReadOnlySpan<'T> * length: int -> 'T array + 2 overloads member GetString: choices: ReadOnlySpan<char> * length: int -> string member Next: unit -> int + 2 overloads member NextBytes: buffer: byte array -> unit + 1 overload member NextDouble: unit -> float member NextInt64: unit -> int64 + 2 overloads member NextSingle: unit -> float32 member Shuffle<'T> : values: Span<'T> -> unit + 1 overload ...
<summary>Represents a pseudo-random number generator, which is an algorithm that produces a sequence of numbers that meet certain statistical requirements for randomness.</summary>

--------------------
Random() : Random
Random(Seed: int) : Random
val next: min: int -> max: int -> Effect<#IRandomProvider,int,'b>
<summary> An effect that generates a random integer from <paramref name="min" />, inclusive, to <paramref name="max" />, exclusive. </summary>
<param name="min">The inclusive lower bound.</param>
<param name="max">The exclusive upper bound.</param>
<example><code lang="fsharp"> let rollDie () = Random.next 1 7 </code></example>
val now: DateTimeOffset
module Time from Orsak
<summary> Effects for reading the time through an <see cref="T:Orsak.ITimeProvider" />. </summary>
val utcNow: unit -> Effect<#ITimeProvider,DateTimeOffset,'b>
<summary> An effect that reads the current UTC time from the environment's clock. </summary>
<example><code lang="fsharp"> let isExpired (expires: DateTimeOffset) = eff { let! now = Time.utcNow () return now &gt; expires } </code></example>
DateTimeOffset.AddDays(days: float) : DateTimeOffset
val isValid: voucher: Voucher -> Effect<#ITimeProvider,bool,'b>
Voucher.Expires: DateTimeOffset
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
Multiple items
type InMemoryStore = interface IVoucherStore new: unit -> InMemoryStore member Saved: Voucher list

--------------------
new: unit -> InMemoryStore
val saved: List<Voucher>
Multiple items
type List<'T> = interface ICollection<'T> interface seq<'T> interface IEnumerable interface IList<'T> interface IReadOnlyCollection<'T> interface IReadOnlyList<'T> interface ICollection interface IList new: unit -> unit + 2 overloads member Add: item: 'T -> unit ...
<summary>Represents a strongly typed list of objects that can be accessed by index. Provides methods to search, sort, and manipulate lists.</summary>
<typeparam name="T">The type of elements in the list.</typeparam>


--------------------
List() : List<'T>
List(collection: 'T seq) : List<'T>
List(capacity: int) : List<'T>
val ofSeq: source: 'T seq -> 'T list
Task.FromResult<'TResult>(result: 'TResult) : Task<'TResult>
module Seq from Microsoft.FSharp.Collections
val filter: predicate: ('T -> bool) -> source: 'T seq -> 'T seq
val v: Voucher
Voucher.Customer: string
val length: source: 'T seq -> int
List.Add(item: Voucher) : unit
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
Multiple items
type SequentialGuids = interface IGuidGenerator new: unit -> SequentialGuids

--------------------
new: unit -> SequentialGuids
val mutable next: int
type IGuidGenerator = abstract GenGuid: unit -> Guid
<summary> Generates <see cref="T:System.Guid" />s. An effect interface, so that code creating ids can be run with a deterministic generator in tests. </summary>
Multiple items
type LowestRandom = interface IRandomGenerator new: unit -> LowestRandom

--------------------
new: unit -> LowestRandom
type IRandomGenerator = IRandom
<summary>An alias of <see cref="T:Orsak.IRandom" />.</summary>
val minValue: int
Multiple items
type TestEnv = interface ITimeProvider interface IRandomProvider interface IGuidGenProvider interface IVoucherStoreProvider new: store: IVoucherStore -> TestEnv member Clock: FakeTimeProvider

--------------------
new: store: IVoucherStore -> TestEnv
val store: IVoucherStore
val guids: SequentialGuids
val random: LowestRandom
val clock: FakeTimeProvider
Multiple items
type FakeTimeProvider = inherit TimeProvider new: unit -> unit + 1 overload member AdjustTime: value: DateTimeOffset -> unit member Advance: delta: TimeSpan -> unit member CreateTimer: callback: TimerCallback * state: obj * dueTime: TimeSpan * period: TimeSpan -> ITimer member GetTimestamp: unit -> int64 member GetUtcNow: unit -> DateTimeOffset member SetLocalTimeZone: localTimeZone: TimeZoneInfo -> unit member SetUtcNow: value: DateTimeOffset -> unit member ToString: unit -> string ...
<summary> Represents a synthetic time provider that can be used to enable deterministic behavior in tests. </summary>

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

--------------------
TimeSpan ()
TimeSpan(ticks: int64) : TimeSpan
TimeSpan(hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int) : TimeSpan
TimeSpan(days: int, hours: int, minutes: int, seconds: int, milliseconds: int, microseconds: int) : TimeSpan
field TimeSpan.Zero: TimeSpan
<summary>Represents the zero <see cref="T:System.TimeSpan" /> value. This field is read-only.</summary>
type IGuidGenProvider = abstract GuidGenerator: IGuidGenerator
<summary> Provides an <see cref="T:Orsak.IGuidGenerator" /> to effects, as part of an environment. </summary>
type IRandomProvider = IRandomGeneratorProvider
<summary>An alias of <see cref="T:Orsak.IRandomGeneratorProvider" />.</summary>
Multiple items
module Effect from Orsak
<summary> Functions for running, combining, recovering and repeating effects. </summary>

--------------------
type Effect = static member Create: f: ('a -> Task<Result<'b,'e>>) -> Effect<'a,'b,'e> + 2 overloads static member Error: error: 'e -> Effect<'a,'b,'e>
<summary> Creates effects. <c>Effect.Create</c> turns a function of an environment into an effect, and is how effects are made from the interfaces that describe side effects. The function may return a plain value, a <c>Result</c>, a <c>Task</c>, a <c>ValueTask</c> or an <c>Async</c>, of a value or of a <c>Result</c>; returning a <c>Result</c> lets the effect fail. </summary>
<example> The usual pattern: an interface describing the side effect, a provider interface exposing it, and a function creating the effect, which requires any environment that implements the provider (<c>#IConsoleProvider</c>). <code lang="fsharp"> type IConsole = abstract ReadLine: unit -&gt; string abstract WriteLine: string -&gt; unit type IConsoleProvider = abstract Console: IConsole module Console = let readLine () = Effect.Create(fun (p: #IConsoleProvider) -&gt; p.Console.ReadLine()) let writeLine line = Effect.Create(fun (p: #IConsoleProvider) -&gt; p.Console.WriteLine line) </code> The Orsak.Myriad generators can write the functions of such a module from the interface. </example>


--------------------
type Effect<'r,'a,'e> = | Effect of EffectDelegate<'r,'a,'e> member Run: r: 'r -> AsyncResult<'a,'e> member RunOrFail: r: 'r -> Task<'a> static member (<*>)<'b,'a (requires member (+))> : f: Effect<'r,('b -> 'a),'a0> * e: Effect<'r,'b,'a0> -> Effect<'r,'a,'a0> (requires member (+)) static member (>>=) : h: Effect<'r,'a,'e> * f: ('a -> Effect<'r,'b,'e>) -> Effect<'r,'b,'e> static member Map: h: Effect<'r,'a,'e> * f: ('a -> 'b) -> Effect<'r,'b,'e> static member Return<'a,'b,'c> : a: 'a0 -> Effect<'b,'a0,'c> static member ap<'r,'a,'b,'e> : applicative: Effect<'r,('b -> 'a),'e> -> e: Effect<'r,'b,'e> -> Effect<'r,'a,'e>
<summary> Describes an effect that can either succeed with <typeparamref name="'a" />, or fail with <typeparamref name="'e" />. The effect is 'cold', and only starts when run with an <typeparamref name="'r" />. </summary>
<remarks> Effects are usually written with the <c>eff</c> computation expression, and created from interfaces that describe side effects with <c>Effect.Create</c>. The environment <typeparamref name="'r" /> is inferred from what the effect uses, and supplied when the effect is run. </remarks>
<example><code lang="fsharp"> let greet () = eff { let! name = Console.readLine () do! Console.writeLine $"Hello {name}" } // at the composition root, with an environment providing the console effects: let! result = greet () |&gt; Effect.run env </code></example>
<typeparam name="'r"> The environment required to run the effect </typeparam>
<typeparam name="'a"> The resulting type when the effect runs successfully </typeparam>
<typeparam name="'e"> The resulting type when the effect fails</typeparam>
type ITimeProvider = abstract Clock: TimeProvider
<summary> Provides a <see cref="T:System.TimeProvider" /> to effects, as part of an environment. Using <c>TimeProvider.System</c> in production and a fake time provider in tests makes time-dependent effects testable. </summary>
val expect: condition: bool -> message: string -> unit
val condition: bool
val message: string
val failwith: message: string -> 'T
val task: TaskBuilder
val store: InMemoryStore
val result: Result<Voucher,VoucherError>
val run: env: 'r -> e: Effect<'r,'a,'e> -> AsyncResult<'a,'e>
<summary> Starts the effect. </summary>
<typeparam name="'r"> The environment required to run the effect </typeparam>
<typeparam name="'a"> The resulting type when the effect runs successfully </typeparam>
<typeparam name="'e"> The resulting type when the effect fails</typeparam>
<param name="env">The environment needed to start the effect</param>
<param name="e">The effect to run</param>
<example><code lang="fsharp"> task { match! placeOrder order |&gt; Effect.run env with | Ok id -&gt; printfn $"Placed {id}" | Error err -&gt; printfn $"Failed: {err}" } </code></example>
Voucher.Code: Guid
Voucher.Percent: int
field TimeSpan.Zero: TimeSpan
property InMemoryStore.Saved: Voucher list with get
val e: VoucherError
val env: TestEnv
val runOrFail: env: 'r -> e: Effect<'r,'a,'e> -> Task<'a>
<summary> Starts the effect, and raises an exception if it fails, with the error's string representation as its message. Prefer <c>Effect.run</c>, which returns the error as a value; this is meant for places where an exception is the only option, such as tests or scripts. </summary>
<typeparam name="'r"> The environment required to run the effect </typeparam>
<typeparam name="'a"> The resulting type when the effect runs successfully </typeparam>
<typeparam name="'e"> The resulting type when the effect fails</typeparam>
<param name="env">The environment needed to start the effect</param>
<param name="e">The effect to run</param>
val fourth: Result<Voucher,VoucherError>
val downStore: IVoucherStore
union case VoucherError.StoreUnavailable: VoucherError
property TestEnv.Clock: FakeTimeProvider with get
FakeTimeProvider.Advance(delta: TimeSpan) : unit
TimeSpan.FromDays(days: int) : TimeSpan
TimeSpan.FromDays(value: float) : TimeSpan
TimeSpan.FromDays(days: int, ?hours: int, ?minutes: int64, ?seconds: int64, ?milliseconds: int64, ?microseconds: int64) : TimeSpan
val validAfter29: bool
val validAfter31: bool
val clockOnly: ITimeProvider
field Guid.Empty: Guid
val valid: bool
val name: string
val test: (unit -> Task<unit>)
val nameof: 'T -> string
val printfn: format: Printf.TextWriterFormat<'T> -> 'T

Type something to start searching.