Design Pattern
.NET .NET Core C# Patterns Visual Studio

Using Design Patterns in .NET Core – Part 8 – Chain of Responsibility Pattern

Welcome to today’s post.

In today’s post I will be discussing another software design pattern, which is the Chain of Responsibility Pattern, which is one of the 24 Gang of Four software design patterns. In real life we draw the analogy for a chain of responsibility with an athletics baton relay, where the runners (the handlers) pass off a baton (the data) until it gets to the last person in the chain, who then proceeds to dash to the finishing line.

The chain of responsibility pattern delegates a parts of an overall process, which can include validations, executions, or conditions and run each delegated process until the end. If any of the delegated handlers results in an error, then the entire chain results in an error. If the entire chain runs without errors, then the responsibility chain is successful.

In the first section, I will show a typical example of how the chain of responsibility is applied in web requests within .NET Core middleware. I will then show how to implement a useful chain of responsibility to that validates data between delegated handlers.

Chain of Responsibility Patterns in Web Requests

The chain of responsibility pattern is you have used .NET Core for quite a while occurs in some common areas of the application pipeline. One of those areas is the construction of .NET Core middleware extensions, where a request delegate handler is used to continue a chain of requests from one request to the next. An example of such an extension is shown below:

public class CustomMiddleware
{
    private readonly RequestDelegate _next;

    public CustomMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext, IMyService svc)
    {
        svc.Prop1 = 1000;
        await _next(httpContext);
    }
}

A delegate handler method is a C# reference type which allows methods to be passed as parameters. A request delegate is a specific type of delegate handler which takes as parameter a method with a HttpContext parameter. The function prototype is shown below:

public delegate System.Threading.Tasks.Task RequestDelegate(HttpContext context);

What a middleware extension does is to continue a request to the next delegate handler. This effectively decouples each handler from the client class and allows multiple handlers of different types to be added to the chain.

With a request delegate handler, the data that is chained across to the next handler is of the same class type. In the chained responsibility pattern, we can use any number of class types for our data within the responsibility chain.

In an application, one typical use for the chain of responsibility would be to apply a series of validations on some data prior to modifying a data source.

Where we have an API method that saves a new record to a table, we could make use of a chain of validation handlers, where the parameters are dependent on the data to be saved. If one of the handlers throws an exception, then the saving of the new record is aborted, and we receive an error response to the client.

An Example of Using Chain of Responsibility for Validations

We can define a series of validations that are extended from a loan validation interface. The validations can then be used within a chain of validations, where we specify the next validation for each validation object. This continues until all validations are completed in the chain. If any validation throws an exception, the validation fails, else the validation succeeds if all validations in the chain succeed.

A diagram showing what we are aiming to achieve is shown below:

First, we define an interface that has two key methods, the first method sets the next validation request in the responsibility chain:

public void SetNextValidation(ILoanValidation validation);

The next method implements the validation for the handler, throwing an exception when the validation fails to be satisfied:

public Task ValidateLoan(UserLoanData data);

The interface is shown below:

using System.Threading.Tasks;

namespace BookLoan.Loan.API.ChainOfResponsibility
{
    public interface ILoanValidation
    {
        public void SetNextValidation(ILoanValidation validation);

        public Task ValidateLoan(UserLoanData data);
    }
}

In the next section, I will show how we can define a class that defines the data that we will pass between each handler within the responsibility chain. The data class itself will be used as the source of the validation conditions for each handler in the responsibility chain.

Defining Data Passed Between Chain of Responsibility Interfaces

We also define an encapsulated class for the data that is being passed between the handlers:

using BookLoan.Models;
using BookLoan.Services;
using BookLoan.Loan.API.Repository;

namespace BookLoan.Loan.API.ChainOfResponsibility
{
    public class UserLoanData
    {
        private readonly IUserContextService _userContextService;
        private readonly ILoanRepository _loanRepository;
        private readonly IBookRepository _bookRepository;

        public UserLoanData(IUserContextService userContextService,
            IBookRepository bookRepository,
            ILoanRepository loanRepository)
        {
            _userContextService = userContextService;
            _loanRepository = loanRepository;
            _bookRepository = bookRepository;
        }

        public LoanViewModel UserLoan { get; private set; }
        public LoanViewModel LoanData { get { return UserLoan; } set { UserLoan = value; } }
        public IUserContextService  UserContext { get { return _userContextService; } }
        public ILoanRepository LoanRepository { get { return _loanRepository; } }
        public IBookRepository BookRepository { get { return _bookRepository; } 
    }
}

This data essentially contains a few key components of data:

