Danom 3.0.0-beta1

This is a prerelease version of Danom.
dotnet add package Danom --version 3.0.0-beta1
                    
NuGet\Install-Package Danom -Version 3.0.0-beta1
                    
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="Danom" Version="3.0.0-beta1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Danom" Version="3.0.0-beta1" />
                    
Directory.Packages.props
<PackageReference Include="Danom" />
                    
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 Danom --version 3.0.0-beta1
                    
#r "nuget: Danom, 3.0.0-beta1"
                    
#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 Danom@3.0.0-beta1
                    
#: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=Danom&version=3.0.0-beta1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Danom&version=3.0.0-beta1&prerelease
                    
Install as a Cake Tool

Danom

NuGet Version build license aot net8.0 net6.0 netstandard2.1

Danom provides Option, Result and Choice types for C#, inspired by F#. It’s designed to be easy to use, efficient, and compatible with existing C# codebases. These discriminated unions offer a type-safe way to represent nullable values, expected errors and decisions, while also supporting a fluent API (e.g., map, bind) for chaining operations and value transformations.

Key Features

  • Implementation of common monads: Option, Result and Choice.
  • Exhaustive matching to prevent null reference exceptions.
  • Fluent API for chaining operations.
  • Built-in error handling with ResultErrors.
  • An API for parsing strings into .NET primitives and value types.
  • Unit type to represent the absence of a value.

Design Goals

  • netstandard2.1 compatible.
  • Enhance functional programming in C#.
  • Provide opinionated monads to encourage consistent use.
  • Help reduce (or eliminate) null reference exceptions.
  • Minimize overhead by creating efficients implementations.

Getting Started

Install the Danom NuGet package:

dotnet add package Danom

OR

PM>  Install-Package Danom

Looking for Validation or ASP.NET Core Integration?

Quick Start

using Danom;

// Option
Option.Some(5)
    .Map(x => x * 2)
    // ^--- transform the value
    .Bind(x => x > 5 ? Option.Some(x) : Option<int>.None())
    // ^--- chain another operation that returns an Option
    .Match(
        some: x => Console.WriteLine("Value: {0}", x),
        none: () => Console.WriteLine("No value"));
    // ^-- Output: Value: 10

// Result
public Result<int, string> TryDivide(int numerator, int denominator) =>
    denominator == 0
        ? Result<int, string>.Error("Cannot divide by zero")
        : Result<int, string>.Ok(numerator / denominator);

TryDivide(10, 2)
    .Map(x => x + 1)
    // ^--- transform the value
    .Bind(x => TryDivide(x, 0))
    // ^--- chain another operation that returns a Result
    .MapError(e => $"Error occurred: {e}")
    // ^--- transform the error
    .Match(
        ok: x => Console.WriteLine("Result: {0}", x),
        error: e => Console.WriteLine("Error: {0}", e));
    // ^-- Output: Error: Error occurred: Cannot divide by zero

// Choice
public void PrintChoice(Choice<int, string> choice) =>
    choice.Match(
        t1: x => Console.WriteLine("Got int: {0}", x),
        t2: s => Console.WriteLine("Got string: {0}", s));

PrintChoice(Choice<int, string>.FromT1(42));
// ^-- Output: Got int: 42
PrintChoice(Choice<int, string>.FromT2("Hello"));
// ^-- Output: Got string: Hello

Option

Options have an underlying type and can optionally hold a value of that type. Options are a much safer way to handle nullable values, they virtually eliminate null reference exceptions. They also provide a fantastic means of reducing primitive congestion in your code.

Creating Options

var option = Option<int>.Some(5);

// or, with type inference
var optionInferred = Option.Some(5);

// or, with no value
var optionNone = Option<int>.NoneValue;

// also returns none
var optionNull = Option<object>.Some(default!);

Using Option

Options are commonly used when a operation might not return a value. For example, the method below tries to find a number in a list that satisfies a predicate. If the number is found, it is returned as a Some, otherwise, None is returned.

using Danom;

public Option<int> TryFind(IEnumerable<int> numbers, Func<int, bool> predicate) =>
    numbers.FirstOrDefault(predicate).ToOption();

With this method defined we can begin performing operations against the Option result:

using Danom;

IEnumerable<int> nums = [1,2,3];

// Exhaustive matching
TryFind(nums, x => x == 1)
    .Match(
        some: x => Console.WriteLine("Found: {0}", x),
        none: () => Console.WriteLine("Did not find number"));

