SimpleChain 8.0.6

dotnet add package SimpleChain --version 8.0.6
NuGet\Install-Package SimpleChain -Version 8.0.6
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="SimpleChain" Version="8.0.6" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add SimpleChain --version 8.0.6
#r "nuget: SimpleChain, 8.0.6"
#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 SimpleChain as a Cake Addin
#addin nuget:?package=SimpleChain&version=8.0.6

// Install SimpleChain as a Cake Tool
#tool nuget:?package=SimpleChain&version=8.0.6

SimpleChain

NuGet version (SimpleChain) GitHub license

A set of extensions to implement in a simple way the Pipeline or Chain-of-responsibility patterns for all types of objects.

Stack used

  • .NET 8
  • xUnit
  • No dependencies

Installation

Depending on your usage, follow one of the guidelines below.

ASP.NET Core

Install with NuGet:

Install-Package SimpleChain

or with .NET CLI:

dotnet add package SimpleChain

How to Use

You can easily create a Chain from every object, Task, IEnumerable or IAsyncEnumerable. All those examples are listened in the project tests:

Consider the example class Sale

internal class Sale
{
    public string? SaleName { get; set; }
    public Product[] Products { get; set; } = Array.Empty<Product>();
    public double Total { get; set; }
    public double Tax { get; set; }
    public string? ApprovedBy { get; set; }
    public SaleStatus Status { get; set; } = SaleStatus.New;
}

internal sealed class Product
{
    public required string Name { get; set; }
    public required double Price { get; set; }
}

internal enum SaleStatus
{
    New,
    Closing,
    Closed
}

Chain-of-responsability pattern

You can easily create a chain of responsability like this:

Using the .AddHandlerNode node, you should return true or false. When it returns true it will ignore the next nodes.

var sale = new Sale();

await sale.ToChain()
    .AddNode(sale =>
    {
        sale.Total = 50;
        return sale;
    })
    .AddApprover1()
    .AddApprover2()
    .ThrowIfNotHandledNode();

    /// sale.ApprovedBy == "Approver 1"

Pipeline pattern

You can wrap functions and write like this with the following wrapping class:

var sale = new Sale();

await sale
    .Checkout()
    .AddTotal()
    .AddTax()
    .SetStatus(SaleStatus.Closed);
Any object
var sale = new Sale();

var chain = sale.ToChain()
    .AddNode(sale => 
    {
        sale.SaleName = "Sale1";
        sale.Products = new[] 
        {
            new Product
            {
                Name = "Product1",
                Price = "10"
            },
            new Product
            {
                Name = "Product2",
                Price = "15"
            }
        };
        return sale;
    })
    .AddNode(sale => sale.Products.Sum(x => x.Price));

var total = await chain();

// Total: 25
    
Task
var sale = _repository.GetSale() // returns Task<Sale> type
    .ToChain()
    .AddNode(sale =>
    {
        sale.Total = sale.Products.Sum(x => x.Price);
        return sale;
    })
    .AddNode(sale =>
    {
        sale.Tax = sale.Total * 0.12;
        return sale;
    });
IEnumerable
var sales = new List<Sale>();

var newSales = await sales.ToChain()
    .AddNode(sales =>
    {
        return sales.Select(sale =>
        {
            sale.Total = sale.Products.Sum(x => x.Price);
            return sale;
        });
    })
    .AddNode(sales =>
    {
        return sales.Select(sale =>
        {
            sale.Tax = sale.Total * 0.12;
            return sale;
        });
    });
IAsyncEnumerable
var asyncSales = await _repository.GetSalesAsync() // functions returns IAsyncEnumerable<Sale>
    .ToChain()
    .AddNode(async sales =>
    {
        await foreach(var sale in sales)
        {
            sale.Total = sale.Products.Sum(x => x.Price);
            yield return sale;
        }
    })
    .AddNode(async sales =>
    {
        await foreach(var sale in sales)
        {
            sale.Tax = sale.Total * 0.12;
            yield return sale;
        }
    });

await foreach(var sale in asyncSales.WithCancellationToken(ct))
{
    (...)
}
Or you can use .AddAsyncNode() directly:
var asyncSales = await _repository.GetSalesAsync() // functions returns IAsyncEnumerable<Sale>
    .ToChain()
    .AddAsyncNode(sale =>
    {
        sale.Total = sale.Products.Sum(x => x.Price);
        return sale;
    })
    .AddAsyncNode(sale =>
    {
        sale.Tax = sale.Total * 0.12;
        return sale;
    });

await foreach(var sale in asyncSales.WithCancellationToken(ct))
{
    (...)
}

Special Nodes

Chunk Node for IAsyncEnumerable

NOTE: You can specify the chunk size with .Chunk() node

var asyncSales = await _repository.GetSalesAsync() // functions returns IAsyncEnumerable<Sale>
    .ToChain()
    .Chunk(10)
    .AddAsyncNode(chunk =>
    {
        return chunk.Select(sale => 
        {
            sale.Total = sale.Products.Sum(x => x.Price);
            return sale;
        });
    })
    .AddAsyncNode(chunk =>
    {
        return chunk.Select(sale => 
        {
            sale.Tax = sale.Total * 0.12;
            return sale;
        });
    });

await foreach(var chunk in asyncSales.WithCancellationToken(ct))
{
    (...)
}

Wrapping Node functions

You can wrap functions and write like this with the following wrapping class:

var sale = new Sale();

await sale
    .Checkout()
    .AddTotal()
    .AddTax()
    .SetStatus(SaleStatus.Closed);
internal static class Wrappers
{
    public static Chain<Sale> Checkout(this Sale sale, CancellationToken cancellationToken = default)
    {
        return sale.ToChain(cancellationToken)
            .SetStatus(SaleStatus.Closing);
    }

    public static Chain<Sale> SetStatus(this Chain<Sale> chain, SaleStatus status)
    {
        return chain.AddNode(sale =>
        {
            sale.Status = status;
            return sale;
        });
    }

    public static Chain<Sale> AddTotal(this Chain<Sale> chain)
    {
        return chain.AddNode(sale =>
        {
            sale.Total = sale.Products.Sum(x => x.Price);
            return sale;
        });
    }

    public static Chain<Sale> AddTax(this Chain<Sale> chain)
    {
        return chain.AddNode(sale =>
        {
            sale.Tax = sale.Total * 0.12;
            return sale;
        });
    }
}
  public static Chain<Sale> AddApprover1(this Chain<Sale> chain)
  {
      return chain.AddHandlerNode(sale =>
      {
          if (sale.Total is > 0 and < 100)
          {
              sale.ApprovedBy = "Approver 1";
              return true;
          }
          return false;
      });
  }

  public static Chain<Sale> AddApprover2(this Chain<Sale> chain)
  {
      return chain.AddHandlerNode(sale =>
      {
          if (sale.Total is > 100 and <= 1000)
          {
              sale.ApprovedBy = "Approver 2";
              return true;
          }
          return false;
      });
  }
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.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
8.0.6 1,500 2/27/2024
8.0.5 736 2/5/2024
8.0.4 81 2/2/2024
8.0.3 178 2/1/2024
8.0.2 72 1/31/2024
8.0.1 82 1/31/2024