Summary: in this tutorial, you’ll learn about the Chain of Responsibility Design Pattern and how to implement it in C#.
Introduction to the C# Chain of Responsibility Design Pattern
The chain of responsibility pattern is a behavioral pattern that allows you to pass a request through a chain of handlers until one of the handlers handles the request.
In the chain of handlers, each handler has a reference to the next handler so that it can pass the request to the next handler until one of the handlers can handle the request.
The chain of responsibility pattern solves the problem of determining which handler in a group of handlers should handle a particular request while avoiding tight coupling between the sender and receiver of the request.
Because of this, the chain of responsibility pattern achieves better separation of concerns, reduces code duplication, and increases the flexibility and scalability of your system.
The following UML diagram illustrates the chain of responsibility design pattern:
In this diagram:
Handler
– defines an interface for handling requests. TheHandler
has a link to its successor.ConcreteHandler
– is a class that implements theHandler
interface. TheConcreteHandler
handles the request it is responsible for. If theConcreteHandler
cannot handle the request, it forwards the request to its successor.Client
– initiates the request to aConcreteHandler
object on the chain.
The Client
passes a request to a chain of handlers until a ConcretteHandler
can handle it.
C# Chain of Responsibility Design Pattern Example
The following program illustrates the chain of responsibility pattern, which writes log messages to different targets based on the log message severity level.
namespace ChainOfResponsibilityPattern;
public enum LogLevel
{
Debug,
Info,
Warning,
Error
}
public interface ILogger
{
void Log(string message, LogLevel level);
ILogger? Next
{
get; set;
}
}
public class ConsoleLogger : ILogger
{
public ILogger? Next
{
get; set;
}
public void Log(string message, LogLevel level)
{
if (level == LogLevel.Debug || level == LogLevel.Info)
{
Console.WriteLine($"[Console Logger] {level}: {message}");
}
else
{
Next?.Log(message, level);
}
}
}
public class FileLogger : ILogger
{
public ILogger? Next
{
get; set;
}
public void Log(string message, LogLevel level)
{
if (level == LogLevel.Warning)
{
Console.WriteLine($"[File Logger] {level}: {message}");
}
else
{
Next?.Log(message, level);
}
}
}
public class EmailLogger : ILogger
{
public ILogger? Next
{
get; set;
}
public void Log(string message, LogLevel level)
{
if (level == LogLevel.Error)
{
Console.WriteLine($"[Email Logger] {level}: {message}");
}
else
{
Next?.Log(message, level);
}
}
}
public class Program
{
public static void Main(string[] args)
{
var consoleLogger = new ConsoleLogger();
var fileLogger = new FileLogger();
var emailLogger = new EmailLogger();
consoleLogger.Next = fileLogger;
fileLogger.Next = emailLogger;
consoleLogger.Log("This is a debug message", LogLevel.Debug);
consoleLogger.Log("This is an info message", LogLevel.Info);
consoleLogger.Log("This is a warning message", LogLevel.Warning);
consoleLogger.Log("This is an error message", LogLevel.Error);
}
}
Code language: C# (cs)
Output:
[Console Logger] Debug: This is a debug message
[Console Logger] Info: This is an info message
[File Logger] Warning: This is a warning message
[Email Logger] Error: This is an error message
Code language: C# (cs)
How it works.
First, define the LogLevel
enum that contains four log levels Debug
, Info
, Warning
, and Error
:
public enum LogLevel
{
Debug,
Info,
Warning,
Error
}
Code language: C# (cs)
Second, define the
interface that has a ILogger
Log()
method and a Next
property that serves as its successor in the chain. The
interface serves as the Handler in the pattern:ILogger
public interface ILogger
{
void Log(string message, LogLevel level);
ILogger? Next
{
get; set;
}
}
Code language: C# (cs)
Third, define the ConsoleLogger
class that implements the ILogger
interface. If the log level is debug or info, it handles the log message by displaying it to the console. Otherwise, it forwards the log message and log level to its successor:
public class ConsoleLogger : ILogger
{
public ILogger? Next
{
get; set;
}
public void Log(string message, LogLevel level)
{
if (level == LogLevel.Debug || level == LogLevel.Info)
{
Console.WriteLine($"[Console Logger] {level}: {message}");
}
else
{
Next?.Log(message, level);
}
}
}
Code language: C# (cs)
Fourth, define the FileLogger
class that implements the
interface. The ILogger
is similar to the ILogger
ConsoleLogger
except that it handles only the warning log message:
public class FileLogger : ILogger
{
public ILogger? Next
{
get; set;
}
public void Log(string message, LogLevel level)
{
if (level == LogLevel.Warning)
{
Console.WriteLine($"[File Logger] {level}: {message}");
}
else
{
Next?.Log(message, level);
}
}
}
Code language: C# (cs)
Fifth, define the
class that implements the EmailLogger
ILogger
interface. The
handles the error log message:EmailLogger
public class EmailLogger : ILogger
{
public ILogger? Next
{
get; set;
}
public void Log(string message, LogLevel level)
{
if (level == LogLevel.Error)
{
Console.WriteLine($"[Email Logger] {level}: {message}");
}
else
{
Next?.Log(message, level);
}
}
}
Code language: C# (cs)
Finally, create three logger objects including
, ConsoleLogger
, and FileLogger
EmailLogger
, and set the next handler for the
and ConsoleLogger
objects as FileLogger
and FileLogger
Email
Logger respectively. Use the
to log messages with different levels:ConsoleLogger
public class Program
{
public static void Main(string[] args)
{
var consoleLogger = new ConsoleLogger();
var fileLogger = new FileLogger();
var emailLogger = new EmailLogger();
consoleLogger.Next = fileLogger;
fileLogger.Next = emailLogger;
consoleLogger.Log("This is a debug message", LogLevel.Debug);
consoleLogger.Log("This is an info message", LogLevel.Info);
consoleLogger.Log("This is a warning message", LogLevel.Warning);
consoleLogger.Log("This is an error message", LogLevel.Error);
}
}
Code language: C# (cs)
Summary
- Use the chain of responsibility pattern to solve the problem of determining which handler should handle a request, while avoiding tight coupling between the sender and receiver of the request.