Optima.Net 1.0.2

Prefix Reserved
There is a newer version of this package available.
See the version list below for details.
dotnet add package Optima.Net --version 1.0.2
                    
NuGet\Install-Package Optima.Net -Version 1.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Optima.Net" Version="1.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Optima.Net" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="Optima.Net" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Optima.Net --version 1.0.2
                    
#r "nuget: Optima.Net, 1.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Optima.Net@1.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Optima.Net&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=Optima.Net&version=1.0.2
                    
Install as a Cake Tool

Optima.Net Optional<T>

Optional<T> is a utility class designed to represent a value that may or may not exist. Its purpose is to provide a safer, more expressive alternative to null checks, making code easier to read and maintain.

Overview

  • Some(value): Wraps a value in an Optional.
  • None(): Represents the absence of a value.
  • Accessors: Value, ValueOrDefault, ValueOrNull, TryGetValue
  • Functional operations (sync): Map, Bind, Match, Tap, Where, Or, Zip, Flatten, ToEnumerable
  • Functional operations (async): MapAsync, BindAsync, MatchAsync, TapAsync, WhereAsync, OrAsync, ZipAsync

Optional<T> is immutable and thread-safe. It is intended to be used wherever a value might not be present, allowing developers to handle both cases explicitly and safely.

Creating Optionals

Optional with a value:

    var maybeUser = Optional<User>.Some(user);

Optional with no value:

    var noUser = Optional<User>.None();

Some(value) throws ArgumentNullException if the value is null.
None() is a singleton representing no value.

Accessing Values

Value:

    var user = maybeUser.Value; // Throws NullValueException if None

ValueOrDefault:

    var user = maybeUser.ValueOrDefault(defaultUser);

ValueOrNull:

    var user = maybeUser.ValueOrNull();

Functional Operations

Map:
Map transforms the inner value without unwrapping the Optional. Think of it like saying: “If I have a value, do something to it; if not, just keep None.” Example:

    Optional<int> maybeNumber = Optional<int>.Some(10);
    Optional<string> maybeString = maybeNumber.Map(n => $"Number is {n}");
    Console.WriteLine(maybeString.Value); // "Number is 10"

Input: Optional<T> Mapper: Func<T, TResult> Output: Optional<TResult>

If maybeNumber was None(), the result would also be None(). No exceptions.

Bind:
(also called FlatMap in some languages) Is for chaining operations that themselves return an Optional. Why not use Map? Because Map would give you Optional<Optional<TResult>>—nested Optionals. Bind flattens it. Example:

    Optional<int> maybeNumber = Optional<int>.Some(10);

    Optional<string> maybeString = maybeNumber.Bind(n =>
    {
        if (n > 5) return Optional<string>.Some($"Big number: {n}");
        return Optional<string>.None();
    });

    Console.WriteLine(maybeString.HasValue); // true
    Console.WriteLine(maybeString.Value);    // "Big number: 10"

Input: Optional<T> Binder: Func<T, Optional<TResult>> Output: Optional<TResult>

“If I have a value, I may or may not return another Optional; if I don’t have a value, the result is None.”

Match:
Is a safe way to handle both cases, Some and None, without throwing exceptions.

    maybeUser.Match(
	onSome: u => Console.WriteLine(u.Name),
	onNone: () => Console.WriteLine("No user found")
	);

You provide two functions:

onSome(T): what to do if there’s a value

onNone(): what to do if there isn’t

It returns the result of the function you call, so no need to unwrap manually or risk exceptions.

It’s basically a “pattern match” for Optionals.

Optional<T> Extensions

The extensions are divided into 3 name spaces:

- Optima.Net.Extensions  
- Optima.Net.Extensions.Collections  
- Optima.Net.Extensions.LINQ  
  

Tap:
If optional.HasValue is true, it executes the action with the contained value. Returns the original optional, which means you continue chaining operations like Map, Bind, or Match.

Does not modify the optional or create a new one, it is purely a side-effect hook.

    var maybeUser = Optional<User>.Some(user);

    maybeUser
    .Tap(u => Console.WriteLine($"Found user: {u.Name}"))
    .Map(u => u.Orders.Count)
    .Match(
        onSome: count => Console.WriteLine($"User has {count} orders"),
        onNone: () => Console.WriteLine("No user found")
    );

Logs the user if it exists. Continues mapping to get order count. Still handles the None case cleanly.

TapAsync:
Checks if optional.HasValue is true. If so, executes the provided async action (Func<T, Task>). Returns the original optional wrapped in a Task<Optional<T>> so you can continue chaining.

    var maybeUser = Optional<User>.Some(user);

    await maybeUser
    .TapAsync(async u => await notificationService.NotifyUserAsync(u))
    .Map(u => u.Orders.Count)
    .TapAsync(async count => await logger.LogAsync($"User has {count} orders"))
    .Match(
        onSome: count => Console.WriteLine($"Processed {count} orders"),
        onNone: () => Console.WriteLine("No user to process")
    );


Sends an async notification if the user exists. Logs the order count asynchronously. Still handles the None case safely.

Where:
If the optional is None, it returns None immediately. If the optional has a value, it evaluates the predicate:

  • True: returns the original optional.
  • False: returns None.

This lets you chain filters safely without null checks.

    var maybeUser = Optional<User>.Some(user);

    // Only keep active users
    var activeUser = maybeUser.Where(u => u.IsActive);

    activeUser.Match(
        onSome: u => Console.WriteLine($"{u.Name} is active"),
        onNone: () => Console.WriteLine("User is inactive or missing")
    );

If user is inactive, activeUser becomes None. The pipeline remains clean and expressive, no if statements needed.

Where: (for a collection of Optional<T>)
Same as a single the where on a single Optional<T>. Now you can filter the collection and remove the None(s)

    var users = new List<Optional<User>>
    {
        Optional<User>.Some(new User("Alice", true)),
        Optional<User>.Some(new User("Bob", false)),
        Optional<User>.None(),
        Optional<User>.Some(new User("Charlie", true))
    };

    // Keep only active users
    var activeUsers = users.Where(u => u.IsActive);

    foreach (var userOpt in activeUsers)
    {
        userOpt.Match(
            onSome: u => Console.WriteLine($"Active: {u.Name}"),
            onNone: () => {} // This won't run, because Where already filtered out Nones
        );
    }

    // Or get a flattened list of active users
    var activeUsersList = users
        .Where(u => u.IsActive)
        .Flatten()
        .ToList();

Where filters both Nones and values that fail the predicate. Flatten gives a clean IEnumerable<T> with only the values you care about. This is much better than iterating and checking each optional manually.

WhereAsync:
WhereAsync preserves the Optional pipeline model; no leaking null, no messy .Any() or await tangles. It’s safe and clean even with async I/O operations.

    var maybeUser = Optional<User>.Some(user);

    var maybeActiveUser = await maybeUser.WhereAsync(async u =>
    {
        // Pretend this checks a database or cache
        await Task.Delay(10);
        return u.IsActive;
    });

    maybeActiveUser.Match(
        onSome: u => Console.WriteLine($"Active user: {u.Name}"),
        onNone: () => Console.WriteLine("User inactive or not found")
    );

WhereAsync: (for a collection of Optional<T>)
WhereAsync preserves the Optional pipeline model; no leaking null, no messy .Any() or await tangles. It’s safe and clean even with async I/O operations.

    var users = new List<Optional<User>>
    {
        Optional<User>.Some(new User("Alice", true)),
        Optional<User>.Some(new User("Bob", false)),
        Optional<User>.None(),
        Optional<User>.Some(new User("Charlie", true))
    };

    var activeUsers = await users.WhereAsync(async u =>
    {
        await Task.Delay(5);
        return u.IsActive;
    });

    foreach (var user in activeUsers)
    {
        user.Tap(u => Console.WriteLine($"Active: {u.Name}"));
    }

