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);Resultresult=Result.Combine(name,email);if(result.IsFailure)returnError(result.Error);varcustomer=newCustomer(name.Value,email.Value);Maybe<Customer>customerOrNothing=_customerRepository.GetById(id);if(customerOrNothing.HasNoValue)returnError("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");// orMaybe<string>apple=Maybe.From("apple");// type inference// orvarapple=Maybe.From("apple");Use case: Replacing null or the
Null Object Pattern for representing 'missing' data.
intstoreInventory= ...Maybe<string> fruit =storeInventory>0?Maybe<string>.From("apple"):Maybe<string>.None;// or where the generic type is a reference typeMaybe<string>fruit=null;// or where the generic type is a value typeMaybe<int>fruit=default;Use case: Easily creating a Maybe from a value
// Constructing a MaybeMaybe<string>apple="apple";// implicit conversion// Or as a method return valueMaybe<string>GetFruit(stringfruit){if(string.IsNullOrWhiteSpace(fruit)){returnMaybe<string>.None;}returnfruit;// 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";stringalsoOrange="orange";Maybe<string>noFruit=Maybe<string>.None;Console.WriteLine(apple==orange);// falseConsole.WriteLine(apple!=orange);// trueConsole.WriteLine(orange==alsoOrange);// trueConsole.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: 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(newCustomException()));// throws CustomException !!Use case: Procedurally checking if the Maybe has a value, usually before accessing the value directly
voidResponse(stringfruit){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
voidResponse(stringfruit){Console.WriteLine($"It's a {fruit}");}Maybe<string>apple="apple";Maybe<string>unknownFruit=Maybe<string>.None;stringappleValue=apple.GetValueOrDefault("banana");stringunknownFruitValue=unknownFruit.GetValueOrDefault("banana");Response(appleValue);// It's a appleResponse(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 )
boolIsMyFavorite(stringfruit){returnfruit=="papaya";}Maybe<string>apple="apple";Maybe<string>favoriteFruit=apple.Where(IsMyFavorite);Console.WriteLine(favoriteFruit.ToString());// "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
stringCreateMessage(stringfruit){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";}returnMaybe<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"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 (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
voidPrintFruit(stringfruit){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: Executing a void (or Task) returning operation when the Maybe has no value
voidLogNoFruit(stringfruit){Console.WriteLine($"There are no {fruit}");}Maybe<string>apple="apple";Maybe<string>banana=Maybe<string>.None;apple.ExecuteNoValue(()=>LogNoFruit("apple"));// no output to consolebanana.ExecuteNoValue(()=>LogNoFruit("banana"));// "There are no banana"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"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 Matchapple.Match(
fruit =>Console.WriteLine($"It's a {fruit}"),()=>Console.WriteLine("There's no fruit"));// Mapping MatchstringfruitMessage=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.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"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"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;stringerrorMessage="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"Use case: Creating a new Result in a Success or Failure state
recordFruitInventory(stringName,intCount);Result<FruitInventory>appleInventory=Result.Success(newFruitInventory("apple",4));Result<FruitInventory>failedOperation=Result.Failure<FruitInventory>("Could not find inventory");ResultsuccessInventoryUpdate=Result.Success();Use case: Creating successful or failed Results based on expressions or delegates instead of if/else statements or ternary expressions
boolonTropicalIsland=true;ResultfoundCoconut=Result.SuccessIf(onTropicalIsland,"These trees seem bare 🥥");ResultfoundGrapes=Result.FailureIf(()=>onTropicalIsland,"No grapes 🍇 here");// orboolisNewShipmentDay=true;Result<FruitInventory>appleInventory=Result.SuccessIf(isNewShipmentDay,newFruitInventory("apple",4),"No 🍎 today");Result<FruitInventory>bananaInventory=Result.SuccessIf(()=>isNewShipmentDay,newFruitInventory("banana",2),"All out of 🍌");// orboolafterBreakfast=true;Result<FruitInventory>orangeInventory=Result.FailureIf(afterBreakfast,newFruitInventory("orange",10),"No 🍊 today");Result<FruitInventory>grapefruitInventory=Result.FailureIf(()=>afterBreakfast,newFruitInventory("grapefruit",5),"No grapefruit 😢");Use case: Easily creating a successful result from a value
Result<FruitInventory>appleInventory=newFruitInventory("apple",4);ResultfailedInventoryUpdate="Could not update inventory";Use case: Printing out the state of a Result and its inner value or error
Result<FruitInventory>appleInventory=newFruitInventory("apple",4);Result<FruitInventory>bananaInventory=Result.Failure<FruitInventory>("Could not find any bananas");ResultfailedInventoryUpdate="Could not update inventory";ResultsuccessfulInventoryUpdate=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"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
stringCreateMessage(FruitInventoryinventory){return$"There are {inventory.Count}{inventory.Name}(s)";}Result<FruitInventory>appleInventory=newFruitInventory("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)"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
stringErrorEnhancer(stringerrorMessage){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"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
varresult=Result.Success(420);result.Should().Succeed();// passesresult.Should().SucceedWith(420);// passesresult.Should().SucceedWith(69);// throwsresult.Should().Fail();// throwsA 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- 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!
- 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
- 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