// Mapping the value (i.e., I want to access the value)
var optionSum =
    TryFind(nums, x => x == 1)
        .Map(x => x + 1);

// Binding the option (i.e., when a nested operation also returns an Option)
var optionBindSum =
    TryFind(nums, x => x == 1)
        .Bind(num1 =>
            TryFind(nums, x => x == 2)
                .Map(num2 => num1 + num2));

// Handling "None"
var optionDefault =
    TryFind(nums, x => x == 4)
        .DefaultValue(99);

var optionDefaultWith =
    TryFind(nums, x => x == 4)
        .DefaultWith(() => 99); // useful if creating the value is expensive

var optionOrElse =
    TryFind(nums, x => x == 4)
        .OrElse(Option<int>.Some(99));

varoptionOrElseWith =
    TryFind(nums, x => x == 4)
        .OrElseWith(() => Option<int>.Some(99)); // useful if creating the value is expensive

// Procedural style
var result = TryFind(nums, x => x == 1);

if (result.TryGet(out var value)) {
    Console.WriteLine("Found: {0}", value);
}

Result

Results are used to represent a success or failure outcome. They provide a more concrete way to manage the expected errors of an operation, then throwing exceptions. Especially in recoverable or reportable scenarios.

Creating Results

using Danom;

var result = Result<int, string>.Ok(5);

// or, with an error
var resultError = Result<int, string>.Error("An error occurred");

Using Results

Results are commonly used when an operation might not succeed, and you want to manage or report back the expected errors. For example:

Let's create a simple inline function to divide two numbers. If the denominator is zero, we want to return an error message.

using Danom;

public Result<int, string> TryDivide(int numerator, int denominator) =>
    denominator == 0
        ? Result<int, string>.Error("Cannot divide by zero")
        : Result<int, string>.Ok(numerator / denominator);

With this method defined we can begin performing operations against the result:

using Danom;

// Exhaustive matching
TryDivide(10, 2)
    .Match(
        ok: x => Console.WriteLine("Result: {0}", x),
        error: e => Console.WriteLine("Error: {0}", e));

// Mapping the value
var resultSum =
    TryDivide(10, 2)
        .Map(x => x + 1);

// Binding the result (i.e., when a nested operation also returns a Result)
var resultBindSum =
    TryDivide(10, 2)
        .Bind(num1 =>
            TryDivide(20, 2)
                .Map(num2 =>
                    num1 + num2));

// Handling errors
var resultDefault =
    TryDivide(10, 0)
        .DefaultValue(99);

var resultDefaultWith =
    TryDivide(10, 0)
        .DefaultWith(() => 99); // useful if creating the value is expensive

var resultOrElse =
    TryDivide(10, 0)
        .OrElse(Result<int, string>.Ok(99));

var resultOrElseWith =
    TryDivide(10, 0)
        .OrElseWith(() =>
            Result<int, string>.Ok(99)); // useful if creating the value is expensive

// Procedural style
var result = TryDivide(10, 2);

if (result.TryGet(out var value)) {
    Console.WriteLine("Result: {0}", value);
}

// or, accessing the error
if (result.TryGetError(out var error)) {
    Console.WriteLine("Error: {0}", error);
}

Result Errors

Danom provides a built-in error type, ResultErrors, to simplify the creation of results with multiple errors. This type can be initialized with a single string, a collection of strings, or a key-value pair. It can be thought of as a domain-specific dictionary of string keys and N string values.

using Danom;

var resultOk = Result.Ok(5); // or, Result<int>.Ok(5);

var resultErrors =
    Result<int>.Error("An error occurred");

var resultErrorsMultiError =
    Result<int>.Error(["An error occurred", "Another error occurred"]);

var resultErrorsKeyed =
    Result<int>.Error("error-key", "An error occurred");

var resultErrorsKeyedMulti =
    Result<int>.Error("error-key", ["An error occurred", "Another error occurred"]);

Choice

The Choice type is a discriminated union that allows you to represent a value that can be one of several different types. It is similar to the Option and Result types but supports multiple possible value types (up to 5 type parameters). This is useful when you want to model a value that can take on different forms, each with its own type.

Creating Choices

using Danom;

var choice1 = Choice<int, string>.FromT1(42); // holds an int
var choice2 = Choice<int, string>.FromT2("Hello"); // holds a string

