Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/vkhorikov/CSharpFunctionalExtensions

Functional extensions for C#
https://github.com/vkhorikov/CSharpFunctionalExtensions

csharp entity functional-programming maybe-monad result value-object

Last synced: about 2 months ago
JSON representation

Functional extensions for C#

Awesome Lists containing this project

README

        

# Functional Extensions for C#

[![Build Status](https://dev.azure.com/EnterpriseCraftsmanship/CSharpFunctionalExtensions/_apis/build/status/CSharpFunctionalExtensions?branchName=master)](https://dev.azure.com/EnterpriseCraftsmanship/CSharpFunctionalExtensions/_build/latest?definitionId=1&branchName=master)
[![NuGet downloads](https://img.shields.io/nuget/v/csharpfunctionalextensions.svg)](https://www.nuget.org/packages/CSharpFunctionalExtensions/)
[![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/LICENSE)

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](https://enterprisecraftsmanship.com/ps-func).

## Installation

Available on [NuGet](https://www.nuget.org/packages/CSharpFunctionalExtensions/)

```bash
dotnet add package CSharpFunctionalExtensions
```

or

```powershell
PM> Install-Package CSharpFunctionalExtensions
```

Also available as a strong named assembly (big thanks to [bothzoli](https://github.com/bothzoli) who made it possible!).

On [NuGet](https://www.nuget.org/packages/CSharpFunctionalExtensions.StrongName/)

```bash
dotnet add package CSharpFunctionalExtensions.StrongName
```

## Core Concepts

### Get rid of primitive obsession

```csharp
Result name = CustomerName.Create(model.Name);
Result 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

```csharp
Maybe customerOrNothing = _customerRepository.GetById(id);
if (customerOrNothing.HasNoValue)
return Error("Customer with such Id is not found: " + id);
```

## Compose multiple operations in a single chain

```csharp
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

```csharp
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

```csharp
Maybe apple = Maybe.From("apple");

// or

Maybe apple = Maybe.From("apple"); // type inference

// or

var apple = Maybe.From("apple");
```

#### None/No Value

Use case: Replacing `null` or the
[Null Object Pattern](https://enterprisecraftsmanship.com/2015/03/13/functional-c-non-nullable-reference-types/) for representing 'missing' data.

```csharp
int storeInventory = ...

Maybe fruit = storeInventory > 0
? Maybe.From("apple")
: Maybe.None;

// or where the generic type is a reference type

Maybe fruit = null;

// or where the generic type is a value type

Maybe fruit = default;
```

#### Implicit Conversion

Use case: Easily creating a Maybe from a value

```csharp
// Constructing a Maybe
Maybe apple = "apple"; // implicit conversion

// Or as a method return value
Maybe GetFruit(string fruit)
{
if (string.IsNullOrWhiteSpace(fruit))
{
return Maybe.None;
}

return fruit; // implicit conversion
}
```

#### Equality

Use case: Comparing Maybes or values without knowledge of the
inner value of the Maybes

```csharp
Maybe apple = "apple";
Maybe orange = "orange";
string alsoOrange = "orange";
Maybe noFruit = Maybe.None;

Console.WriteLine(apple == orange); // false
Console.WriteLine(apple != orange); // true
Console.WriteLine(orange == alsoOrange); // true
Console.WriteLine(alsoOrange == noFruit); // false
```

#### ToString

```csharp
Maybe apple = "apple";
Maybe noFruit = Maybe.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

```csharp
Maybe apple = "apple";
Maybe noFruit = Maybe.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

```csharp
void Response(string fruit)
{
Console.WriteLine($"Yum, a {fruit} 😀");
}

Maybe apple = "apple";
Maybe noFruit = Maybe.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

```csharp
void Response(string fruit)
{
Console.WriteLine($"It's a {fruit}");
}

Maybe apple = "apple";
Maybe unknownFruit = Maybe.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 )

```csharp
bool IsMyFavorite(string fruit)
{
return fruit == "papaya";
}

Maybe apple = "apple";

Maybe 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

```csharp
string CreateMessage(string fruit)
{
return $"The fruit is a {fruit}";
}

Maybe apple = "apple";
Maybe noFruit = Maybe.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

```csharp
Maybe MakeAppleSauce(Maybe fruit)
{
if (fruit == "apple") // we can only make applesauce from apples 🍎
{
return "applesauce";
}

return Maybe.None;
}

Maybe apple = "apple";
Maybe banana = "banana";
Maybe noFruit = Maybe.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

```csharp
IEnumerable> unknownFruits = new[] { "apple", Maybe.None, "banana" };

IEnumerable knownFruits = unknownFruits.Choose();
IEnumerable 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

```csharp
void PrintFruit(string fruit)
{
Console.WriteLine($"This is a {fruit}");
}

Maybe apple = "apple";
Maybe noFruit = Maybe.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

```csharp
void LogNoFruit(string fruit)
{
Console.WriteLine($"There are no {fruit}");
}

Maybe apple = "apple";
Maybe banana = Maybe.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` (ex `() => "banana"`) will only be executed
if the Maybe has no inner value

```csharp
Maybe apple = "apple";
Maybe banana = "banana";
Maybe noFruit = Maybe.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

```csharp
Maybe apple = "apple";
Maybe noFruit = Maybe.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

```csharp
IEnumerable fruits = new[] { "apple", "coconut", "banana" };

Maybe firstFruit = fruits.TryFirst();
Maybe probablyABanana = fruits.TryFirst(fruit => fruit.StartsWith("ba"));
Maybe aPeachOrAPear = fruits.TryFirst(fruit => fruit.StartsWith("p"));

Console.WriteLine(firstFruit.ToString()); // "apple"
Console.WriteLine(probablyABanana.ToString()); // "banana"
Console.WriteLine(aPeachOrAPear.ToString()); // "No value"

Maybe lastFruit = fruits.TryLast();
Maybe 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

```csharp
Dictionary fruitInventory = new()
{
{ "apple", 10 },
{ "banana", 2 }
};

Maybe appleCount = fruitInventory.TryFind("apple");
Maybe 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

```csharp
Maybe fruit = "banana";
Maybe noFruit = Maybe.None;

string errorMessage = "There was no fruit to give";

Result weGotAFruit = fruit.ToResult(errorMessage);
Result 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

```csharp
Maybe error = new Error();
Maybe noFruit = Maybe.None;

UnitResult weGotAnError = error.ToUnitResult();
UnitResult 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

```csharp
record FruitInventory(string Name, int Count);

Result appleInventory = Result.Success(new FruitInventory("apple", 4));
Result failedOperation = Result.Failure("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` and `Task`.

```csharp
Result something = Result.Of(_service.CreateSomething());
Result something = await Result.Of(_service.CreateSomethingAsync());
Result something = Result.Of(() => _service.CreateSomething());
Result 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

```csharp
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 appleInventory = Result.SuccessIf(isNewShipmentDay, new FruitInventory("apple", 4), "No 🍎 today");
Result bananaInventory = Result.SuccessIf(() => isNewShipmentDay, new FruitInventory("banana", 2), "All out of 🍌");

// or

bool afterBreakfast = true;

Result orangeInventory = Result.FailureIf(afterBreakfast, new FruitInventory("orange", 10), "No 🍊 today");
Result grapefruitInventory = Result.FailureIf(() => afterBreakfast, new FruitInventory("grapefruit", 5), "No grapefruit 😢");
```

#### Implicit Conversion

Use case: Easily creating a successful result from a value

```csharp
Result 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

```csharp
Result appleInventory = new FruitInventory("apple", 4);
Result bananaInventory = Result.Failure("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

```csharp
string CreateMessage(FruitInventory inventory)
{
return $"There are {inventory.Count} {inventory.Name}(s)";
}

Result appleInventory = new FruitInventory("apple", 4);
Result bananaInventory = Result.Failure("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

```csharp
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](https://github.com/NitroDevs/CSharpFunctionalExtensions.FluentAssertions) more information!

Includes custom assertions for
- Maybe
- Result
- Result
- Result
- UnitResult

#### Example

```csharp
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](https://github.com/AlmarAubel/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](https://www.nuget.org/packages/CSharpFunctionalExtensions.Analyzers)
```bash
dotnet add package CSharpFunctionalExtensions.Analyzers
```

## Read or Watch more about these ideas

- [Functional C#: Primitive obsession](https://enterprisecraftsmanship.com/2015/03/07/functional-c-primitive-obsession/)
- [Functional C#: Non-nullable reference types](https://enterprisecraftsmanship.com/2015/03/13/functional-c-non-nullable-reference-types/)
- [Functional C#: Handling failures, input errors](https://enterprisecraftsmanship.com/2015/03/20/functional-c-handling-failures-input-errors/)
- [Applying Functional Principles in C# Pluralsight course](https://enterprisecraftsmanship.com/ps-func)

## Related Projects

- [Typescript Functional Extensions](https://github.com/seangwright/typescript-functional-extensions)
- [UniTask extensions for Unity](https://github.com/Razenpok/CSharpFunctionalExtensions.UniTask)

## Contributors

A big thanks to the project contributors!

- [Marcin Jahn](https://github.com/marcinjahn)
- [Jannes Kaspar-Müller](https://github.com/JKamue)
- [dbuckin1](https://github.com/dbuckin1)
- [bothzoli](https://github.com/bothzoli)
- [Pavel Zemlianikin](https://github.com/PNZeml)
- [Simon Lang](https://github.com/redx177)
- [Nils Vreman](https://github.com/NilsVreman)
- [Scheichsbeutel](https://github.com/Scheichsbeutel)
- [Alexey Malinin](https://github.com/TechnoBerry)
- [Robert Larkins](https://github.com/robertlarkins)
- [tinytownsoftware](https://github.com/tinytownsoftware)
- [piotr121993](https://github.com/piotr121993)
- [Dmitry Korotin](https://github.com/teheran)
- [michalsznajder](https://github.com/michalsznajder)
- [Xavier](https://github.com/xavierjohn)
- [Julien Aspirot](https://github.com/julienasp)
- [Kyle McMaster](https://github.com/KyleMcMaster)
- [Vinícius Beloni Cubas](https://github.com/vinibeloni)
- [rutkowskit](https://github.com/rutkowskit)
- [Giovanni Costagliola](https://github.com/MrBogomips)
- [Mark Wainwright](https://github.com/wainwrightmark)
- [ProphetLamb](https://github.com/ProphetLamb)
- [Paul Williams](https://github.com/Paul-Williams)
- [alexmurari](https://github.com/alexmurari)
- [ruud](https://github.com/ruudhe)
- [Tomasz Malinowski](https://github.com/Yaevh)
- [Staffan Wingren](https://github.com/staffanwingren)
- [Tim Schneider](https://github.com/DerStimmler)
- [Piotr Karasiński](https://github.com/Caleb9)
- [Marcel Roozekrans](https://github.com/MarcelRoozekrans)
- [guythetechie](https://github.com/guythetechie)
- [Logan Kahler](https://github.com/lqkahler)
- [Ali Khalili](https://github.com/AliKhalili)
- [Andrei Andreev](https://github.com/Razenpok)
- [YudApps](https://github.com/YudApps)
- [dataphysix](https://github.com/dataphysix)
- [Laszlo Lueck](https://github.com/LaszloLueck)
- [Sean G. Wright](https://github.com/seangwright)
- [Samuel Viesselman](https://github.com/SamuelViesselman)
- [Stian Kroknes](https://github.com/stiankroknes)
- [dataneo](https://github.com/dataneodev)
- [michaeldileo](https://github.com/michaeldileo)
- [Renato Ramos Nascimento](https://github.com/renato04)
- [Patrick Drechsler](https://github.com/draptik)
- [Vadim Mingazhev](https://github.com/mingazhev)
- [Darick Carpenter](https://github.com/darickc)
- [Stéphane Mitermite](https://github.com/kakone)
- [Markus Nißl](https://github.com/mnissl)
- [Adrian Frielinghaus](https://github.com/freever)
- [svroonland](https://github.com/svroonland)
- [JvSSD](https://github.com/JvSSD)
- [Vladimir Makaev](https://github.com/VladimirMakaev)
- [Ben Smith](https://github.com/benprime)
- [pedromtcosta](https://github.com/pedromtcosta)
- [Michał Bator](https://github.com/MikelThief)
- [mukmyash](https://github.com/mukmyash)
- [azm102](https://github.com/azm102)
- [ThomasDC](https://github.com/thomasdc)
- [bopazyn](https://github.com/bopazyn)
- [Joris Goovaerts](https://github.com/CommCody)
- [Ivan Deev](https://github.com/BillyFromAHill)
- [Damian Płaza](https://github.com/dpraimeyuu)
- [ergwun](https://github.com/ergwun)
- [Michael DiLeo](https://github.com/pilotMike)
- [Jean-Claude](https://github.com/jcsonder)
- [Matt Jenkins](https://github.com/space-alien)
- [Michael Altmann](https://github.com/altmann)
- [Steven Giesel](https://github.com/linkdotnet)
- [Anton Hryshchanka](https://github.com/ahryshchanka)
- [Mikhail Bashurov](https://github.com/saitonakamura)
- [kostekk88](https://github.com/kostekk88)
- [Carl Abrahams](https://github.com/CarlHA)
- [golavr](https://github.com/golavr)
- [Sviataslau Hankovich](https://github.com/hankovich)
- [Chad Gilbert](https://github.com/freakingawesome)
- [Robert Sęk](https://github.com/robosek)
- [Sergey Solomentsev](https://github.com/SergAtGitHub)
- [Malcolm J Harwood](https://github.com/mjharwood)
- [Dragan Stepanovic](https://github.com/dragan-stepanovic)
- [Ivan Novikov](https://github.com/jonny-novikov)
- [Denis Molokanov](https://github.com/dmolokanov)
- [Gerald Wiltse](https://github.com/solvingJ)
- [yakimovim](https://github.com/yakimovim)
- [Alex Erygin](https://github.com/alex-erygin)
- [Omar Aloraini](https://github.com/omaraloraini)