It parallelizes using Task.WhenAll() for max efficiency.

Or:
If this optional has a value, return it. If not, return the fallback optional. That’s the Optional equivalent of the ?? (null-coalescing) operator, but safer and more expressive.

    var maybeUser = userCache.FindUser(id);
    var result = maybeUser.Or(() => userRepository.FindUser(id));

OrAsync:
If this optional has a value, return it. If not, return the fallback optional. That’s the Optional equivalent of the ?? (null-coalescing) operator, but safer and more expressive.

    var maybeUser = await userCache.FindUserAsync(id);
    var result = await maybeUser.OrAsync(() => userRepository.FindUserAsync(id));

If maybeUser already has a value, you get that. If not, it returns another Optional<T> or it calls the fallback factory userRepository.FindUser(id).

Flatten:
For nested optionals: Flattens a nested Optional<Optional<T>> into a single Optional<T>.

    Optional<Optional<T>> nested = ...;
    var flat = nested.Flatten(); // Optional<T>

Useful when you have operations that already return Optional<T> and you want to avoid double wrapping.

MatchAsync:
Just like Match, but the handlers (onSome, onNone) can be asynchronous functions. It’s especially useful when your match handlers do async work (e.g., database calls, HTTP requests, or logging).

Return Result

    var maybeUser = await userRepository.FindUserAsync(id);

    var message = await maybeUser.MatchAsync(
        onSome: async user =>
        {
            await auditService.RecordAccessAsync(user.Id);
            return $"User {user.Name} found";
        },
        onNone: async () =>
        {
            await logger.LogAsync("User not found");
            return "No user available";
        });

Side-Effect Only

await maybeUser.MatchAsync(
    onSome: async user => await emailService.SendWelcomeAsync(user),
    onNone: async () => await metrics.IncrementMissingUserAsync());

Chainable

await Optional<User>.Some(user)
    .TapAsync(async u => await audit.LogAccessAsync(u.Id))
    .WhereAsync(async u => await permissions.IsActiveAsync(u))
    .MatchAsync(
        onSome: async u => await logger.LogAsync($"User {u.Name} processed"),
        onNone: async () => await logger.LogAsync("No user to process"))
    .TapAsync(async _ => await metrics.IncrementProcessedAsync());

MapAsync:
If the optional has a value, it applies the async mapper function and wraps the result in a new Optional<TResult>.

Transforming values asynchronously while preserving the Optional pipeline.

    Optional<string> maybeApiKey = configRepository.GetApiKey("PaymentService");

    Optional<string> maybeEncryptedKey = await maybeApiKey.MapAsync(async key =>
    {
        await Task.Delay(50); // simulate async work
        return Encrypt(key);
    });

    maybeEncryptedKey.Match(
        onSome: encrypted => Console.WriteLine($"Encrypted key: {encrypted}"),
        onNone: () => Console.WriteLine("No API key present")
    );

Chaining Transformations

    Optional<User> maybeUser = await userService.FindUserByEmailAsync(email);

    Optional<OrderSummary> maybeOrderSummary = await maybeUser
        .MapAsync(async u => await orderService.GetLatestOrderForUserSummaryAsync(u.Id));

    await maybeOrderSummary.MatchAsync(
        onSome: async summary => await notificationService.NotifyUserAsync(summary),
        onNone: async () => await logger.LogAsync("No recent order found")
    );

Works with BindAsync, WhereAsync, TapAsync, and MatchAsync. Keeps your optional pipeline fully async without breaking the fluent interface.
Returns a Task<Optional<TResult>> so it can continue chaining in an await-based async flow.

BindAsync:
It unwraps an Optional<T> and applies an async binder function that itself returns an Optional<TResult>.
If the original optional is empty, it short-circuits and returns Optional<TResult>.None().
This is critical for building pipelines where multiple async steps might fail or not produce a value.

