SimpleChain 9.0.0

dotnet add package SimpleChain --version 9.0.0
                    
NuGet\Install-Package SimpleChain -Version 9.0.0
                    
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="9.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SimpleChain" Version="9.0.0" />
                    
Directory.Packages.props
<PackageReference Include="SimpleChain" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SimpleChain --version 9.0.0
                    
#r "nuget: SimpleChain, 9.0.0"
                    
#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.
#addin nuget:?package=SimpleChain&version=9.0.0
                    
Install as a Cake Addin
#tool nuget:?package=SimpleChain&version=9.0.0
                    
Install as a Cake Tool

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 9
  • 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-responsibility pattern

You can easily create a chain of responsibility like this:

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

NOTE: All extension functions used here are detailed at the end of this README

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:

NOTE: All extension functions used here are detailed at the end of this README

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))
{
    (...)
}
Pipeline circuit breaker

You can interrupt the pipeline by calling the Cancel() method from ChainState. That will throw an exception OperationCanceledException by cancelling the local CancellationToken from the Chain.

    var sale = new Sale();

    var result = async () => await sale.ToChain()
        .AddNode((sale, _, state) =>
        {
            sale.Total = sale.Products.Sum(x => x.Price);
            state.Cancel();
            return sale;
        })
        .AddNode(sale =>
        {
            sale.Tax = sale.Total * 0.12;
        });

When calling the state.Cancel(); method the chain execution will be cancelled and an exception of type OperationCanceledException will be thrown.

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 net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net9.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
9.0.0 124 7/1/2025
8.0.6 4,917 2/27/2024
8.0.5 802 2/5/2024
8.0.4 138 2/2/2024
8.0.3 236 2/1/2024
8.0.2 132 1/31/2024
8.0.1 136 1/31/2024