CSharpEssentials.Maybe
6.5.2
dotnet add package CSharpEssentials.Maybe --version 6.5.2
NuGet\Install-Package CSharpEssentials.Maybe -Version 6.5.2
<PackageReference Include="CSharpEssentials.Maybe" Version="6.5.2" />
<PackageVersion Include="CSharpEssentials.Maybe" Version="6.5.2" />
<PackageReference Include="CSharpEssentials.Maybe" />
paket add CSharpEssentials.Maybe --version 6.5.2
#r "nuget: CSharpEssentials.Maybe, 6.5.2"
#:package CSharpEssentials.Maybe@6.5.2
#addin nuget:?package=CSharpEssentials.Maybe&version=6.5.2
#tool nuget:?package=CSharpEssentials.Maybe&version=6.5.2
CSharpEssentials.Maybe
Maybe (Option) monad for null-safe, expressive code. Avoid NullReferenceException by making missing values explicit.
Features
- Maybe<T>: Explicit presence/absence of a value with implicit conversions.
- Functional API:
Map,Bind,Where,Match,Or,Tap. - LINQ Support: Query syntax with
Select,SelectMany,Where. - Async Support:
MapAsync,BindAsync, and awaitable extensions. - Result Bridge: Convert
Maybe<T>toResult<T>and back. - Sequence Helpers:
TryFirst,TryLast,TryFind,Choose,Sequence,Traverse,Partition.
Targets net11.0, net10.0, net9.0 and netstandard2.1. Depends on CSharpEssentials.Results and CSharpEssentials.Errors. Maybe<T> and the non-generic Maybe (for Maybe.None, Maybe.From, Maybe.FromAsync and Maybe.FromTry) live in the CSharpEssentials.Maybe namespace.
Installation
dotnet add package CSharpEssentials.Maybe
Usage
Creating Maybe
using CSharpEssentials.Maybe;
public Maybe<User> FindUser(int id)
{
User? user = _repo.Find(id);
return user; // implicit from T? to Maybe<T>
}
Maybe<string> none = Maybe.None;
Maybe<int> value = 42;
Maybe<string> name = Maybe.From("Ann"); // the non-generic Maybe infers T
Maybe<int> five = 5.AsMaybe();
Maybe<int> parsed = Maybe.FromTry(() => int.Parse("42"));
Maybe<string> lazy = Maybe.From(() => Environment.GetEnvironmentVariable("HOME"));
From gives None for null. Maybe<T> serializes with System.Text.Json as {"HasValue":true,"value":5} and deserializes back from that shape.
Functional Chaining
string city = FindUser(1)
.Where(u => u.IsActive)
.Map(u => u.Address)
.Map(a => a.City)
.GetValueOrDefault("Unknown");
LINQ Queries
Maybe<string> tracking =
from user in FindUser(1)
from order in FindOrder(user.Id)
where order.Amount > 100
select order.TrackingNumber;
Matching
FindUser(1).Match(
some: user => Console.WriteLine($"Found: {user.Name}"),
none: () => Console.WriteLine("Not found"));
string label = FindUser(1).Match(some: u => u.Name, none: () => "stranger");
string loaded = await FindUser(1).MatchAsync(
some: async (u, ct) => await LoadProfileAsync(u, ct), // async handlers receive a CancellationToken
none: async ct => await LoadAnonymousAsync(ct),
cancellationToken: token);
Reading the Value
Maybe<int> maybe = 5;
if (maybe.HasValue) { int v = maybe.Value; } // Value throws InvalidOperationException on None
bool found = maybe.TryGetValue(out int value);
var (hasValue, inner) = maybe; // Deconstruct
int a = maybe.GetValueOrDefault(0); // fallback value
int b = maybe.GetValueOrDefault(() => 0); // lazy fallback
int c = maybe.GetValueOrDefault(v => v * 2, -1); // map when Some, else the fallback
int d = maybe.GetValueOrThrow("missing"); // or GetValueOrThrow(new MyException())
int? e = maybe.AsNullable(); // T? for reference types; default(T) for value types
List<int> list = maybe.ToList(); // one element, or empty
Filtering, Fallbacks and Side Effects
Maybe<int> big = maybe.Where(v => v > 3); // None when the predicate fails
Maybe<int> alt = Maybe<int>.None.Or(7); // Or(Maybe<T>) (a plain T converts), Or(Func<T>), Or(Func<Maybe<T>>)
maybe.Tap(v => Console.WriteLine(v)); // only when Some; returns the same Maybe
maybe.TapIf(v => v > 3, v => Console.WriteLine(v)); // predicate-gated tap
maybe.Execute(v => Console.WriteLine(v)); // Action<T> on Some
maybe.ExecuteNoValue(() => Console.WriteLine("none")); // Action on None
Maybe<int> doubled = maybe.MapIf(v => v > 3, v => v * 2);
Maybe<int> kept = maybe.BindIf(v => v > 3, v => Maybe<int>.From(v + 1));
MapIf, BindIf and TapIf also take a bool or a Func<bool> condition. MapIfAsync and BindIfAsync exist on Maybe<T>, Task<Maybe<T>> and ValueTask<Maybe<T>>; TapIfAsync exists on the Task and ValueTask sources.
Converting to Result
Maybe<Order> order = FindOrder(1);
Result<Order> result = order.ToResult(Error.NotFound("Order.Missing", "Order not found."));
// Result -> Maybe
Result<Order> result2 = new Order();
Maybe<Order> maybe = result2.AsMaybe();
ToResult and ToUnitResult are aliases of ToMaybeResult and ToMaybeUnitResult. Without an error argument a None becomes the Maybe.Result NotFound error. Always pass the error to ToResult(error), or call ToMaybeResult(): maybe.ToResult() with no argument binds the generic ToResult<TValue>(this TValue value) of CSharpEssentials.Results and returns a successful Result<Maybe<T>>. AsMaybe on a failed Result<T> gives None and drops the errors. The async forms are ToResultAsync, ToUnitResultAsync, ToMaybeResultAsync, ToMaybeUnitResultAsync and AsMaybeAsync.
Async
Maybe<User> user = await FindUserAsync(1);
Maybe<Order> order = await user
.BindAsync(u => GetOrderAsync(u.Id))
.MapAsync(o => EnrichAsync(o));
Every Maybe member that returns Task or ValueTask ends in Async: WhereAsync, ExecuteAsync, ExecuteNoValueAsync, OrAsync, MatchAsync, ToMaybeResultAsync, ToMaybeUnitResultAsync and Maybe.FromAsync (maybe.ExecuteAsync(async v => ...), task.OrAsync(() => ...)). The old unsuffixed overloads still compile as [Obsolete] forwarders and will be removed in 7.0. OrElseAsync keeps its name. An untyped async lambda on an instance ExecuteAsync, ExecuteNoValueAsync, OrAsync, WhereAsync or MatchAsync, or on a key/value MatchAsync, binds to the Task overload through OverloadResolutionPriority, which needs C# 13 or later; on C# 12, type the lambda or pass a typed local such as Func<T, Task>. The instance MapAsync has a Task and a ValueTask twin without that priority, so maybe.MapAsync(async v => ...) is ambiguous (CS0121) on every language version; pass a lambda that returns a Task instead (maybe.MapAsync(v => LoadAsync(v))) or type the delegate.
Collection Extensions
// Require ALL lookups to succeed — None if any is missing
Maybe<User[]> allUsers = userIds.Traverse(id => FindUser(id));
// Collect present values and count absences
var (values, missingCount) = maybes.Partition();
// Sequence: None if any element is None
Maybe<Config[]> configs = configMaybes.Sequence();
| Method | Returns | Behavior |
|---|---|---|
Sequence() |
Maybe<T[]> |
None if any element is None |
Traverse(selector) |
Maybe<TOut[]> |
Applies selector then sequences |
Partition() |
(T[] Values, int NoneCount) |
Never returns None; always splits |
Choose() |
IEnumerable<T> |
Keeps the present values; Choose(selector) maps them too, ChooseAsync streams a sequence of Task<Maybe<T>> |
TryFirst(), TryFirst(predicate), TryLast(), TryLast(predicate) |
Maybe<T> |
First or last element, None when there is none |
dictionary.TryFind(key) |
Maybe<TValue> |
Lookup on an IReadOnlyDictionary |
TryFirst is built on FirstOrDefault: for a sequence of value types such as int[], an empty sequence or a predicate without a match gives Some(default) instead of None. TryLast and the other helpers do not have this limitation.
Choosing between T?, Maybe<T> and Result<T>
| Use | When | Example |
|---|---|---|
T? |
At boundaries: DTOs, entity columns, JSON | public string? MiddleName { get; set; } |
Maybe<T> |
Inside the domain, when absence is an expected state composed with Map/Bind |
Maybe<User> user = _cache.TryFind(id); |
Result<T> |
When the failure carries a reason the caller acts on | Result<User> user = FindUser(id); |
| Exceptions | Only for bugs and invariant violations | ArgumentNullException, broken invariants |
// Boundary: a nullable column converts implicitly into the domain
Maybe<string> middleName = entity.MiddleName;
// Domain: absence is expected, compose it
string display = middleName.Map(n => n.ToUpperInvariant()).GetValueOrDefault("-");
// The caller needs to know why it is missing: upgrade to Result
Result<User> found = _cache.TryFind(id).ToMaybeResult(Error.NotFound("User.NotFound", "User does not exist"));
Maybe<T>.None never equals a non-null plain T: Maybe<int>.None == 0 is false, while Maybe<int>.From(0) == 0 is true. null compares as absence: Maybe<string>.None == null is true and Maybe<string>.From("a") == null is false, in either operand order. A typed null means absence, but Equals(object?) and maybe == (object?)null follow the BCL rule that x.Equals(null) is false: Maybe<string>.None.Equals((object?)null) and Maybe<string>.None == (object?)null are both false. The static type of the operand picks the rule, so cast to T? or use HasNoValue to test for absence. Prefer HasNoValue over == null or == default for clarity. For a value type T, maybe == default means default(T) (so Maybe<int>.None == default is false), and Maybe<int> == null does not compile cleanly (CS9342, a known overload ambiguity), so use HasNoValue. ToString() returns Some(value) or None; IFormattable values are formatted with the invariant culture.
Exception-safe Creation
using System.Text.Json;
// Returns None if the factory throws; OperationCanceledException is rethrown
Maybe<int> result = Maybe<int>.FromTry(() => int.Parse(userInput));
Maybe<User> user = Maybe<User>.FromTry(() => JsonSerializer.Deserialize<User>(json));
TapNone: Side Effect on Absence
Maybe<int> maybe = Maybe.None;
maybe.TapNone(() => logger.LogWarning("Value missing")); // called when None
// Async
await maybe.TapNoneAsync(async () => await NotifyAsync("Missing"));
// Via Task<Maybe<T>> / ValueTask<Maybe<T>>
await GetMaybeAsync().TapNoneAsync(() => logger.LogWarning("Value missing"));
GetValueOrElse: Lazy Fallback
// Factory only called when None (unlike GetValueOrDefault which always evaluates)
int value = maybe.GetValueOrElse(() => ComputeExpensiveDefault());
Flatten
Maybe<int> inner = Maybe<Maybe<int>>.From(Maybe<int>.From(5)).Flatten(); // Some(5)
Maybe<int>? stored = null; // e.g. a Maybe<int>? property mapped to a nullable column
Maybe<int> points = stored.Flatten(); // None
OrElse: Lazy Maybe Chain
// Alias for Or(Func<Maybe<T>>) — returns self if Some, calls factory if None
Maybe<Config> config = GetFromCache().OrElse(() => GetFromDatabase());
// Async
Maybe<Config> loaded = await GetFromCache().OrElseAsync(() => Task.FromResult(GetFromDatabase()));
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- CSharpEssentials.Errors (>= 6.5.2)
- CSharpEssentials.Results (>= 6.5.2)
- System.Text.Json (>= 9.0.4)
-
net10.0
- CSharpEssentials.Errors (>= 6.5.2)
- CSharpEssentials.Results (>= 6.5.2)
-
net11.0
- CSharpEssentials.Errors (>= 6.5.2)
- CSharpEssentials.Results (>= 6.5.2)
-
net9.0
- CSharpEssentials.Errors (>= 6.5.2)
- CSharpEssentials.Results (>= 6.5.2)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on CSharpEssentials.Maybe:
| Package | Downloads |
|---|---|
|
CSharpEssentials
A comprehensive C# library enhancing functional programming capabilities with type-safe monads (Maybe, Result), discriminated unions (Any), and robust error handling. Features include: domain-driven design support, enhanced Entity Framework integration, testable time management, JSON utilities, and LINQ extensions. Built for modern C# development with focus on maintainability, testability, and functional programming principles. |
|
|
CSharpEssentials.EntityFrameworkCore
Enhances Entity Framework Core with functional programming patterns and DDD-friendly features. Includes base entity classes, soft delete support, audit trails, query filters, optimistic concurrency, PostgreSQL integration, query performance monitoring, and domain event handling. Perfect for building maintainable and scalable data access layers in modern .NET applications. |
|
|
CSharpEssentials.Mediator
High-performance CQRS and pipeline behaviors built on the Mediator source-generator library. Provides Result-integrated commands, queries, validation, logging, caching, and transaction behaviors with a Result-first pipeline. |
|
|
CSharpEssentials.These
These<E, A> — a third result type that can carry Left (error), Right (value), or Both. Ideal for partial success scenarios, batch validation, and warning systems. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 6.5.2 | 172 | 10/10/2026 |
| 6.5.1 | 175 | 10/10/2026 |
| 6.5.0 | 176 | 10/9/2026 |
| 6.4.0 | 180 | 10/9/2026 |
| 6.3.0 | 176 | 10/8/2026 |
| 6.2.0 | 184 | 10/8/2026 |
| 6.1.0 | 187 | 10/7/2026 |
| 6.0.0 | 191 | 10/7/2026 |
| 5.2.1 | 190 | 10/7/2026 |
| 5.2.0 | 188 | 10/7/2026 |
| 5.1.0 | 190 | 10/7/2026 |
| 5.0.0 | 190 | 10/6/2026 |
| 4.1.0 | 192 | 10/6/2026 |
| 4.0.0 | 196 | 10/5/2026 |
| 3.2.3 | 421 | 5/31/2026 |
| 3.2.2 | 249 | 5/31/2026 |
| 3.2.1 | 217 | 5/31/2026 |
| 3.2.0 | 217 | 5/30/2026 |
| 3.1.0 | 229 | 5/27/2026 |
| 3.0.8 | 214 | 5/20/2026 |