Chaining async operations

    Optional<User> maybeUser = await userService.FindUserByEmailAsync(email);

    Optional<Order> maybeLatestOrder = await maybeUser.BindAsync(async u =>
    {
        // this step might also return None
        return await orderService.GetLatestOrderForUserAsync(u.Id);
    });

    await maybeLatestOrder.MatchAsync(
        onSome: async order => await notificationService.NotifyUserAsync(order),
        onNone: async () => await logger.LogAsync("No recent order for this user")
    );

Mixing MapAsync and BindAsync

    Optional<string> maybeApiKey = configRepository.GetApiKey("PaymentService");

    Optional<EncryptedKey> maybeEncryptedKey = await maybeApiKey
        .MapAsync(async key => await PreEncryptAsync(key))
        .BindAsync(async preEncrypted => await EncryptKeyAsync(preEncrypted));

    await maybeEncryptedKey.MatchAsync(
        onSome: async encrypted => await logger.LogAsync($"Encrypted key ready: {encrypted}"),
        onNone: async () => await logger.LogAsync("No key available")
    );

TryGetValue:
TryGetValue is an alternative accessor to Value or ValueOrDefault. It returns a bool indicating whether the optional has a value, and outputs the value via an out parameter.

    Optional<User> maybeUser = userService.FindUserByEmail(email);

    if (maybeUser.TryGetValue(out var user))
    {
        Console.WriteLine($"User found: {user.Name}");
    }
    else
    {
        Console.WriteLine("No user found");
    }

Async example

    Optional<User> maybeUser = await userService.FindUserByEmailAsync(email);

    if (maybeUser.TryGetValue(out var user))
    {
        await logger.LogAsync($"User {user.Name} found");
    }
    else
    {
        await logger.LogAsync("User not found");
    }

Zip:
Zip takes two Optionals: Optional<T1> and Optional<T2>. If both have values, it applies a result selector function and returns Optional<TResult>.
If either is empty, the result is Optional<TResult>.None().

Eliminates nested if or .HasValue checks for multiple Optionals. Works well with Map, Bind, Where, and other pipeline operations. Provides a functional, declarative way to combine Optionals safely.

    Optional<User> maybeUser = userRepository.FindUserById(id);
    Optional<Profile> maybeProfile = profileRepository.FindProfileByUserId(id);

    Optional<UserProfile> maybeUserProfile = maybeUser.Zip(
        maybeProfile,
        (user, profile) => new UserProfile(user, profile)
    );

    maybeUserProfile.Match(
        onSome: up => Console.WriteLine($"UserProfile ready: {up.User.Name}"),
        onNone: () => Console.WriteLine("UserProfile not available")
    );

Chaining with Map

    Optional<User> maybeUser = userRepository.FindUserById(id);
    Optional<Profile> maybeProfile = profileRepository.FindProfileByUserId(id);

    Optional<string> maybeSummary = maybeUser
        .Zip(maybeProfile, (user, profile) => $"{user.Name} - {profile.Bio}")
        .Map(summary => summary.ToUpper());

    maybeSummary.Match(
        onSome: Console.WriteLine,
        onNone: () => Console.WriteLine("No summary available")
    );

ZipAsync:
Same as Zip just Async.

    Task<Optional<User>> maybeUserTask = userRepository.FindUserByIdAsync(id);
    Task<Optional<Profile>> maybeProfileTask = profileRepository.FindProfileByUserIdAsync(id);

    Optional<UserProfile> maybeUserProfile = await maybeUserTask.ZipAsync(
        maybeProfileTask,
        (user, profile) => new UserProfile(user, profile)
    );

    await maybeUserProfile.MatchAsync(
        onSome: async up => await logger.LogAsync($"UserProfile ready: {up.User.Name}"),
        onNone: async () => await logger.LogAsync("UserProfile not available")
    );