  1. A user context UserContext, containing the username and application roles.
  2. A loan repository LoanRepository, that is used to retrieve current and past loan data for library users.
  3. A book repository BookRepository, used to retrieve current catalogue of library media that are available for loan.
  4. A record LoanData, containing the loan details.

In the next section, I will show how to extend the validation handler interfaces.

Extending Validation Handlers

In this section, I will show how we extend the chain of responsibility validation interfaces. We will extend the handlers that will include chained validations. All handlers are implemented as chained, except the last handler, which will be evaluated without being chained.

These handlers are:

ValidateUserRoles – Validates the user has the requisite roles to create a new loan record. The user context is used to determine these roles. The implementation is shown below:

using System.Threading.Tasks;
using BookLoan.Helpers;

namespace BookLoan.Loan.API.ChainOfResponsibility
{
    public class ValidateUserRoles: ILoanValidation
    {
        private ILoanValidation _nextValidation;

        public void SetNextValidation(ILoanValidation validation)
        {
            _nextValidation = validation;
        }

        public async Task ValidateLoan(UserLoanData data)
        {
            bool isMember = data.UserContext.IsMemberUser();
            if (isMember)
            {
                await _nextValidation.ValidateLoan(data);
            }
            else
            {
                throw new AppException("The current user cannot be validated for a loan.");
            }
        }
    }
}

ValidatePastLoans – Validates the past loans for the current user from the Loan Repository, ensuring that the user does not have an active loan that is overdue. The implementation is shown below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BookLoan.Helpers;
using BookLoan.Models;

namespace BookLoan.Loan.API.ChainOfResponsibility
{
    public class ValidatePastLoans: ILoanValidation
    {
        private ILoanValidation _nextValidation;

        public void SetNextValidation(ILoanValidation validation)
        {
            _nextValidation = validation;
        }

        public async Task ValidateLoan(UserLoanData data)
        {            
            List<LoanViewModel> loans = await data.LoanRepository.GetByLoanedUser(
                    data.UserContext.GetUserDetails().userName);
            if (!loans.Any(l => l.BookID == data.LoanData.BookID && 
                           l.DateReturn < l.DateLoaned &&   
                           l.DateDue < DateTime.Now))
            {
                await _nextValidation.ValidateLoan(data);
            }
            else
            {
                throw new AppException("The current user has a previous overdue loan.");
            }
        }
    }
}

ValidateBookAvailability – Validates the Book ID from the requested loan data exists within the Book Repository. This handler is the last validation on the chain, with a successful validation terminating the chain and an unsuccessful validation throws an exception. The implementation is shown below:

using System.Threading.Tasks;
using BookLoan.Models;
using BookLoan.Helpers;

namespace BookLoan.Loan.API.ChainOfResponsibility
{
    public class ValidateBookAvailability : ILoanValidation
    {
        private ILoanValidation _nextValidation;

        public void SetNextValidation(ILoanValidation validation)
        {
            _nextValidation = validation;
        }

        public async Task ValidateLoan(UserLoanData data)
        {
            BookViewModel book = await data.BookRepository.GetByIdAsync(data.UserLoan.BookID);
            if (book == null)
            {
                throw new AppException("The book to be loaned has an invalid book id.");
            }
        }
    }
}

In the next section, I will show how to apply the above handlers to perform a chained validation.

Applying the Chain of Validations

To tie all the above validation handler classes together to apply the chain of responsibility pattern within our save loan method we first create the handlers: 

ValidatePastLoans validatePastLoans = new ValidatePastLoans();
ValidateUserRoles validateUserRoles = new ValidateUserRoles();
ValidateBookAvailability validateBookAvailability = new ValidateBookAvailability();

We next set the successors for each of the validation handlers:

validatePastLoans.SetNextValidation(validateUserRoles);
validateUserRoles.SetNextValidation(validateBookAvailability);

The ValidateBookAvailability handler does not need a validation successor as it is the last validation in the responsibility chain.

We then set the loan data that will be passed between each handler:

_userLoanData.LoanData = vm;

Finally, we execute the validation to assess whether the loan will be saved:

await validatePastLoans.ValidateLoan(_userLoanData);

If any validations throw an exception, then the subsequent save operation shown below will be aborted:

vm.DateCreated = DateTime.Now;
vm.DateUpdated = DateTime.Now;
_db.Add(vm);
await _db.SaveChangesAsync();

What we have seen is a quite a useful way in which we can apply the chain of responsibility pattern in a real-world scenario. This gives us a pattern that can be re-used where we wished to utilize multiple validations prior to saving data to a data store and ensure security and data integrity within the application.

By chaining the validations, we have distributed the validation logic for a transaction across multiple interfaces and extended classes. This satisfies the single responsibility principle.   

The pattern satisfies the SOLID principle of decoupling, interface segregation and so permits unit testing.

That is all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial