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 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
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);
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; // 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;
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); // false
Maybe<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(new CustomException())); // throws CustomException !!
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.GetValueOrDefault("banana"); string unknownFruitValue = unknownFruit.GetValueOrDefault("banana"); Response(appleValue); // It's a apple Response(unknownFruitValue); // It's a banana
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"
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).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(", ", knownFruits)) // "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
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
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"
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 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"
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; 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"
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
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());
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 😢");
Use case: Easily creating a successful result from a value
Result<FruitInventory> appleInventory = new FruitInventory("apple", 4); Result failedInventoryUpdate = "Could not update inventory";
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"
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)"
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"
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
var result = Result.Success(420); result.Should().Succeed(); // passes result.Should().SucceedWith(420); // passes result.Should().SucceedWith(69); // throws result.Should().Fail(); // throws
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.
- ⚙️ 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.
app.MapGet("/books", (BookService service) => service.Get() //Result<Book[]> .ToOkHttpResult() //Results<Ok<Book[]>, ProblemHttpResult> );
Check the repo out here.
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
- 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!
- 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