ChainStrategy 1.0.0

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

// Install ChainStrategy as a Cake Tool
#tool nuget:?package=ChainStrategy&version=1.0.0                

ChainStrategy

An implementation of the Chain of Responsibility and Strategy patterns for the dotnet platform.

TempIcon

build-status downloads downloads activity

Overview

The advantages of ChainStrategy are:

  • 📃 Ready to go with minimal boilerplate
  • ✔️ Easy unit testing
  • ⬇️ Built with dependency injection in mind
  • 🌱 Small footprint
  • 📚 Easy-to-learn API
  • 👛 Cancellation Token support

Table of Contents

Samples

If you would like code samples for ChainStrategy, they can be found here.

Dependencies

ChainStrategy has one dependency on a single Microsoft package that allows for integration into the universal dependency injection container.

Installation

The easiest way to get started is to: Install with NuGet.

Install where you need with:

Install-Package ChainStrategy

Setup

ChainStrategy provides a built-in method for easy Dependency Injection with any DI container that is Microsoft compatible.

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddChainStrategy(Assembly.GetExecutingAssembly());

        // Continue setup below
    }
}

The method also accepts params of Assemblies to register from if you need to add handlers and profiles from multiple assemblies.

builder.Services.AddChainStrategy(Assembly.Load("FirstProject"), Assembly.Load("SecondProject"));

Chain of Responsibility

Quick Start for Chain of Responsibility

Create a payload object that inherits from the ChainPayload base class.

public class MyChainPayload : ChainPayload
{
    public int InitialValue { get; set; }

    public int FinalValue { get; set; }
}

Your payload object should contain all data necessary for a chain, including initial, temporary, and final values.

Create handlers that inherit from the ChainHandler of T, where T is the type of your payload object.

Implement the DoWork method for each handler.

public class MyChainHandler : ChainHandler<MyChainPayload>
{
    public MyChainHandler(IChainHandler<MyChainPayload>? handler)
        : base(handler)
    {
    }

    public override Task<MyChainPayload> DoWork(MyChainPayload payload, CancellationToken cancellationToken)
    {
        payload.Value += 10;

        return Task.FromResult(payload);
    }
}

Create a profile for a chain that inherits from the ChainProfile of type T, where T is your payload object class. Add steps in the constructor.

These steps are executed in the order. Make sure you double check your order of operations.

public class MyProfile : ChainProfile<MyChainPayload>
{
    public MyProfile()
    {
        AddStep<MyChainHandler>()
        .AddStep<NextStep>()
        .AddStep<FinalStep>();
    }
}

Start a chain by injecting an IChainFactory into a service. Call the Execute method, and pass a payload object.

public class IMyService
{
    private readonly IChainFactory _chainFactory;

    public IMyService(IChainFactory chainFactory)
    {
        _chainFactory = chainFactory;
    }

    public async Task Handle()
    {
        var result = await _chainFactory.Execute(new MyPayload());
    }
}

Detailed Usage for Chain of Responsibility

Custom Payload Objects

You may create a custom implementation of the IChainPayload interface if you desire. It has only one property that must be implemented. This property is checked by each handler before it executes. If the value is true, the chain is aborted and returned to the caller.

public interface IChainPayload
{
    bool IsFaulted { get; }
}

The base ChainPayload class has two virtual methods that may be overridden. The most common use case would be if you wanted to enrich faults with more metadata.

public abstract class MyCustomPayload : ChainPayload
{
    public DateTime FaultedAt { get; private set; }

    public override void Faulted(Exception exception)
    {
        FaultedAt = DateTime.UtcNow;
        base.Faulted(exception);
    }
}

All of your payloads would now inherit from your new base payload class.

Accepting Dependencies

ChainStrategy is built for dependency injection. You may inject any dependency you need into the constructor.

public class MyChainHandler : ChainHandler<MyChainPayload>
{
    private readonly IMyDataSource _data;

    public MyChainHandler(IChainHandler<MyChainPayload>? handler, IMyDataSource data)
        : base(handler)
    {
        _data = data;
    }

    public override async Task<MyChainPayload> DoWork(MyChainPayload payload, CancellationToken cancellationToken)
    {
        var myData = await _data.GetData();

        payload.Value = myData;

        return payload;
    }
}

You may start another chain or strategy from inside a chain handler. Inject the appropriate factory and execute a payload or request.

public class MyChainHandler : ChainHandler<MyChainPayload>
{
    private readonly IStrategyFactory _strategyFactory;

    public MyChainHandler(IChainHandler<MyChainPayload>? handler, IStrategyFactory strategyFactory)
        : base(handler)
    {
        _strategyFactory = strategyFactory;
    }

    public override async Task<MyChainPayload> DoWork(MyChainPayload payload, CancellationToken cancellationToken)
    {
        var strategyResult = await _strategyFactory.Execute(new StrategyRequest(payload));

        payload.Value = strategyResult;

        return payload;
    }
}
Aborting A Chain

There may be conditions where your chain faults or must return early. There is a built-in way of returning a payload to the originator to avoid finishing the entire chain.

public class MyChainHandler : ChainHandler<MyChainPayload>
{
    private readonly IMyDataSource _data;

    public MyChainHandler(IChainHandler<MyChainPayload>? handler, IMyDataSource data)
        : base(handler)
    {
        _data = data;
    }

    public override async Task<MyChainPayload> DoWork(MyChainPayload payload, CancellationToken cancellationToken)
    {
        try
        {
            var myData = await _data.GetData();

            payload.Value = myData;
        }
        catch
        {
            payload.Faulted();
        }

        return payload;
    }
}

You may also pass an exception to the Faulted method if you'd like to log the object.

    catch (Exception exception)
    {
        payload.Faulted(exception);
    }

Do not throw exceptions from inside handlers. Catch, handle gracefully, and return a faulted payload object.

Using A Base Handler

If you happen to be repeating yourself in multiple handlers, you may create a base handler to accomplish common tasks.

The example below shows an abstract handler that will override the Middleware method. Middleware just calls DoWork under the hood.

public abstract class SampleTryCatchHandler<T> : ChainHandler<T>
    where T : ChainPayload
{
    protected SampleTryCatchHandler(IChainHandler<T>? handler)
        : base(handler)
    {
    }

    public override async Task<T> Middleware(T payload, CancellationToken cancellationToken)
    {
        try
        {
            return await base.Middleware(payload, cancellationToken);
        }
        catch (Exception exception)
        {
            payload.Faulted(exception);

            return payload;
        }
    }
}

Your handlers that need to use this can simply inherit from this class instead.

public class MyChainHandler : SampleTryCatchHandler<MyChainPayload>
{
    public MyChainHandler(IChainHandler<MyChainPayload>? handler)
        : base(handler)
        {
        }

    public override async Task<MyChainPayload> DoWork(MyChainPayload payload, CancellationToken cancellationToken)
    {
        // implement and return payload.
    }
}

When adding steps in your profile, make sure you are using the correct handler. Accidentally adding an abstract base handler will throw an exception.

Handler Constraints

You may reuse a handler in multiple chains by constraining the payload type via an interface.

The interface needs to inherit from the "IChainPayload" interface, even if you rely on the default implementation.

public interface IData : IChainPayload
{
    Guid Id { get; }

    void UpdateData(MyData data);
}
public class MyChainPayload : ChainPayload, IData
{
    // implement properties and methods
}

Add the constraint handler and implement the interface accordingly.

Constrained handlers need to be abstract classes which utilize the generic constraint.

public abstract class MyConstrainedHandler<T> : ChainHandler<T>
    where T : IData
{
    protected MyConstrainedHandler(IChainHandler<T>? successor)
        : base(successor)
        {
        }

    public override Task<T> DoWork(T payload, CancellationToken cancellationToken)
    {
        if (payload.id == Guid.Empty)
        {
            payload.UpdateId(id);
        }

        return Task.FromResult(payload);
    }
}

Your concrete handler only needs to derive from the constrained base.

public class MyHandler : MyConstrainedHandler<MyChainPayload>
{
    public MyHandler(IChainHandler<MyPayload>? handler)
        : base(handler)
        {
        }
}
Testing