Using Choices

Choices are commonly used when a value can be one of several different types. For example, you might have a function that can return either an integer or a string based on some condition (you can imagine how this gets more interesting with complex types).

using Danom;

public Choice<int, string> GetValue(bool returnInt) =>
    returnInt
        ? Choice<int, string>.FromT1(42)
        : Choice<int, string>.FromT2("Hello");

GetValue(true)
    .Match(
        t1: x => Console.WriteLine("Got int: {0}", x),
        t2: s => Console.WriteLine("Got string: {0}", s));

// Procedural style
var choice = GetValue(false);

if (choice.TryGetT1(out var intValue)) {
    Console.WriteLine("Got int: {0}", intValue);
}

if (choice.TryGetT2(out var stringValue)) {
    Console.WriteLine("Got string: {0}", stringValue);
}

Unit

Danom provides a Unit type to represent the absence of a value. This type is useful in functional programming as a way to represent the absence of a meaningful value. It acts as a placeholder when a function needs to return something, but there is no actual data to return—similar to void in C#, but as a real type that can be used in generic code, composed in monads like Option and Result, or passed as a value. This enables more consistent and expressive APIs, especially when working with functional patterns, pipelines, or workflows where a type is always required.

using Danom;

void Log(string message) => Console.WriteLine(message);

// Convert an Action to a Func that returns Unit
Func<string, Unit> logFunc = Log.ToUnitFunc();

// Use in a functional pipeline
Option<string> maybeMessage = Option.Some("Hello, world!");

maybeMessage
    // Logs the message and returns Option<Unit>
    .Map(logFunc);

Procedural Programming

Inevitably you'll need to interact with these functional types in a procedural way. Both Option and Result provide a TryGet method to retrieve the underlying value. This method will return a bool indicating whether the value was successfully retrieved and the value itself as an output parameter.

Option TryGet

using Danom;

var option = Option<int>.Some(5);

if (option.TryGet(out var value)) {
    Console.WriteLine("Value: {0}", value);
}
else {
    Console.WriteLine("No value");
}

Result TryGet and TryGetError

using Danom;

var result = Result<int, string>.Ok(5);

if (result.TryGet(out var value)) {
    Console.WriteLine("Result: {0}", value);
}
else if (result.TryGetError(out var error)) {
    Console.WriteLine("Error: {0}", error);
}
else {
    Console.WriteLine("No value or error");
}

Choice TryGetTn

using Danom;

var choice = Choice<int, string>.FromT1(42);

if (choice.TryGetT1(out var intValue)) {
    Console.WriteLine("Got int: {0}", intValue);
}
else if (choice.TryGetT2(out var stringValue)) {
    Console.WriteLine("Got string: {0}", stringValue);
}

String Parsing

Most applications will at some point need to parse strings into primitives and value types. This is especially true when working with external data sources.

Option provides a natural mechanism to handle the case where the string cannot be parsed. The "TryParse" API is provided to simplify the process of parsing strings into .NET primitives and value types.

using Danom;

// a common pattern
var x = int.TryParse("123", out var y) ? Option<int>.Some(y) : Option<int>.NoneValue;

// or, more simply using the TryParse API
var myInt = intOption.TryParse("123"); // -> Some(123)
var myDouble = doubleOption.TryParse("123.45"); // -> Some(123.45)
var myBool = boolOption.TryParse("true"); // -> Some(true)

// if the string cannot be parsed
var myIntNone = intOption.TryParse("danom"); // -> None
var myDoubleNone = doubleOption.TryParse("danom"); // -> None
var myBoolNone = boolOption.TryParse("danom"); // -> None

// null strings are treated as None
var myIntNull = intOption.TryParse(null); // -> None

The full API is below:

public static class boolOption {
    public static Option<bool> TryParse(string? x);
}

public static class byteOption {
    public static Option<byte> TryParse(string? x, System.Globalization.NumberStyles styles, IFormatProvider? provider);
    public static Option<byte> TryParse(string? x);
}

public static class shortOption {
    public static Option<short> TryParse(string? x, System.Globalization.NumberStyles styles, IFormatProvider? provider);
    public static Option<short> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<short> TryParse(string? x);
}

public static class intOption {
    public static Option<int> TryParse(string? x, System.Globalization.NumberStyles styles, IFormatProvider? provider);
    public static Option<int> TryParse(string? x);
}

