Option<T> holds a value or nothing. Use it where a value may be missing, like a customer's
nickname or the result of a lookup. The compiler then makes sure you handle the missing case before
you use the value.
There is no .Value to read. You reach the content through Match, TryGetValue and the other
members on this page, and each of them asks what should happen when there is nothing. An option that
says it has a value always has one: T is notnull, Option.Some(null) throws, and a
default(Option<T>) is None. ToString() never prints the content.
Option<T> lives in the CodoMetis.TypeKit namespace, and the
base README is its reference. The examples import that
namespace and share a customer with an optional nickname and an optional address:
public readonly partial record struct CustomerId : IValue<Guid>;
public sealed record Address(string Town, string Country);
public sealed record Customer(CustomerId Id, string Name, string Tier, Option<string> Nickname, Option<Address> Address);Option<Customer> found = customers.GetValueOrNone(ann.Id); // Some
Option<Customer> unknown = customers.GetValueOrNone(CustomerId.New()); // None
Option<Customer> gold = customers.Values.FirstOrNone(c => c.Tier == "gold"); // Some, the first match (or None)
Option<string> nickname = ann.Nickname; // Some
Option<int> parsed = int.TryParse(text, out var number) ? Option.Some(number) : Option.None(); // None, taking its type from the conditional
Option<int> none = Option.None<int>(); // where nothing supplies the typeMost options come from a lookup:
GetValueOrNone(key)on a dictionary.FirstOrNone()andLastOrNone()on a sequence, with or without a predicate.SingleOrNone(), which throws when there is more than one element.ElementAtOrNone(index), for the element at a position.ToOption()on a nullable, such as anint?or astring?. A null becomesNone.
To build one yourself, use Option.Some(value) and Option.None(). None() takes its type from
where it goes: a return, a conditional beside a Some, or a parameter. When nothing tells it the
type, as with var, write Option.None<T>().
FirstOrNone, SingleOrNone and ElementAtOrNone also work on an IQueryable<T>. The database
provider runs the predicate and fetches only the rows they need. LastOrNone on a query fails to
compile, because a query has no last row until you order it.
The predicate runs in the database only when it is an expression, which a lambda written at the call
is. A Func<T, bool> held in a variable, or a method group, binds to the sequence overload instead.
That reads every row and checks the condition in memory, and the analyzer warns at the call
(CMTK0009). Pass an Expression<Func<T, bool>> there.
Good to know
- Prefer these to
FirstOrDefault(). For a value object or aResult,FirstOrDefault()and its kind hand you adefaultinstance that went through no factory. The analyzer points them out (CMTK0009).
string greeting = found.Match(customer => $"Hello again {customer.Name}", () => "Hello"); // "Hello again Ann"
string tier = unknown.Map(customer => customer.Tier).Or("standard"); // "standard"
int? limit = parsed.OrNull(); // null, an int? for an Option<int>
int sum = found.Map(customer => customer.Name.Length).OrDefault(); // 3, a struct's default for None
bool known = found.IsSome(); // true
if (found.TryGetValue(out var customer)) visits.Add(customer.Id); // customer is non-null in the branch
foreach (var each in unknown.AsEnumerable()) visits.Add(each.Id); // runs once for Some, never for NoneMatch takes one branch for a value and one for nothing, and returns what they return. It is the one
call that can't forget the missing case.
Or(fallback)andOrDefault()give you the value, or a fallback when there is none.OrNull()gives you a nullable: anint?for anOption<int>, astring?for anOption<string>.TryGetValue(out value)fits anif, and the value is non-null inside the branch.AsEnumerable()gives you zero or one element, for aforeachor LINQ.IsSome()andIsNone()answer the question on its own. Use them for a guard or an assertion. To branch and then read the value, useMatchorTryGetValue.
Option<decimal> discount = found.Filter(customer => customer.Tier == "gold").Map(_ => 0.1m); // Some, Ann is gold
Option<string> country = found.Bind(customer => customer.Address).Map(address => address.Country); // Some, the Option<Address> flattened and then mapped
Option<(string Name, string Town)> label = found.Zip(found.Bind(c => c.Address), (c, a) => (c.Name, a.Town)); // Some, only when both are
Option<Customer> seen = found.Tap(customer => visits.Add(customer.Id)); // runs for Some, hands the option on
IEnumerable<string> nicknames = customers.Values.Choose(customer => customer.Nickname); // "Annie", the nicknames of those who have one: a filter and a map in one
IEnumerable<Customer> known = ids.Choose(id => customers.GetValueOrNone(id)); // the customers the ids find, the unknown ones dropped
Option<string> greeting =
from customer in found
from address in customer.Address // a later from sees the earlier ones
where address.Country == "DE"
select $"{customer.Name} in {address.Town}"; // SomeMapchanges the value, and a missing value stays missing.Bindtakes a selector that returns an option itself, so you never end up with anOption<Option<T>>.Filterturns a value that fails the predicate intoNone.Zipcombines two to six options into one value, when every one of them has a value.Tapruns an action on the value and hands the option on unchanged.Chooseworks on a sequence. It keeps, in order, the values the selector returnsSomefor, and drops the rest: a filter and a map in one. It runs lazily, like any LINQ operator. On a sequence of options,Choose()without a selector keeps the values they hold.
Select, SelectMany and Where make query syntax work, as in the last example. Each from after
the first can use the ones before it.
A lookup that is still running is a Task<Option<T>>. MapAsync, BindAsync, FilterAsync,
TapAsync and ToResultAsync continue it the way Result's async chains
continue a pending result. You await the whole chain once, at the end.
A callback in such a chain runs where the task it waits for finished. If the lookup had already finished, the callback runs at once, on your synchronization context. If it had not, the callback runs without that context, usually on a thread-pool thread (Where a callback runs).
In a Blazor component or a WPF window, await the lookup first, then continue with the synchronous members:
Option<Customer> loaded = await shop.FindAsync(ann.Id); // the await returns to the caller's context
string shown = loaded.Map(customer => customer.Name).Or("unknown"); // "Ann", the selector on that context tooKeep options inside your code, and use a nullable T? wherever data crosses a boundary. An option
is not a wire type, a column or a route parameter. System.Text.Json rejects it in both directions
(JSON), and EF Core can't map it
(EF Core).
public sealed record CustomerDto(string Name, string? Nickname); // the wire says absent with null
static Customer FromDto(CustomerDto dto) => new(CustomerId.New(), dto.Name, "standard", dto.Nickname.ToOption(), Option.None());
static CustomerDto ToDto(Customer customer) => new(customer.Name, customer.Nickname.OrNull());Use ToOption() on the way in and OrNull() on the way out. The option lives between the two
shapes.
Result<Customer, LookupFault> required = customers.GetValueOrNone(CustomerId.New()).ToResult(LookupFault.Unknown); // Error: UnknownWhen a missing value is an error the caller has to act on, ToResult(error) turns the option into a
Result, and the Result page takes it from there.
- The caller needs to know why. Use a Result, which carries the error.
Email.TryFrom(text)tells you whether the text is valid.Email.Create(text)tells you which rule it broke. - Zero to many values. Use a collection. An empty one says "none" without a special type.
- A value that is always there once it is set. Use a
requiredproperty. - A property of an entity or a DTO. Use
T?. EF Core and the serializer both reject anOptionproperty there. CallToOption()where your domain code wants an option.
This table lists every public member: on Option<T>, on the static class Option, and the
extensions in the CodoMetis.TypeKit namespace. T is notnull throughout, with one exception: the
elements Choose(selector) reads may be null, and only what the selector finds counts as a value.
| Member | On | Returns |
|---|---|---|
Some(value) |
Option |
Option<T>; throws ArgumentNullException for null |
None() |
Option |
a None marker that converts to any Option<T> |
None<T>() |
Option |
Option<T> |
ToOption() |
T? (a nullable struct or a reference) |
Option<T>: None for null |
FirstOrNone(), FirstOrNone(predicate) |
IEnumerable<T>, IQueryable<T> |
the first element, or the first matching one; None for none. On a query the provider runs the predicate and returns one row, when the predicate is an expression; a Func variable or a method group reads every row |
LastOrNone(), LastOrNone(predicate) |
IEnumerable<T> |
the last, likewise; a compile error on a query, which has no last row until it is ordered |
SingleOrNone(), SingleOrNone(predicate) |
IEnumerable<T>, IQueryable<T> |
the only element, or None for none; throws for more than one. A query is asked for two rows |
ElementAtOrNone(index) |
IEnumerable<T> (an int or an Index), IQueryable<T> (an int) |
the element at the index, or None past either end; an Index on a query is a compile error |
GetValueOrNone(key) |
IReadOnlyDictionary<TKey, TValue> |
Option<TValue> |
Choose(selector) |
IEnumerable<T>, its elements nullable or not |
the values the selector answers Some for, in order and lazily: a filter and a map in one |
Choose() |
IEnumerable<Option<T>> |
the values of the options that hold one, in order and lazily |
Choose(selector), Choose() |
IAsyncEnumerable<T>, IAsyncEnumerable<Option<T>> |
the same, as an IAsyncEnumerable of the values as they arrive; the token given to WithCancellation reaches the stream |
IsSome(), IsNone() |
Option<T> |
bool |
Match(onSome, onNone) |
Option<T> |
what the branch returns |
TryGetValue(out value) |
Option<T> |
bool; value is non-null when true |
Or(fallback) |
Option<T> |
T |
OrDefault() |
Option<T> where T : struct |
T, default for None; CMTK0009 for a value object, whose default is no value |
OrNull() |
Option<T> |
T?: int? for an Option<int>, string? for an Option<string> |
AsEnumerable() |
Option<T> |
zero or one element |
Map(selector) |
Option<T> |
Option<TResult>; throws if the selector returns null |
Bind(selector) |
Option<T> |
the Option<TResult> the selector returns, or None |
Filter(predicate) |
Option<T> |
the same option, or None when the predicate fails |
Tap(action) |
Option<T> |
the same option, after running the action on a value |
Zip(b, ..., selector) |
Option<T> with two to six options |
Option<TResult>, Some only when every one is |
ToResult(error) |
Option<T> |
Result<T, TError>: the error for None |
MapAsync(selector), BindAsync(selector), TapAsync(action) |
Option<T>, Task<Option<T>> |
the same over a callback that returns a Task, or continuing a pending option; the chain is awaited once |
FilterAsync(predicate) |
Task<Option<T>> |
Filter, once the option is known |
ToResultAsync(error) |
Task<Option<T>> |
Task<Result<T, TError>>: a pending lookup into a Result pipeline |
Select, SelectMany, Where |
Option<T> |
query syntax over Map, Bind and Filter |
implicit from None |
Option<T> |
None |
Two options are equal when both are None, or when both hold equal values, as with any record
struct. A default(Option<T>) is None, but the analyzer still rejects writing it (CMTK0001). Write
Option.None() instead.