Functional Extensions for C#
This library helps write code in more functional way. To get to know more about the principles behind it, check out the Applying Functional Principles in C# Pluralsight course.
Installation
Available on NuGet
dotnet add package CSharpFunctionalExtensions
or
PM> Install-Package CSharpFunctionalExtensions
Also available as a strong named assembly (big thanks to bothzoli who made it possible!).
On NuGet
dotnet add package CSharpFunctionalExtensions.StrongName
Core Concepts
Get rid of primitive obsession
Result<CustomerName> name = CustomerName.Create(model.Name);
Result<Email> email = Email.Create(model.PrimaryEmail);
Result result = Result.Combine(name, email);
if (result.IsFailure)
return Error(result.Error);
var customer = new Customer(name.Value, email.Value);
Make nulls explicit with the Maybe type
Maybe<Customer> customerOrNothing = _customerRepository.GetById(id);
if (customerOrNothing.HasNoValue)
return Error("Customer with such Id is not found: " + id);
Compose multiple operations in a single chain
return _customerRepository.GetById(id)
.ToResult("Customer with such Id is not found: " + id)
.Ensure(customer => customer.CanBePromoted(), "The customer has the highest status possible")
.Tap(customer => customer.Promote())
.Tap(customer => _emailGateway.SendPromotionNotification(customer.PrimaryEmail, customer.Status))
.Finally(result => result.IsSuccess ? Ok() : Error(result.Error));
Wrap multiple operations in a TransactionScope
return _customerRepository.GetById(id)
.ToResult("Customer with such Id is not found: " + id)
.Ensure(customer => customer.CanBePromoted(), "The customer has the highest status possible")
.WithTransactionScope(customer => Result.Success(customer)
.Tap(customer => customer.Promote())
.Tap(customer => customer.ClearAppointments()))
.Tap(customer => _emailGateway.SendPromotionNotification(customer.PrimaryEmail, customer.Status))
.Finally(result => result.IsSuccess ? Ok() : Error(result.Error));
API Examples
Maybe
Explicit Construction
Use case: Creating a new Maybe containing a value
Maybe<string> apple = Maybe<string>.From("apple");
// or
Maybe<string> apple = Maybe.From("apple"); // type inference
// or
var apple = Maybe.From("apple");
None/No Value
Use case: Replacing null
or the
Null Object Pattern for representing 'missing' data.
int storeInventory = ...
Maybe<string> fruit = storeInventory > 0
? Maybe<string>.From("apple")
: Maybe<string>.None;
// or where the generic type is a reference type
Maybe<string> fruit = null;
// or where the generic type is a value type
Maybe<int> fruit = default;
Implicit Conversion
Use case: Easily creating a Maybe from a value
// Constructing a Maybe
Maybe<string> apple = "apple"; // implicit conversion
// Or as a method return value
Maybe<string> GetFruit(string fruit)
{
if (string.IsNullOrWhiteSpace(fruit))
{
return Maybe<string>.None;
}
return fruit; // implicit conversion
}
Equality
Use case: Comparing Maybes or values without knowledge of the inner value of the Maybes
Maybe<string> apple = "apple";
Maybe<string> orange = "orange";
string alsoOrange = "orange";
Maybe<string> noFruit = Maybe<string>.None;
Console.WriteLine(apple == orange); // false
Console.WriteLine(apple != orange); // true
Console.WriteLine(orange == alsoOrange); // true
Console.WriteLine(alsoOrange == noFruit); // false
ToString
Maybe<string> apple = "apple";
Maybe<string> noFruit = Maybe<string>.None;
Console.WriteLine(apple.ToString()); // "apple"
Console.WriteLine(noFruit.ToString()); // "No value"
GetValueOrThrow
Use case: Procedurally accessing the inner value of the Maybe
Note: Calling this will throw a InvalidOperationException
if there is no value
Maybe<string> apple = "apple";
Maybe<string> noFruit = Maybe<string>.None;
Console.WriteLine(apple.GetValueOrThrow()); // "apple";
Console.WriteLine(noFruit.GetValueOrThrow()); // throws InvalidOperationException !!
Console.WriteLine(noFruit.GetValueOrThrow(new CustomException())); // throws CustomException !!
HasValue and HasNoValue
Use case: Procedurally checking if the Maybe has a value, usually before accessing the value directly
void Response(string fruit)
{
Console.WriteLine($"Yum, a {fruit} 😀");
}
Maybe<string> apple = "apple";
Maybe<string> noFruit = Maybe<string>.None;
if (apple.HasValue)
{
Response(apple.Value); // safe to access since we checked above
}
if (noFruit.HasNoValue)
{
Response("We're all out of fruit 😢");
}
GetValueOrDefault
Use case: Safely accessing the inner value, without checking if there is one, by providing a fallback if no value exists
void Response(string fruit)
{
Console.WriteLine($"It's a {fruit}");
}
Maybe<string> apple = "apple";
Maybe<string> unknownFruit = Maybe<string>.None;
string appleValue = apple.GetValueOrDefault("banana");
string unknownFruitValue = unknownFruit.GetValueOrDefault("banana");
Response(appleValue); // It's a apple
Response(unknownFruitValue); // It's a banana
Where
Use case: Converting a Maybe with a value to a Maybe.None
if a condition isn't met
Note: The predicate passed to Where
(ex )
bool IsMyFavorite(string fruit)
{
return fruit == "papaya";
}
Maybe<string> apple = "apple";
Maybe<string> favoriteFruit = apple.Where(IsMyFavorite);
Console.WriteLine(favoriteFruit.ToString()); // "No value"
Map
Use case: Transforming the value in the Maybe, if there is one, without needing to check if the value is there
Note: the delegate (ex CreateMessage
) passed to Maybe.Map()
is only executed if the Maybe has an inner value
string CreateMessage(string fruit)
{
return $"The fruit is a {fruit}";
}
Maybe<string> apple = "apple";
Maybe<string> noFruit = Maybe<string>.None;
Console.WriteLine(apple.Map(CreateMessage).Unwrap("No fruit")); // "The fruit is a apple"
Console.WriteLine(noFruit.Map(CreateMessage).Unwrap("No fruit")); // "No fruit"
Select
Alias: Maybe.Select()
is an alias of Maybe.Map()
Bind
Use case: Transforming from one Maybe into another Maybe
(like Maybe.Map
but it transforms the Maybe instead of the inner value)
Note: the delegate (ex MakeAppleSauce
) passed to Maybe.Bind()
is only executed if the Maybe has an inner value
Maybe<string> MakeAppleSauce(Maybe<string> fruit)
{
if (fruit == "apple") // we can only make applesauce from apples 🍎
{
return "applesauce";
}
return Maybe<string>.None;
}
Maybe<string> apple = "apple";
Maybe<string> banana = "banana";
Maybe<string> noFruit = Maybe<string>.None;
Console.WriteLine(apple.Bind(MakeAppleSauce).ToString()); // "applesauce"
Console.WriteLine(banana.Bind(MakeAppleSauce).ToString()); // "No value"
Console.WriteLine(noFruit.Bind(MakeAppleSauce).ToString()); // "No value"
SelectMany
Alias: Maybe.SelectMany()
is an alias of Maybe.Bind()
Choose
Use case: Filter a collection of Maybes to only the ones that have a value, and then return the value for each, or map that value to a new one
Note: the delegate passed to Maybe.Choose()
is only executed on the Maybes of the collection with an inner value
IEnumerable<Maybe<string>> unknownFruits = new[] { "apple", Maybe<string>.None, "banana" };
IEnumerable<string> knownFruits = unknownFruits.Choose();
IEnumerable<string> fruitResponses = unknownFruits.Choose(fruit => $"Delicious {fruit}");
Console.WriteLine(string.Join(", ", knownFruits)) // "apple, banana"
Console.WriteLine(string.Join(", ", fruitResponses)) // "Delicious apple, Delicious banana"
Execute
Use case: Safely executing a void
(or Task
) returning operation on the Maybe inner value
without checking if there is one
Note: the Action
(ex PrintFruit
) passed to Maybe.Execute()
is only executed if the Maybe has an inner value
void PrintFruit(string fruit)
{
Console.WriteLine($"This is a {fruit}");
}
Maybe<string> apple = "apple";
Maybe<string> noFruit = Maybe<string>.None;
apple.Execute(PrintFruit); // "This is a apple"
noFruit.Execute(PrintFruit); // no output to the console
ExecuteNoValue
Use case: Executing a void
(or Task
) returning operation when the Maybe has no value
void LogNoFruit(string fruit)
{
Console.WriteLine($"There are no {fruit}");
}
Maybe<string> apple = "apple";
Maybe<string> banana = Maybe<string>.None;
apple.ExecuteNoValue(() => LogNoFruit("apple")); // no output to console
banana.ExecuteNoValue(() => LogNoFruit("banana")); // "There are no banana"
Or
Use case: Supplying a fallback value Maybe or value in the case that the Maybe has no inner value
Note: The fallback Func<T>
(ex () => "banana"
) will only be executed
if the Maybe has no inner value
Maybe<string> apple = "apple";
Maybe<string> banana = "banana";
Maybe<string> noFruit = Maybe<string>.None;
Console.WriteLine(apple.Or(banana).ToString()); // "apple"
Console.WriteLine(noFruit.Or(() => banana)).ToString()); // "banana"
Console.WriteLine(noFruit.Or("banana").ToString()); // "banana"
Console.WriteLine(noFruit.Or(() => "banana").ToString()); // "banana"
Match
Use case: Defining two operations to perform on a Maybe. One to be executed if there is an inner value, and the other to executed if there is not
Maybe<string> apple = "apple";
Maybe<string> noFruit = Maybe<string>.None;
// Void returning Match
apple.Match(
fruit => Console.WriteLine($"It's a {fruit}"),
() => Console.WriteLine("There's no fruit"));
// Mapping Match
string fruitMessage = noFruit.Match(
fruit => $"It's a {fruit}",
() => "There's no fruit"));
Console.WriteLine(fruitMessage); // "There's no fruit"
TryFirst and TryLast
Use case: Replacing .FirstOrDefault()
and .LastOrDefault()
so that you can return a
Maybe instead of a null
or value type default value (like 0
, false
) when working with collections
IEnumerable<string> fruits = new[] { "apple", "coconut", "banana" };
Maybe<string> firstFruit = fruits.TryFirst();
Maybe<string> probablyABanana = fruits.TryFirst(fruit => fruit.StartsWith("ba"));
Maybe<string> aPeachOrAPear = fruits.TryFirst(fruit => fruit.StartsWith("p"));
Console.WriteLine(firstFruit.ToString()); // "apple"
Console.WriteLine(probablyABanana.ToString()); // "banana"
Console.WriteLine(aPeachOrAPear.ToString()); // "No value"
Maybe<string> lastFruit = fruits.TryLast();
Maybe<string> anAppleOrApricot = fruits.TryLast(fruit => fruit.StartsWith("a"));
Console.WriteLine(lastFruit.ToString()); // "banana"
Console.WriteLine(anAppleOrApricot.ToString()); // "apple"
TryFind
Use case: Safely getting a value out of a Dictionary
Dictionary<string, int> fruitInventory = new()
{
{ "apple", 10 },
{ "banana", 2 }
};
Maybe<int> appleCount = fruitInventory.TryFind("apple");
Maybe<int> kiwiCount = fruitInventory.TryFind("kiwi");
Console.WriteLine(appleCount.ToString()); // "10"
Console.WriteLine(kiwiCount.ToString()); // "No value"
ToResult
Use case: Representing the lack of an inner value in a Maybe as a failed operation
Note: See Result
section below
Maybe<string> fruit = "banana";
Maybe<string> noFruit = Maybe<string>.None;
string errorMessage = "There was no fruit to give";
Result<string> weGotAFruit = fruit.ToResult(errorMessage);
Result<string> failedToGetAFruit = noFruit.ToResult(errorMessage);
Console.WriteLine(weGotAFruit.Value); // "banana"
Console.WriteLine(failedToGetAFruit.Error); // "There was no fruit to give"
ToUnitResult
Use case: Representing the lack of an inner value in a Maybe as a failed operation, if an Error is provided
Use case: Representing the presence of an inner value in a Maybe as a failed operation
Note: See UnitResult
section below
Maybe<Error> error = new Error();
Maybe<string> noFruit = Maybe<string>.None;
UnitResult<Error> weGotAnError = error.ToUnitResult();
UnitResult<Error> failedToGetAFruit = noFruit.ToUnitResult(new Error());
Console.WriteLine(weGotAnError.IsFailure); // true
Console.WriteLine(failedToGetAFruit.IsFailure); // true
Result
Explicit Construction: Success and Failure
Use case: Creating a new Result in a Success or Failure state
record FruitInventory(string Name, int Count);
Result<FruitInventory> appleInventory = Result.Success(new FruitInventory("apple", 4));
Result<FruitInventory> failedOperation = Result.Failure<FruitInventory>("Could not find inventory");
Result successInventoryUpdate = Result.Success();
To create a success result of a value you can also use the Of
method which has overloads for Func<T>
and Task<T>
.
Result<Something> something = Result.Of(_service.CreateSomething());
Result<Something> something = await Result.Of(_service.CreateSomethingAsync());
Result<Something> something = Result.Of(() => _service.CreateSomething());
Result<Something> something = await Result.Of(() => _service.CreateSomethingAsync());
Conditional Construction: SuccessIf and FailureIf
Use case: Creating successful or failed Results based on expressions or delegates instead of if/else statements or ternary expressions
bool onTropicalIsland = true;
Result foundCoconut = Result.SuccessIf(onTropicalIsland, "These trees seem bare 🥥");
Result foundGrapes = Result.FailureIf(() => onTropicalIsland, "No grapes 🍇 here");
// or
bool isNewShipmentDay = true;
Result<FruitInventory> appleInventory = Result.SuccessIf(isNewShipmentDay, new FruitInventory("apple", 4), "No 🍎 today");
Result<FruitInventory> bananaInventory = Result.SuccessIf(() => isNewShipmentDay, new FruitInventory("banana", 2), "All out of 🍌");
// or
bool afterBreakfast = true;
Result<FruitInventory> orangeInventory = Result.FailureIf(afterBreakfast, new FruitInventory("orange", 10), "No 🍊 today");
Result<FruitInventory> grapefruitInventory = Result.FailureIf(() => afterBreakfast, new FruitInventory("grapefruit", 5), "No grapefruit 😢");
Implicit Conversion
Use case: Easily creating a successful result from a value
Result<FruitInventory> appleInventory = new FruitInventory("apple", 4);
Result failedInventoryUpdate = "Could not update inventory";
ToString
Use case: Printing out the state of a Result and its inner value or error
Result<FruitInventory> appleInventory = new FruitInventory("apple", 4);
Result<FruitInventory> bananaInventory = Result.Failure<FruitInventory>("Could not find any bananas");
Result failedInventoryUpdate = "Could not update inventory";
Result successfulInventoryUpdate = Result.Success();
Console.WriteLine(appleInventory.ToString()); // "Success(FruitInventory { Name = apple, Count = 4 })"
Console.WriteLine(bananaInventory.ToString()); // "Failure(Could not find any bananas)"
Console.WriteLine(failedInventoryUpdate.ToString()); // "Failure(Could not update inventory)"
Console.WriteLine(successfulInventoryUpdate.ToString()); // "Success"
Map
Use case: Transforming the inner value of a successful Result, without needing to check on the success/failure state of the Result
Note: the delegate (ex CreateMessage
) passed to Result.Map()
is only executed if the Result was successful
string CreateMessage(FruitInventory inventory)
{
return $"There are {inventory.Count} {inventory.Name}(s)";
}
Result<FruitInventory> appleInventory = new FruitInventory("apple", 4);
Result<FruitInventory> bananaInventory = Result.Failure<FruitInventory>("Could not find any bananas");
Console.WriteLine(appleInventory.Map(CreateMessage).ToString()); // "Success(There are 4 apple(s))"
Console.WriteLine(bananaInventory.Map(CreateMessage).ToString()); // "Failure(Could not find any bananas)"
MapError
Use case: Transforming the inner error of a failed Result, without needing to check on the success/failure state of the Result
Note: the delegate (ex ErrorEnhancer
) passed to Result.MapError()
is only executed if the Result failed
string ErrorEnhancer(string errorMessage)
{
return $"Failed operation: {errorMessage}";
}
Console.WriteLine(appleInventory.MapError(ErrorEnhancer).ToString()); // "Success(FruitInventory { Name = apple, Count = 4 })"
Console.WriteLine(bananaInventory.MapError(ErrorEnhancer).ToString()); // "Failed operation: Could not find any bananas"
Testing
CSharpFunctionalExtensions.FluentAssertions
A small set of extensions to make test assertions more fluent when using CSharpFunctionalExtensions! Check out the repo for this library more information!
Includes custom assertions for
- Maybe
- Result
- Result
- Result<T, E>
- UnitResult
Example
var result = Result.Success(420);
result.Should().Succeed(); // passes
result.Should().SucceedWith(420); // passes
result.Should().SucceedWith(69); // throws
result.Should().Fail(); // throws
Web APIs / HttpResults
CSharpFunctionalExtensions.HttpResults
This library provides convenient extension methods to seamlessly map Results from CSharpFunctionalExtensions to HttpResults. With this, it streamlines your Web API resulting in cleaner, more fluent code.
Key Benefits
- ⚙️ Zero Configuration: Get started immediately — the mapping works out of the box without any configuration.
- 🛠️ Customizable Mappings: Tailor default mappings or define custom mappings for specific use cases.
- 🔗 Fluent API: Maintain a smooth, railway-oriented flow by chaining HttpResult mappings at the end of your Result chain.
- 🧱 Separation of Domain and HTTP Errors: Keeps domain errors distinct from HTTP errors, improving maintainability and clarity between business logic and web API concerns.
- ⚡ Minimal APIs & Controllers Support: Works with both Minimal APIs and traditional controllers in ASP.NET.
- 📦 Full Support for ASP.NET Results: Supports all built-in HTTP response types in ASP.NET, including
Ok
,Created
,NoContent
,Accepted
,FileStream
, and more. - 🦺 Typed Results: Utilizes
TypedResults
for consistent, type-safe API responses. - 📑 OpenAPI Ready: Ensures accurate OpenAPI generation for clear and reliable API documentation.
- 🛡️ RFC Compliance: Default mappings adhere to the RFC 9457 standard (
ProblemDetails
), ensuring your API errors are standardized and interoperable. - 🧑💻 Developer-Friendly: Includes built-in analyzers and source generators to speed up development and reduce errors.
Example
app.MapGet("/books", (BookService service) =>
service.Get() //Result<Book[]>
.ToOkHttpResult() //Results<Ok<Book[]>, ProblemHttpResult>
);
Check the repo out here.
Analyzers
CSharpFunctionalExtensions.Analyzers
A Roslyn analyzer package that provides warnings and recommendations to prevent misuse of Result
objects in CSharpFunctionalExtensions
. Ensures more robust implementation when working with Result types.
Available on NuGet
dotnet add package CSharpFunctionalExtensions.Analyzers
Read or Watch more about these ideas
- Functional C#: Primitive obsession
- Functional C#: Non-nullable reference types
- Functional C#: Handling failures, input errors
- Applying Functional Principles in C# Pluralsight course
Related Projects
Contributors
A big thanks to the project contributors!
- Yehuda Ringler
- Rory Sánchez
- Chris C
- Marcin Jahn
- Jannes Kaspar-Müller
- dbuckin1
- bothzoli
- Pavel Zemlianikin
- Simon Lang
- Nils Vreman
- Scheichsbeutel
- Alexey Malinin
- Robert Larkins
- tinytownsoftware
- piotr121993
- Dmitry Korotin
- michalsznajder
- Xavier
- Julien Aspirot
- Kyle McMaster
- Vinícius Beloni Cubas
- rutkowskit
- Giovanni Costagliola
- Mark Wainwright
- ProphetLamb
- Paul Williams
- alexmurari
- ruud
- Tomasz Malinowski
- Staffan Wingren
- Tim Schneider
- Piotr Karasiński
- Marcel Roozekrans
- guythetechie
- Logan Kahler
- Ali Khalili
- Andrei Andreev
- YudApps
- dataphysix
- Laszlo Lueck
- Sean G. Wright
- Samuel Viesselman
- Stian Kroknes
- dataneo
- michaeldileo
- Renato Ramos Nascimento
- Patrick Drechsler
- Vadim Mingazhev
- Darick Carpenter
- Stéphane Mitermite
- Markus Nißl
- Adrian Frielinghaus
- svroonland
- JvSSD
- Vladimir Makaev
- Ben Smith
- pedromtcosta
- Michał Bator
- mukmyash
- azm102
- ThomasDC
- bopazyn
- Joris Goovaerts
- Ivan Deev
- Damian Płaza
- ergwun
- Michael DiLeo
- Jean-Claude
- Matt Jenkins
- Michael Altmann
- Steven Giesel
- Anton Hryshchanka
- Mikhail Bashurov
- kostekk88
- Carl Abrahams
- golavr
- Sviataslau Hankovich
- Chad Gilbert
- Robert Sęk
- Sergey Solomentsev
- Malcolm J Harwood
- Dragan Stepanovic
- Ivan Novikov
- Denis Molokanov
- Gerald Wiltse
- yakimovim
- Alex Erygin
- Omar Aloraini