For the complete documentation index, see llms.txt. This page is also available as Markdown.

Configuration

This library has a few configurable behaviours. You set them through MonadOptions.Configure, once, at start-up. Configure each option below once in your application's lifetime.

If your application has a dependency injection container, configure the library there instead. Install Waystone.Monads.Extensions.Hosting on a host, or Waystone.Monads.Extensions.DependencyInjection without one, and the container writes these settings for you. You get a delegate that can resolve services out of the container, optional binding from IConfiguration, and a diagnostic event when configuration was registered but never installed.

Reach for MonadOptions.Configure when there is no container — a library, a test, or a small console application. Reach for a scope when one region of code needs different settings. Every Use… method below is the same one on all three routes.

MonadOptions.Configure(options => options
    .UseFallbackErrorCode("Unknown")
    .UseFallbackErrorMessage("Something went wrong."));

How configuration works

You do not hold options. You describe them, and the library publishes the result.

Configure hands your callback a MonadOptionsBuilder. Every Use… method is on the builder, and each returns the builder so you can chain. When your callback returns, the library builds an immutable MonadOptions from it and swaps that in as the options the whole process reads.

Three consequences, all of which matter in practice.

There is nothing to read. MonadOptions exposes no public property, accessor or instance member — only the two statics Configure and BeginScope. Nothing in your code can inspect the current configuration, and nothing needs to.

A reader never sees a half-configured state. The swap is atomic. Code running on another thread reads either the options from before your Configure call or the ones from after it, never a mixture. Before 7.0.0, configuration mutated a shared object in place, so a concurrent reader could see one setting applied and the next not.

If you are upgrading from 6.x

The common call shape is unchanged, because the lambda parameter's type is inferred:

That compiled against 6.x and it compiles against 7.0.0. You do not have to touch call sites that already build.

Three things do change:

  • An explicitly typed lambda parameter breaks. (MonadOptions options) => becomes (MonadOptionsBuilder options) =>, or drop the annotation and let it infer.

  • A field, parameter, local or property typed MonadOptions has no replacement. Configuration is reachable only inside a Configure or BeginScope callback now, so move the Use… calls into one rather than passing options around.

  • In the satellite packages, MonadOptionsExtensions is now MonadOptionsBuilderExtensions. A using static or a qualified static call naming the old class needs updating. A normal extension call on the callback's parameter, the usual shape, needs nothing.

The 6.x to 7.0.0 upgrade page covers this with the compiler diagnostics you will see.

Logging

This library catches exceptions in several places and turns them into non-throwing types. To see those exceptions, install Waystone.Monads.Extensions.Logging and hand the library your logger once:

Use UseLoggerFactory(factory) if you have no service provider, or UseLogger(logger) if you already hold a logger.

Each entry carries the exception plus the call site that caught it — the member name, the source text of the delegate you passed, and the line number.

The three methods above ship in that package, and Logging covers them properly — the levels, the category, and what lands in each entry.

To count these exceptions instead, install nothing at all. Read Observability for the signals that need no package.

The library also writes to the console whenever a debugger is attached, whether or not you configure a logger. It prints the exception, the call site and the argument expression, then reports it through the signals above as well. This is a debugging aid, so it costs nothing in a normal run — but do not read a console message as proof that your logger ran.

Cancellation

Option.Try, Option.TryAsync, Result.Try and Result.TryAsync catch the exceptions your factory throws and turn them into a None or an Err. From 6.0.0 they make one exception to that: an OperationCanceledException propagates to your caller instead.

Cancelling is you telling the work to stop. It is not the work failing, and turning it into a None leaves the caller unable to tell "cancelled" from "genuinely absent".

TaskCanceledException inherits from OperationCanceledException, so it propagates too.

If you need the pre-6.0.0 behaviour, opt back in:

A cancellation is then caught, counted and logged like any other handled exception, and becomes a None or an Err as it did before.

