Awesome
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<T>
- 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
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!
- 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