public static class longOption {
    public static Option<long> TryParse(string? x, System.Globalization.NumberStyles styles, IFormatProvider? provider);
    public static Option<long> TryParse(string? x);
}

public static class decimalOption {
    public static Option<decimal> TryParse(string? x, System.Globalization.NumberStyles styles, IFormatProvider? provider);
    public static Option<decimal> TryParse(string? x);
}

public static class doubleOption {
    public static Option<double> TryParse(string? x, System.Globalization.NumberStyles styles, IFormatProvider? provider);
    public static Option<double> TryParse(string? x);
}

public static class floatOption {
    public static Option<float> TryParse(string? x, System.Globalization.NumberStyles styles, IFormatProvider? provider);
    public static Option<float> TryParse(string? x);
}

public static class GuidOption {
    public static Option<Guid> TryParse(string? x);
    public static Option<Guid> TryParseExact(string? x, string? format);
}

public static class DateTimeOffsetOption {
    public static Option<DateTimeOffset> TryParse(string? x, IFormatProvider? provider, System.Globalization.DateTimeStyles dateTimeStyles);
    public static Option<DateTimeOffset> TryParse(string? x);
    public static Option<DateTimeOffset> TryParseExact(string? x, string? format, IFormatProvider? provider = null, System.Globalization.DateTimeStyles dateTimeStyles = System.Globalization.DateTimeStyles.None);
}

public static class DateTimeOption {
    public static Option<DateTime> TryParse(string? x, IFormatProvider? provider, System.Globalization.DateTimeStyles dateTimeStyles);
    public static Option<DateTime> TryParse(string? x);
    public static Option<DateTime> TryParseExact(string? x, string? format, IFormatProvider? provider = null, System.Globalization.DateTimeStyles dateTimeStyles = System.Globalization.DateTimeStyles.None);
}

#if NET6_0_OR_GREATER
public static class DateOnlyOption {
    public static Option<DateOnly> TryParse(string? x, IFormatProvider? provider, System.Globalization.DateTimeStyles dateTimeStyles);
    public static Option<DateOnly> TryParse(string? x);
    public static Option<DateOnly> TryParseExact(string? x, string? format, IFormatProvider? provider = null, System.Globalization.DateTimeStyles dateTimeStyles = System.Globalization.DateTimeStyles.None);
}

public static class TimeOnlyOption {
    public static Option<TimeOnly> TryParse(string? x, IFormatProvider? provider, System.Globalization.DateTimeStyles dateTimeStyles);
    public static Option<TimeOnly> TryParse(string? x);
    public static Option<TimeOnly> TryParseExact(string? x, string? format, IFormatProvider? provider = null, System.Globalization.DateTimeStyles dateTimeStyles = System.Globalization.DateTimeStyles.None);
}
#endif

public static class TimeSpanOption {
    public static Option<TimeSpan> TryParse(string? x, IFormatProvider? provider = null);
    public static Option<TimeSpan> TryParse(string? x);
    public static Option<TimeSpan> TryParseExact(string? x, string? format, IFormatProvider? provider = null);
}

public static class EnumOption {
    public static Option<TEnum> TryParse<TEnum>(string? x) where TEnum : struct, Enum;
}

Find a bug?

There's an issue for that.

License

Licensed under MIT.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  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 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 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. 
.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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.1

    • No dependencies.
  • net10.0

    • No dependencies.
  • net6.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on Danom:

Package Downloads
Danom.Validation

Validators and validation helpers for Danom.

Danom.Mvc

ASP.NET Core MVC and Razor Pages support for Danom.

Danom.MinimalApi

ASP.NET Core Minimal API support for Danom.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0-beta1 176 8/1/2026
2.1.0 2,312 12/30/2025
2.0.0 463 9/17/2025
2.0.0-beta6 305 8/8/2025
2.0.0-beta5 237 7/17/2025
2.0.0-beta4 302 6/24/2025
2.0.0-beta3 229 6/23/2025
2.0.0-beta2 386 5/16/2025
2.0.0-beta1 292 4/18/2025
1.2.0 849 12/6/2024
1.1.1 231 12/6/2024
1.1.0 206 12/1/2024
1.0.0 258 11/18/2024
1.0.0-beta1 235 10/11/2024
1.0.0-alpha3 199 8/31/2024
1.0.0-alpha2 250 8/30/2024
1.0.0-alpha1 194 8/28/2024