UseCancellationAsFailure takes an optional bool, which defaults to true, so the call above turns the behaviour on. Pass false to put it back:

You need that only when something earlier already turned it on and you want to undo it — a container registration from a library, or a Configure call elsewhere in start-up. A builder inherits the settings in effect when it was handed to you, so passing false is the only way to reverse the decision.

We recommend leaving this off. It exists so that upgrading to 6.0.0 does not force you to rewrite every call site at once. Scope it with MonadOptions.BeginScope if only part of your code needs it.

Error Code Generation

There are a few factory methods included in the library for generating ErrorCode instances from Enum and from Exception instances. To customise how these error codes are generated, create a class inheriting from ErrorCodeFactory and override the methods you wish to customise. Then create an instance and pass it into the MonadOptions instance via the UseErrorCodeFactory.

Register your instance once, at start-up:

Error Code and Message Fallbacks

There may be exception circumstances which cause the string used to create the ErrorCode or the message of the Error classes to be null or white-space. In these situations, a set of fallbacks are used. These fallbacks can be configured.

The substitution is silent. new ErrorCode(code) and new Error(code, message) trim what you pass, and swap in the fallback when the result is empty. Neither throws, and nothing is logged. So an Error whose message reads An unexpected error occurred. is telling you a call site passed a blank message, not that the library hit an unexpected error. Pass a real message at every call site — a fallback says nothing about what actually failed.

Both configuration methods reject a blank argument themselves. UseFallbackErrorCode and UseFallbackErrorMessage throw an ArgumentException when you pass null, empty or whitespace, because a fallback that is itself unusable would leave nothing to fall back to.

Scoped Configuration

Use MonadOptions.BeginScope when you want different options for one region of code — a single request, a test, or a block you are debugging. The scope applies until you dispose it, and your global configuration is untouched.

A scope accepts the same configuration methods as Configure, so it can override any option:

What a scope does

  • Inherits what you do not set. Options you leave alone keep the values they had when the scope opened.

  • Takes a snapshot. Calling Configure while a scope is open does not change that scope. The new global value applies once the scope ends. This is not new in 7.0.0 — a scope has always held its own copy.

  • Nests. Disposing the innermost scope restores the scope around it.

  • Restores only from the inside out. A scope that is no longer the innermost one declines to restore anything when you dispose it, and reports itself instead. See below.

  • Isolates concurrent work. A scope applies to the current asynchronous flow, so parallel work each sees its own options. This makes scopes safe to use in tests that run in parallel.

A scope affects work you start inside it. It does not affect work that was already running when you opened the scope.

What happens when you dispose out of order

Since 7.0.0, a scope restores only when it is the innermost one still open. Disposing it at any other time changes nothing and reports the mistake.

Dispose looks at the options in effect on the current flow and picks one of three paths:

What it finds
What it does

The options this scope installed, so this scope is the innermost one

Restores what came before it

The options this scope restored to, so it has already been disposed

Nothing, silently

Anything else

Nothing, and writes a ScopeDisposedOutOfOrder diagnostic event

It never throws, on any path.

The third path leaves the early-disposed scope's options in effect. They stay live until the inner scope that is still open is disposed, which then restores them as its own predecessor — so those options outlive the scope that installed them. That is the bug the event exists to tell you about.

Two more cases take the third path, both worth knowing:

  • Disposing a scope from a different asynchronous flow than the one that opened it. A scope lives in the flow, so another flow's Dispose never sees it.

  • Disposing a default(MonadOptionsScope). Before 7.0.0 this dropped the flow back to your global configuration; now it reports like any other out-of-order disposal.

Repeated disposal on the third path reports every time. A scope that has already declined cannot remember that it did, because it is a readonly struct, so each further Dispose writes the event again. Deduplicate in your subscriber if that matters. The "harmless twice" promise covers the restoring path only.

To see these events, see Watching for a scope disposed out of order.

Waystone.Monads.FluentValidation options are covered by the same scope, so you only ever open one.

Last updated

Was this helpful?