LeanSharp 1.1.4

There is a newer version of this package available.
See the version list below for details.
dotnet add package LeanSharp --version 1.1.4                
NuGet\Install-Package LeanSharp -Version 1.1.4                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="LeanSharp" Version="1.1.4" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add LeanSharp --version 1.1.4                
#r "nuget: LeanSharp, 1.1.4"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install LeanSharp as a Cake Addin
#addin nuget:?package=LeanSharp&version=1.1.4

// Install LeanSharp as a Cake Tool
#tool nuget:?package=LeanSharp&version=1.1.4                

LeanSharp

About

LeanSharp allows you to write code in a more declarative and functional way in C#.

By using LeanSharp, a lot of boilerplate and imperative code can be removed from your codebase. After looking at the Getting Started section, you can use the unit tests in the repository to help you understand how to use each Class/Method.

One of the things provided by this project is the potential to use Railway-Oriented Programming (ROP) in C#. The code related to ROP was initially taken from https://github.com/habaneroofdoom/AltNetRop and then based on real world usage it was grown and tweaked to its current state. In the same way, the Maybe monad used in here was taken from https://gist.github.com/johnazariah/d95c03e2c56579c11272a647bab4bc38, feel free to navigate there to see a good explanation about it.

Getting Started

Install LeanSharp Nuget Package:

Install-Package LeanSharp

Now make sure you add a namespace reference in the C# file you want to start using it:

using LeanSharp;

Examples:

Railway-Oriented Programming (ROP):

var success = Result<int, string>.Succeeded(2);
var newResult = success.Map(two => two + 3); // Success(5)
var finalResult = newResult.Bind(five => Result<int, string>.Succeeded(five + 5)); // Success(10)

Applying chaining:

Result<int, string>.Succeeded(2)
   .Map(two => two + 3)
   .Bind(five => Result<int, string>.Succeeded(five + 5))

ROP is a functional approach to handling errors. The Result class is more commonly known as the Either Monad, which could be either a Right (success) or a Left (error). This allows us to get rid of Exceptions side-effect:

public async Task<Result<Customer, Exception>> Insert(Customer customer)
{
  try
  {
      // Try to insert Customer into the DB.
      // ....
      return Result<Customer, Exception>.Succeeded(customer);
  }
  catch (Exception ex) 
  {
      return Result<Customer, Exception>.Failed(ex);
  }
}

You can take it even further and convert the try/catch statement to an expression, in which the boilerplate catch logic will be removed:

public async Task<Result<Customer, Exception>> Insert(Customer customer)
=> await Try.ExpressionAsync(async () =>
{
  // Try to asynchronously insert the Customer into the DB.
  // ....
  return customer;
});

After getting the Result, methods like Map and Bind can be chained, and the passed-in delegates will only be run if the Result contains a success. You can also use LINQ Query syntax, since Result contains the necessary methods to allow Monadic Composition:

var result = from s1 in Result<int, string>.Succeeded(1)
             from s2 in Result<int, string>.Succeeded(2)
             from s3 in Result<int, string>.Succeeded(3)
             select s1 + s2 + s3; // Success(6)

Avoid dealing with null values, the Maybe Monad:

public Maybe<Order> GetOrderById(int id)
{
    var order = GetOrderFromDB(id);
    return Maybe<Order>.Some(order);
}

Instead of having to deal with null when calling GetOrderById (or forget about it and end up with a NullReferenceException), you can perform safe operations on Maybe, they are safe because if the order was not found, the operations will not be performed, and Maybe encapsulates an centralizes the logic to do this. Also, with this signature, GetOrderById is honest, it is very explicit about the fact that the order might not be found, which is a very important piece of information.

// AssingOrderToCustomer returns a newly created Customer with the given order assgined to it.
// What if the order was not found? Leave that to the Maybe Monad.

var customerId = GetOrderById(5)
  .Map(order => AssingOrderToCustomer(customer, order));
  .GetOrElse(customer => customer.Id, 0);

Declarative Dispose (made into an expression):

var result = await Dispose.UsingAsync(() => new DisposableInstance, async disposableInstance =>
{
    // Add in here whatever you need in the body of the dipose.
    // ...
    return disposableInstance.DoTheWork();
});

In general, try to favor expressions over statements. Expressions are reusable, they can be returned out of a method or passed into one. Statements cannot do any of that. This is such a powerful technique that in C# 8 Microsoft added 'using' expressions to deal with disposable objects. And they did the same with 'switch' expressions.

Object mapping:

var greeting = "hello";
var message = greeting.MapTo(g => $"{g} world"); // "hello world"

Imperative code:

var customer = GetCustomerById(id);
var billModel = new BillModel 
{
    CustomerId = customer.Id,
    CustomerName = customer.FullName,
    StreetName = customer.StreetName
    ...
};

Declarative version:

var billModel = GetCustomerById(id)
                  .MapTo(c => new BillModel 
                  {
                      customerId = c.Id,
                      customerName = c.FullName,
                      StreetName = c.StreetName
                      ...
                  });

Pipeline (Composable pipelines):

Composing a pipeline with a method.

int AddFour(int number) => number + 4;

var pipeline = CreatePipeline.With(() => 5).Select(AddFour);
var task = pipeline.Flatten(); // Task(9)

Composing a pipeline with another pipeline:

var initialPipeline = CreatePipeline.With(() => 5);
var resultingPipeline = initialPipeline.SelectMany(five => CreatePipeline.With(() => five + 4));

var task = resultingPipeline.Flatten(); // Task(9)

Hello Monadic Composition again!

var firstPipeline = CreatePipeline.With(() => 5);
var secondPipeline = CreatePipeline.With(() => 6);
var thirdPipeline = CreatePipeline.With(() => 9);

var resultingPipeline = from firstValue in firstPipeline
                        from secondValue in secondPipeline
                        from thirdValue in thirdPipeline
                        select firstValue + secondValue + thirdValue;

var task = resultingPipeline.Flatten();  // Task(20)

To see a more real-world example using pipelines, you can check https://gist.github.com/ericrey85/da9671a22234ef981e5ee3653face4af.

SafePipeline(Composable exception-free pipelines)

After a year writing ROP almost everyday, the evident downside that I found was a lot of consecutive await(s), in order to await all the chained tasks. In order to save developers from this, Pipeline was created, but then if I wanted to do ROP and at the same time use Pipeline, I needed to deal with two Monads at the same time (Pipeline and Result). After seeing how many times I ended up with something like Pipeline<Result<TSuccess, TFailure>> to create a Pipeline that was exception-free, I decided to create SafePipeline. SafePipeline is also a Monad Transformer, it gives you a pipeline that encapsulates ROP for you, and knows how to act depending of the underlying value being a success or a failure.

async Task<Result<int, string>> GetFirstValue(int number) => await Result<int, string>.Succeeded(number + 4).AsTask();
async Task<Result<int, string>> GetSecondValue(int number) => await Result<int, string>.Succeeded(number + 5).AsTask();
async Task<int> GetThirdValue(int number) => await (number + 4).AsTask();
int GetFourthValue(int number) => number + 5;

SafePipeline<int, Exception> GetFithValue(int number) => CreateSafePipeline.TryWith(() => number + 6);

var firstPipe = CreateSafePipeline.With(() => GetFirstValue(5));
var secondPipe = firstPipe.Select(GetSecondValue);
var thirdPipe = secondPipe.Select(GetThirdValue);
var fourthPipe = secondPipe.Select(GetFourthValue);
var fithPipe = fourthPipe.SelectMany(value => GetFithValue(value).ToStringFailure());

var finalPipe = from firstValue in secondPipe
                from secondValue in thirdPipe
                from thirdValue in fithPipe
                select firstValue + secondValue + thirdValue;

var result = await finalPipe.Flatten(); // Success (57)

Operations that throw exceptions are safe to use now, and if an exception occurs, it is wrapped in a failed Result.

int InvalidDivideByZeroOperation(int number) => number / 0;
async Task<int> GetValue(int number) => await (number + 4).AsTask();

var firstPipe = CreateSafePipeline.TryWith(() => InvalidDivideByZeroOperation(5));
var finalPipe = firstPipe.Select(GetValue);

var result = await finalPipe.Flatten(); // Failure (exception)

One thing to notice is that SafePipeline is pure and deterministic, it does not matter how many times you call it, as long as you pass the same parameters, you will get the same response (huge benefit in software development), and exceptions will not spoil that.

There are a lot of things that you can do with LeanSharp, hopefully these examples give you an idea of how to get started.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.1.5 2,367 7/15/2022
1.1.4 425 7/14/2022
1.1.3 19,056 3/28/2021
1.1.1 2,689 10/20/2020
1.1.0 458 10/20/2020
1.0.11 504 10/5/2020
1.0.10 557 10/3/2020
1.0.9 475 10/3/2020
1.0.8 641 7/11/2020
1.0.7 577 7/9/2020
1.0.6 525 7/9/2020
1.0.5 576 6/20/2020
1.0.4 532 6/4/2020
1.0.3 497 6/3/2020
1.0.2 480 6/3/2020
1.0.1 469 6/3/2020
1.0.0 497 6/3/2020