C# Chain of Responsibility Design Pattern

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:

C# Chain of Responsibility Design Pattern

In this diagram:

  • Handler – defines an interface for handling requests. The Handler has a link to its successor.
  • ConcreteHandler – is a class that implements the Handler interface. The ConcreteHandler handles the request it is responsible for. If the ConcreteHandler cannot handle the request, it forwards the request to its successor.
  • Client – initiates the request to a ConcreteHandler 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 messageCode 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 ILogger interface that has a Log() method and a Next property that serves as its successor in the chain. The ILogger interface serves as the Handler in the pattern:

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 ILogger interface. The ILogger is similar to the 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 EmailLogger class that implements the ILogger interface. The EmailLogger handles the error log message:

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, FileLogger, and EmailLogger, and set the next handler for the ConsoleLogger and FileLogger objects as FileLogger and Email Logger respectively. Use the ConsoleLogger to log messages with different levels:

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.
Was this tutorial helpful ?