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