Testing a chain handler is no different than unit testing any other class or method.

[TestClass]
public class MyHandlerTests
{
    [TestMethod]
    public async Task MyHandleWorks()
    {
        var handler = new MyHandler(null);

        var result = await handler.Handle(new MyPayload(), CancellationToken.None);

        Assert.AreEqual(expected, result);
    }

    [TestMethod]
    public async Task WithDependency()
    {
        var mock = new Mock<IMyDependency>();
        mock.Setup(x => x.MyMethod()).ReturnsAsync(new MyExpectedReturn());

        var handler = new MyHandler(null, mock.Object);

        var result = await handler.Handle(new MyPayload(), CancellationToken.None);

        Assert.AreEqual(expected, result);
    }

    [TestMethod]
    public async Task ServiceTestForFactory()
    {
        var mock = new Mock<IChainFactory<MyPayload>>();
        mock.Setup(x => x.Execute(It.IsAny<MyPayload>(), CancellationToken.None))
            .ReturnsAsync(new MyPayload());

        var service = new MyService(mock.Object);

        var serviceResult = await service.DoSomething();

        Assert.AreEqual(expected, serviceResult);
    }
}

Strategy

Quick Start for Strategy

Unlike chains, strategies use both a request and response object.

public class MyResponse
{
    public int MyResult { get; set; }
}

Request objects will implement the IStrategyRequest interface of type T, where T is your response type.

public class MyRequest : IStrategyRequest<MyResponse>
{
    // properties in here
}

If your strategy has no return type, use the non-generic version of the IStrategyRequest interface.

public class MyRequest : IStrategyRequest
{
}

Implement a handler by inheriting from the IStrategyHandler interface. Pass the request and response types as the generic parameters.

Implement the Handle method as required.

public class MyStrategyHandler : IStrategyHandler<MyRequest, MyResponse>
{
    public async Task<MyResponse> Handle(MyRequest request, CancellationToken cancellationToken)
    {
        // implement and return response
    }
}

If your request object does not have a return type, the Nothing class will be used instead. Nothing, as the name states, is a substitute for void.

public class MyStrategyHandler : IStrategyHandler<MyRequest>
{
    public async Task<Nothing> Handle(MyRequest request, CancellationToken cancellationToken)
    {
        // implement and return response
    }
}

All Strategy handlers must have a public or default constructor to be initialized properly.

Profiles are very similar to chains except you are defining conditions instead of steps.

You define a strategy by giving it a predicate based on your request object properties.

Note: These are executed in order, put your most constrained definitions first.

public class MyStrategyProfile : StrategyProfile<MyRequest, MyResponse>
{
    public MyStrategyProfile()
    {
        AddStrategy<MySecondHandler>(request => request.Value == 0);
        AddStrategy<MyFirstHandler>(request => request.Value > 10);
    }
}

Strategies follow the same pattern as chains; in other words, inject the factory into the class you want to use. Call the Execute method when required.

public class MyService
{
    private readonly IStrategyFactory _strategyFactory;

    public MyService(IStrategyFactory strategyFactory)
    {
        _strategyFactory = strategyFactory;
    }

    public async Task Handle()
    {
        var result = await _strategyFactory.Execute(new MyRequest());
    }
}

Detailed Usage for Strategies

Default Profiles

A profile may have a Default handler if no condition is satisfied.

public class MyStrategyProfile : StrategyProfile<MyRequest, MyResponse>
{
    public MyStrategyProfile()
    {
        AddStrategy<MySecondHandler>(request => request.Value == 0);
        AddStrategy<MyFirstHandler>(request => request.Value > 10);
        AddDefault<MyFirstHandler>();
    }
}

You may only have one default handler. In fact, calling the method twice will just overwrite the previous one.

Accepting Strategy Dependencies

You may use dependency injection for any other dependencies like normal.

public class MyStrategyHandler : IStrategyHandler<MyRequest, MyResponse>
{
    private readonly IMyDependency _dependency;

