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.
Available on NuGet
dotnet add package CSharpFunctionalExtensionsor
PM> Install-Package CSharpFunctionalExtensionsResult<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);Maybe<Customer> customerOrNothing = _customerRepository.GetById(id);
if (customerOrNothing.HasNoValue)
return Error("Customer with such Id is not found: " + id);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));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));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");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;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
}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); // falseMaybe<string> apple = "apple";
Maybe<string> noFruit = Maybe<string>.None;
Console.WriteLine(apple.ToString()); // "apple"
Console.WriteLine(noFruit.ToString()); // "No value"Use case: Procedurally accessing the inner value of the Maybe
Note: Accessing this property will throw a InvalidOperationException if there is no value
Maybe<string> apple = "apple";
Maybe<string> noFruit = Maybe<string>.None;
Console.WriteLine(apple.Value); // "apple";
Console.WriteLine(noFruit.Value); // throws InvalidOperationException !!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 😢");
}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.UnWrap("banana");
string unknownFruitValue = unknownFruit.UnWrap("banana");
Response(appleValue); // It's a apple
Response(unknownFruitValue); // It's a bananaUse 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); // "No value"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"Alias: Maybe.Select() is an alias of Maybe.Map()
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)); // "applesauce"
Console.WriteLine(banana.Bind(MakeAppleSauce)); // "No value"
Console.WriteLine(noFruit.Bind(MakeAppleSauce)); // "No value"Alias: Maybe.SelectMany() is an alias of Maybe.Bind()
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(", ", fruits)) // "apple, banana"
Console.WriteLine(string.Join(", ", fruitResponses)) // "Delicious apple, Delicious banana"Use case: Safely executing a void 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 consoleUse 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)); // "apple"
Console.WriteLine(noFruit.Or(() => banana))); // "banana"
Console.WriteLine(noFruit.Or("banana")); // "banana"
Console.WriteLine(noFruit.Or(() => "banana")); // "banana"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
Maybe<string> fruitMessage = noFruit.Match(
fruit => $"It's a {fruit}",
() => "There's no fruit"));
Console.WriteLine(fruitMessage); // "There's no fruit"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); // "apple"
Console.WriteLine(probablyABanana); // "banana"
Console.WriteLine(aPeachOrAPear); // "No value"
Maybe<string> lastFruit = fruits.TryLast();
Maybe<string> anAppleOrApricot = fruits.TryLast(fruit => fruit.StartsWith("a"));
Console.WriteLine(lastFruit); // "banana"
Console.WriteLine(anAppleOrApricot); // "apple"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); // "10"
Console.WriteLine(kiwiCount); // "No value"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"// TODO
For extension methods on top of this library's Result and Maybe that you can use in tests,
you can use FluentAssertions
with this NuGet package (GitHub link).
Example:
// Arrange
var myClass = new MyClass();
// Act
Result result = myClass.TheMethod();
// Assert
result.Should().BeSuccess();- Functional C#: Primitive obsession
- Functional C#: Non-nullable reference types
- Functional C#: Handling failures, input errors
- Applying Functional Principles in C# Pluralsight course
A big thanks to the project contributors!
- 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
- mnissl
- 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