FluentCoder.Maybe
1.5.0
dotnet add package FluentCoder.Maybe --version 1.5.0
NuGet\Install-Package FluentCoder.Maybe -Version 1.5.0
<PackageReference Include="FluentCoder.Maybe" Version="1.5.0" />
<PackageVersion Include="FluentCoder.Maybe" Version="1.5.0" />
<PackageReference Include="FluentCoder.Maybe" />
paket add FluentCoder.Maybe --version 1.5.0
#r "nuget: FluentCoder.Maybe, 1.5.0"
#:package FluentCoder.Maybe@1.5.0
#addin nuget:?package=FluentCoder.Maybe&version=1.5.0
#tool nuget:?package=FluentCoder.Maybe&version=1.5.0
<div align="center">
<img src="https://raw.githubusercontent.com/lucafabbri/maybe/main/maybe_logo.png" alt="drawing" width="350"/></br>
Maybe
An elegant, fluent, and intuitive way to handle operations that may succeed or fail.
dotnet add package Maybe
</div>
- Give it a star ⭐!
- Philosophy: Beyond Error Handling
- Core Concepts
- Getting Started 🏃
- Creating a
Maybe
instance - API Reference: Our Vocabulary
- Expressive Success Outcomes
- Custom Errors
- Contribution 🤲
- License 🪪
Give it a star ⭐!
Loving it? Show your support by giving this project a star!
Philosophy: Beyond Error Handling
Maybe
is more than just an error-handling library; it's a tool for writing clearer, more expressive, and more resilient code. It encourages you to think about the different outcomes of your operations, not just success or failure.
By using an elegant, fluent API, Maybe
guides you to:
- Write code that reads like a business process.
- Handle both success and failure paths explicitly.
- Eliminate unexpected runtime exceptions.
- Seamlessly compose synchronous and asynchronous operations.
Core Concepts
Maybe
is designed to be simple for common cases, but powerful for advanced scenarios.
Simplified Usage with Maybe<TValue>
For the majority of use cases, you only need to specify the success type. The error type defaults to a built-in Error
struct that covers all common failure scenarios.
// This signature is clean and simple.
public Maybe<User> FindUser(int id)
{
if (id > 0)
{
return new User(id, "Alice");
}
// Return a built-in error type.
return Error.NotFound("User.NotFound", "The user was not found.");
}
Advanced Usage with Maybe<TValue, TError>
When you need to return a custom, strongly-typed error with specific data, you can use the two-parameter version. This gives you full control over the failure path.
public record UserCreationError(string Field, string Message) : IError { /* ... */ }
public Maybe<User, UserCreationError> CreateUser(string email)
{
if (string.IsNullOrEmpty(email))
{
return new UserCreationError("Email", "Email cannot be empty.");
}
// ...
}
Progressive Enhancement with IOutcome
When you need to communicate a more specific success state (like Created
or Updated
), you can return a value that implements the IOutcome
interface. Maybe
will automatically inspect the value and adopt its specific OutcomeType
, enriching your return value.
// 'Created' implements IOutcome and has its own OutcomeType
public Maybe<Created> CreateUser(string name)
{
// ... create user ...
return Outcomes.Created;
}
var result = CreateUser("Bob");
// result.Type is now 'OutcomeType.Created', not the default 'Success'.
Getting Started 🏃
From Throwing Exceptions to Returning Outcomes
This 👇
public User GetUserById(int id)
{
var user = _db.Users.Find(id);
if (user is null)
{
throw new UserNotFoundException("User not found");
}
return user;
}
Turns into this 👇, using the powerful Match
method to handle both outcomes safely.
public Maybe<User> GetUserById(int id)
{
var user = _db.Users.Find(id);
if (user is null)
{
return Error.NotFound("User.NotFound", "User was not found.");
}
return user;
}
GetUserById(1)
.Match(
onSome: user => Console.WriteLine(user.Name),
onNone: error => Console.WriteLine(error.Message));
Fluent Chaining with Sync & Async Interop
The true power of Maybe
lies in its fluent DSL. The API is designed to be intuitive, automatically handling the transition between synchronous and asynchronous contexts without needing different method names.
// This example finds a user, validates their status, gets their permissions, and transforms the result.
// Notice how .Select and .Ensure are used on an async source without needing an "Async" suffix.
var result = await Api.FindUserAsync(userId) // Start: Task<Maybe<User>>
.Ensure(user => user.IsActive, Errors.UserInactive) // Then: Sync validation
.Select(user => user.Name.ToUpper()) // Then: Sync transformation
.ThenAsync(name => Api.GetPermissionsAsync(name)) // Then: Async chain
.Select(permissions => permissions.ToUpper()); // Finally: Sync transformation
Creating a Maybe
instance
Creating a Maybe
is designed to be frictionless, primarily through implicit conversions.
public Maybe<User> FindUser(int id)
{
if (id == 1)
{
return new User(1, "Alice", true); // Implicit conversion from User to Maybe<User>
}
return Error.NotFound(); // Implicit conversion from Error to Maybe<User>
}
API Reference: Our Vocabulary
Then (Bind / FlatMap)
Purpose: To chain an operation that itself returns a Maybe
. This is the primary method for sequencing operations that can fail.
// Finds a user, and if successful, gets their permissions.
Maybe<string, PermissionsError> result = Api.FindUserInDb(1)
.Then(user => Api.GetPermissions(user));
Select (Map)
Purpose: To transform the value inside a successful Maybe
into something else, without altering the Maybe
's state.
// Finds a user, and if successful, selects their email address.
Maybe<string, UserNotFoundError> userEmail = Api.FindUserInDb(1)
.Select(user => user.Email);
Ensure (Validate)
Purpose: To check if the value inside a successful Maybe
meets a specific condition. If the condition is not met, the chain is switched to an error state.
The library provides two sets of Ensure
overloads:
Ergonomic (Preserves Error Type): Used when the validation error is of the same type as the
Maybe
's error channel.Maybe<User, PermissionsError> validatedUser = GetUser() // Returns Maybe<User, PermissionsError> .Ensure(u => u.IsActive, new PermissionsError()); // Error is also PermissionsError
Unifying (Changes Error Type): Used when the validation introduces a new, potentially incompatible error type, unifying the result to a
Maybe<TValue>
.Maybe<User> validatedUser = GetUser() // Returns Maybe<User, UserNotFoundError> .Ensure(u => u.Age > 18, Error.Validation("User.NotAdult")); // Introduces a new Error
Recover (Error Handling Bind)
Purpose: To handle a failure by executing a recovery function that can return a new Maybe
.
// Try to find a user in the database. If not found, try the cache.
Maybe<User, CacheError> result = await Api.FindUserInDbAsync(1)
.RecoverAsync(error => Api.FindUserInCache(1));
Match (Unwrap)
Purpose: To safely exit the Maybe
context by providing functions for both success and error cases.
string message = maybeUser.Match(
onSome: user => $"Welcome, {user.Name}!",
onNone: error => $"Error: {error.Message}"
);
Else (Fallback)
Purpose: To exit the Maybe
context by providing a default value in case of an error.
string userName = maybeUser.Select(u => u.Name).Else("Guest");
IfSome / IfNone (Side Effects)
Purpose: To perform an action (like logging) without altering the Maybe
. It returns the original Maybe
, allowing the chain to continue.
Maybe<User, UserNotFoundError> finalResult = Api.FindUserInDb(1)
.IfSome(user => Console.WriteLine($"User found: {user.Id}"))
.IfNone(error => Console.WriteLine($"Failed to find user: {error.Code}"));
ThenDo / ElseDo (Terminal Side Effects)
Purpose: To perform a final action on success (ThenDo
) or failure (ElseDo
). These methods terminate the fluent chain.
// Example: Final logging after a chain of operations
await Api.FindUserInDbAsync(1)
.Then(Api.GetPermissions)
.ThenDoAsync(permissions => Log.Information($"Permissions granted: {permissions}"))
.ElseDoAsync(error => Log.Error($"Operation failed: {error.Code}"));
Expressive Success Outcomes
As explained in the Core Concepts, you can use types that implement IOutcome
to communicate richer success states. Maybe
provides a set of built-in, stateless struct
types for common "void" operations, accessible via the Outcomes
static class:
Outcomes.Success
Outcomes.Created
Outcomes.Updated
Outcomes.Deleted
Outcomes.Accepted
Outcomes.Unchanged
new Cached<T>(value)
public Maybe<Deleted> DeleteUser(int id)
{
if (UserExists(id))
{
_db.Users.Remove(id);
return Outcomes.Deleted; // More expressive than returning void or true
}
return Error.NotFound();
}
Custom Errors
While the built-in Error
struct is sufficient for many cases, you can create your own strongly-typed errors by implementing IError
. This is the primary use case for Maybe<TValue, TError>
.
public record InvalidEmailError(string Email) : IError
{
public OutcomeType Type => OutcomeType.Validation;
public string Code => "Email.Invalid";
public string Message => $"The email '{Email}' is not a valid address.";
}
public Maybe<User, InvalidEmailError> CreateUser(string email)
{
if (!IsValid(email))
{
return new InvalidEmailError(email);
}
// ...
}
Contribution 🤲
If you have any questions, comments, or suggestions, please open an issue or create a pull request 🙂
License 🪪
This project is licensed under the terms of the MIT license.
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 is compatible. 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 was computed. 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. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. 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.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on FluentCoder.Maybe:
Package | Downloads |
---|---|
FluentCoder.Maybe.Compat.ErrorOr
A compatibility layer to use FluentCoder.Maybe with ErrorOr library. |
GitHub repositories
This package is not used by any popular GitHub repositories.