Chaining with other async operations

    Optional<User> maybeUser = await userRepository.FindUserByIdAsync(id);
    Optional<Profile> maybeProfile = await profileRepository.FindProfileByUserIdAsync(id);

    Optional<string> maybeSummary = await Task.FromResult(maybeUser)
        .ZipAsync(Task.FromResult(maybeProfile), (user, profile) => $"{user.Name} - {profile.Bio}")
        .MapAsync(async s => (await TranslateAsync(s)).ToUpper());

    await maybeSummary.MatchAsync(
        onSome: async summary => await logger.LogAsync($"Summary: {summary}"),
        onNone: async () => await logger.LogAsync("No summary available")
    );

ToEnumerable:

At first this looks a bit counter intuitive as there is no obvious benefit. Using LINQ query syntax or SelectMany works naturally when each Optional can be treated as a sequence of 0 or 1 items. You can combine two Optionals cleanly without writing nested if checks.

Not so usable:

    Optional<User> maybeUser = userRepository.FindUserById(id);

    foreach (var user in maybeUser.ToEnumerable())
    {
        Console.WriteLine($"User: {user.Name}");
    }

Using LINQ to combine to Optionals, an alternative to Zip.

    var combined = from user in maybeUser.ToEnumerable()
                from profile in maybeProfile.ToEnumerable()
                select $"{user.Name} - {profile.Bio}";

Integrating LINQ Pipelines

    var activeUserName = maybeUser
        .ToEnumerable()
        .Where(u => u.IsActive)
        .Select(u => u.Name)
        .FirstOrDefault(); // returns null if maybeUser is None or inactive

Flatten a List<Optional<User>> cleanly, resulting in a List<User> with only the present values, automatically skipping all Nones.

    var users = optionalUsers.SelectMany(u => u.ToEnumerable());

LINQ support

Select, SelectMany: query syntax:

    var result = from u in maybeUser
                 from o in orderService.GetLatestOrderForUser(u.Id)
                 select o.Total;

Where: query syntax:

    var activeUser = from u in maybeUser
                     where u.IsActive
                     select u;

Makes optionals feel native to C# pipelines.

More Examples

  1. Fetching Optional Configuration:
    Optional<string> maybeApiKey = configRepository.GetApiKey("PaymentService");

    maybeApiKey
    .Map(key => Encrypt(key))
    .Match(
    onSome: encryptedKey => logger.Log($"Using API key: {encryptedKey}"),
    onNone: () => logger.Log("No API key configured")
    );
  1. Chaining Service Calls:
    Optional<User> maybeUser = userService.FindUserByEmail(email);

    Optional<Order> maybeLatestOrder = maybeUser
    .Bind(u => orderService.GetLatestOrderForUser(u.Id));

    maybeLatestOrder.Match(
    onSome: order => notificationService.NotifyUser(order),
    onNone: () => logger.Log("No recent order for this user")
    );
  1. Input Validation and Transformation:
    Optional<string> maybeInput = Optional<string>.Some(request["age"]);

    Optional<int> maybeAge = maybeInput
    .Map(s => int.TryParse(s, out var n) ? n : throw new FormatException())
    .Bind(age => age > 0 ? Optional<int>.Some(age) : Optional<int>.None());

    maybeAge.Match(
    onSome: age => domainService.RegisterAge(age),
    onNone: () => logger.Log("Invalid age provided")
    );
  1. Conditional Feature Toggles:
    Optional<FeatureConfig> maybeFeature = featureService.GetFeatureConfig("NewDashboard");

    maybeFeature
    .Map(cfg => cfg.IsEnabled)
    .Match(
    onSome: enabled => enabled ? dashboardService.Show() : dashboardService.Hide(),
    onNone: () => dashboardService.Hide()
    );
  1. Event-Driven Data Enrichment:
    Optional<string> maybeCustomerId = eventPayload.CustomerId;

    maybeCustomerId
    .Bind(id => customerRepository.FindCustomer(id))
    .Map(customer => new EnrichedEvent(eventPayload, customer))
    .Match(
    onSome: enriched => eventProcessor.Process(enriched),
    onNone: () => logger.Log("Event skipped: missing or invalid customer")
    );

Notes

  • Use Optional<T> whenever a value might be absent, instead of null.
  • Functional operations (Map, Bind, Match) make pipelines readable and safe.
  • Optional<T> integrates cleanly with domain-driven designs and event-driven architectures.

Guideline Matrix

Scenario Recommended Approach Why
You just want the value or an exception if missing Value Simple and direct; throws NullValueException if None. Best for cases where absence is unexpected.
You want the value or a default ValueOrDefault(defaultValue) Avoids exceptions; allows a fallback value. Good for optional configuration or simple defaults.
You want the value or null ValueOrNull() Useful for nullable reference types. Simpler than ValueOrDefault(null).
Traditional C# Try-Get pattern TryGetValue(out var value) Familiar to C# developers; avoids exceptions and works nicely in conditional statements.
Perform side-effects or branching logic Match(onSome, onNone) Declarative and expressive; keeps code functional. Can also use MatchAsync for async side-effects.
Perform side-effects but keep fluent chaining Tap / TapAsync Executes a side-effect without breaking the chain; good for logging, metrics, or auditing.
Transform a value (sync) Map Produces a new Optional<TResult>; keeps pipeline functional.
Transform a value (async) MapAsync Allows async transformation while keeping pipeline fluent.
Chain operations that return Optionals (sync) Bind Avoids nested Optionals; essential for dependent computations.
Chain operations that return Optionals (async) BindAsync Async equivalent of Bind; keeps pipelines fully async and composable.
Filter based on a predicate (sync) Where Returns None if predicate fails; keeps pipeline clean.
Filter based on an async predicate WhereAsync Async equivalent; useful for DB/API validations or async checks.
Fallback to another Optional (sync) Or Provides a safe fallback Optional; avoids null coalescing logic.
Fallback to another Optional (async) OrAsync Async fallback; integrates with async pipelines.
Combine two Optionals (sync) Zip Combines two Optionals only if both are present. Useful for dependent values.
Combine two Optionals (async) ZipAsync Async version; waits for both Optionals before combining.
Integrate with LINQ pipelines or collections ToEnumerable Treats Optional as 0-or-1 sequence. Best when working with multiple Optionals, LINQ, or collections.
Flatten nested Optionals Flatten Converts Optional<Optional<T>> → Optional<T>; keeps pipeline simple.
Product Compatible and additional computed target framework versions.
.NET 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 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on Optima.Net:

Package Downloads
Optima.Net.Events

A fully self-validating Avro-based event serialization framework that dynamically generates, hashes, and verifies schemas for strongly consistent, schema-evolving domain events.

Optima.Net.DomainModel

Optima.Net.DomainModel Defines the immutable structural foundation of the domain, enforcing invariants and preventing illegal domain states without handling behavior, workflows, or persistence.

Optima.Net.NegotiatR

NegotiatR is a deterministic, single-pass negotiation engine that proposes alternative intents when policies fail. It consumes policy diagnostics, never re-evaluates rules, and guarantees outcomes. Designed for domain-driven systems, it keeps fallback decisions explicit, auditable, and separate from policy evaluation, workflows, and execution logic, without retries or implicit control flow.

Optima.Net.Decisioning

Optima.Net.Decisioning is a lightweight, immutable abstraction for representing what a system decided—and why—independent of how that decision was reached. It records intent, outcome, evidence, and optional negotiation results, providing a consistent, auditable decision contract without executing rules, workflows, or domain logic.

Optima.Net.Application

Optima.Net.Application provides the application-layer contracts and orchestration primitives that coordinate domain logic, persistence, and workflows without owning infrastructure or business rules.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.9 270 1/8/2026
1.0.8 319 12/24/2025
1.0.7 188 12/24/2025
1.0.6 259 11/10/2025
1.0.5 249 10/31/2025
1.0.4 204 10/27/2025
1.0.2 202 10/13/2025
1.0.1 194 10/13/2025
1.0.0 121 10/10/2025