    public MyStrategyHandler(IMyDependency dependency)
    {
        _dependency = dependency;
    }

    public async Task<MyResponse> Handle(MyRequest request, CancellationToken cancellationToken)
    {
        // implement and return response
    }
}

Similar to chains, you may start another chain or strategy inside of an existing handler.

public class MyStrategyHandler : IStrategyHandler<MyRequest, MyResponse>
{
    private readonly IChainFactory _chainFactory;

    public MyStrategyHandler(IChainFactory chainFactory)
    {
        _chainFactory = chainFactory;
    }

    public async Task<MyResponse> Handle(MyRequest request, CancellationToken cancellationToken)
    {
        var chainResult = await _chainFactory.Execute(new MyChainPayload(request));

        return new MyResponse(chainResult);
    }
}
Base Strategy Handlers

Similar to chains, you may have a base handler to share common logic. This example shows wrapping logic in a try-catch.

public abstract class SampleStrategyLoggingHandler<TRequest, TResponse> : IStrategyHandler<TRequest, TResponse>
    where TRequest : IStrategyRequest<TResponse>
    where TResponse : new()
{
    private readonly ILogger _logger;

    protected SampleStrategyLoggingHandler(ILogger logger)
    {
        _logger = logger;
    }

    public virtual async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
    {
        try
        {
            return await DoWork(request, cancellationToken);
        }
        catch (Exception exception)
        {
            _logger.Error(exception, $"An exception occurred at {DateTime.UtcNow} in the {GetType().Name} handler.");
        }

        return new TResponse();
    }

    public abstract Task<TResponse> DoWork(TRequest request, CancellationToken cancellationToken);
}

Any handler would inherit from this and implement the DoWork function instead of Handle.

Testing Strategy Handlers

Testing strategy handlers is straightforward.

[TestClass]
public class StrategyTests
{
    [TestMethod]
    public async Task Strategy_IsCorrect()
    {
        var strategy = new MyStrategyHandler();

        var result = await strategy.Execute(new MyStrategyRequest());

        Assert.Equal(expected, result);
    }

    [TestMethod]
    public async Task Strategy_WithDependency_IsCorrect()
    {
        var mock = new Mock<IDependency>();
        mock.Setup(x => x.Something()).ReturnsAsync(expectedObject);

        var strategy = new MyStrategyHandler(mock.Object);

        var result = await strategy.Execute(new MyStrategyRequest());

        Assert.Equal(expected, result);
    }

    [TestMethod]
    public async Task MockingFactory_FromService_IsCorrect()
    {
        var mock = new Mock<IStrategyFactory>();
        mock.Setup(x => x.Execute(It.IsAny<MyStrategyRequest>, CancellationToken.None))
            .ReturnsAsync(new MyStrategyResponse());

        var service = new ServiceWithFactory(mock.Object);

        var result = await service.HandleRequest(new MyRequest());

        Assert.Equal(expected, result);
    }
}

FAQ

Do I need a Chain of Responsibility?

Do you have a complex process that can be broken up into multiple steps to enable easier development and testing?

Do I need a Strategy?

Do you have a common input/output interface that may use different implementations depending on a condition?

It is best to think of a Strategy as a complex switch statement, where each switch case may be a long-lived, complex process.

(A common example is having to process credit cards with different payment providers.)

How is either different from a Mediator?

A Mediator is a one-to-one relationship between a request and a response with a single handler per request.

A Chain of Responsibility is a one-to-many relationship with multiple handlers per request in a specific order.

A Strategy is a one-to-many relationship with a single handler chosen depending on a predicate.

Can I use them together?

Yes! You can use any combination of the three. They are not mutually exclusive.

How often can I use a Chain of Responsibility or Strategy?

A Chain of Responsibility is a medium usage pattern. It is best used when you need to break a problem down into smaller, easier-to-test chunks.

A Strategy is a low usage pattern. It is best used when you need to have multiple implementations of an algorithm that uses the same interface.

Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 is compatible.  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 is compatible.  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 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. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen 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.

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.0.0 136 4/6/2024
0.9.1 195 1/7/2024
0.9.0 138 12/30/2023