The article cooperates with the sample project. Walkthrough with the downloaded project is not required, but I recommend it for better understanding.
The sample project consists of two different approaches to validating the user's data within a registration. Two processors simulate the user's registration process.
BasicUserRegistrationProcessor.cs follows the simple path of if statements.
public class BasicUserRegistrationProcessor
{
public void Register(User user)
{
if (string.IsNullOrWhiteSpace(user.Username))
{
throw new Exception("Username is required.");
}
if (string.IsNullOrWhiteSpace(user.Password))
{
throw new Exception("Password is required.");
}
if (user.BirthDate.Year > DateTime.Now.Year - 18)
{
throw new Exception("Age under 18 is not allowed.");
}
}
}
ChainPatternRegistrationProcessor.cs is taking advantage of the Chain of Responsibility Pattern. This implementation applies the same set of validations.
public class ChainPatternRegistrationProcessor
{
public void Register(User user)
{
var handler = new UsernameRequiredValidationHandler();
handler.SetNext(new PasswordRequiredValidationHandler())
.SetNext(new OnlyAdultValidationHandler());
handler.Handle(user);
}
}
Chain of Responsibility Pattern
Chain of Responsibility Pattern(ChoRP) is a pattern firstly presented in great book Design Patterns: Elements of Reusable Object-Oriented Software.
We can achieve loose coupling in software design by practicing ChoRP. The pattern is a very powerful yet pretty simple to implement in our applications. It allows us to easily decouple parts of code to make it more readable, testable, extensible, and maintainable.
ChoRP comes from a family of the behavioral pattern, also like State Pattern.
How to manage states with State Design Pattern in C#?
Components of ChoRP
ChoRP consists of three components.
- Request
- Abstraction of Handler
- Concrete Handlers
One or more Concrete Handlers will take care of Request in the chained process. Mostly, the Request is some kind of contract or Data Transfer Object of the handling event. In the sample project, it is a User class.
public class User
{
public string Username { get; set; }
public string Password { get; set; }
public DateTime BirthDate { get; set; }
}
I am usually using the interface and the abstract class together as the abstraction for the Concrete Handlers. It contains two methods; Handle and SetNext.
public abstract class Handler<T> : IHandler<T> where T : class
{
private IHandler<T> Next { get; set; }
public virtual void Handle(T request)
{
Next?.Handle(request);
}
public IHandler<T> SetNext(IHandler<T> next)
{
Next = next;
return Next;
}
}
public interface IHandler<T> where T : class
{
IHandler<T> SetNext(IHandler<T> next);
void Handle(T request);
}
T is a generic type of class which represents the Request. Method SetNext() sets a private property Next , which is subsequently invoked in the virtual method Handle(T request) .
Concrete Handler inherits from the abstract class Handler and implements an interface IHandler.
public class OnlyAdultValidationHandler : Handler<User>, IHandler<User>
{
public override void Handle(User user)
{
if (user.BirthDate.Year > DateTime.Now.Year - 18)
{
throw new Exception("Age under 18 is not allowed.");
}
base.Handle(user);
}
}
OnlyAdultValidationHandler class overrides Handle method of its parent but also invokes the parent's Handle the method at the end of the method's body. This is a crucial factor for chaining Concrete Handlers. Also, by inheriting from the Handler class, the OnlyAdultValidationHandler gains SetNext method threw which we will set another chain segment.
Chaining Handlers
First, you need to set up the chain via the SetNext method. The method returns IHandler type so you can set Handlers in a row.
SetNext(new Handler()).SetNext(new Handler())
Once we finish the chain definition, we can continue by invoking Handler.Handle(request) which will call base.Handle(user) therefore Next?.Handle(request) ensures that the chain reaction is triggered.
Drawbacks of Coupled Implementations
Why so much workaround? The basic implementation looks much more straightforward and readable than ChoRP implementation, right?
Well, it is not clear at first look, but those validations are tightly coupled. Password Validation is dependent on the result of Username Validation. Only Adult Age Validation is dependent on the result of previous validations.
It is even more evident if you write unit tests that are going to test different scenarios of user registration. You are not able to test Password Validation without introducing value into the user's Username property.
public class BasicUserRegistrationTests
{
[Fact]
public void When_Username_Is_Empty_Then_Exception
_Should_Be_Thrown()
{
//Arrange
var user = new User();
//Act
Action act = () => new BasicUserRegistrationProcessor()
.Register(user);
//Assert
act.Should().Throw<Exception>();
}
[Fact]
public void When_Password_Is_Empty_Then_Exception
_Should_Be_Thrown()
{
//Arrange
var user = new User
{
Username = "Daniel Rusnok"
};
//Act
Action act = () => new BasicUserRegistrationProcessor()
.Register(user);
//Assert
act.Should().Throw<Exception>();
}
But once you apply a ChoRP, then you are not only able to test Password Validation result, but also you are able to test both possible results of Password Validation which are Throw<Exception> when the password property is empty and NotThrow<Exception> when the password property is filled.
public class ChainPatternRegistrationTests
{
[Fact]
public void When_Password_Is_Empty_Then_Exception
_Should_Be_Thrown()
{
//Arrange
var user = new User{Password = string.Empty};
//Act
Action act = () => new PasswordRequiredValidationHandler()
.Handle(user);
//Assert
act.Should().Throw<Exception>();
}
[Fact]
public void When_Password_Is_Filled_Then_Exception
_Should_NOT_Be_Thrown()
{
//Arrange
var user = new User{Password = Guid.NewGuid().ToString()};
//Act
Action act = () => new PasswordRequiredValidationHandler()
.Handle(user);
//Assert
act.Should().NotThrow<Exception>();
}
With BasicUserRegistrationProcessor we are only able to test if Password Validation NotThrow<Exception> when the whole user object is valid. And this is the biggest demonstration of how basic registration implementation is coupled.
[Fact]
public void When_All_Properties_Are_valid_Then_Exception_Should_NOT_Be_Thrown()
{
//Arrange
var user = new User
{
Username = "Daniel Rusnok",
Password = Guid.NewGuid().ToString(),
BirthDate = DateTime.Now.AddYears(-20)
};
//Act
Action act = () => new BasicUserRegistrationProcessor()
.Register(user);
//Assert
act.Should().NotThrow<Exception>();
}
Summary
The power of decoupling is not only in its independent testability but also in its reusability. Such a Handler does not always have to accept a concrete type as a parameter. It can take an abstraction of the specific type like an interface.
Multiple types of Requests can implement such an interface. In such situations, the Handler becomes useable for various processes, and you are not repeating yourself. So ChoRP also supports the DRY.
Related Reading
- How to Dynamically Add Behaviors With Decorator Pattern
- Ugly dependency graph? The Mediator Design Pattern is the solution for you.
- How to manage states with State Design Pattern in C#?
- How I Upgrade my Code-Style of MediatR Pipeline using .NET 6



Top comments (0)