Design – Behavioral Patterns-2

Each handler does two things:

  • It receives an optional “next handler” from its constructor (highlighted in the code). This creates a chain similar to a singly linked list.
  • It handles only the request it knows about, delegating the others to the next handler in the chain.

Let’s use Program.cs as the consumer of the Chain of Responsibility (the Client) and use a POST requests to interface with our REST API and build the message.Here is the first part of our REST API:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IMessageHandler>(
    new AlarmTriggeredHandler(
        new AlarmPausedHandler(
            new AlarmStoppedHandler())));

In the preceding code, we manually create the Chain of Responsibility and register it as a singleton bound to the IMessageHandler interface. In that registration code, each handler is manually injected in the previous constructor (created with the new keyword).The next code represents the second half of the Program.cs file:

var app = builder.Build();
app.MapPost(
    “/handle”,
    (Message message, IMessageHandler messageHandler) =>
    {
        messageHandler.Handle(message);
        return $”Message ‘{message.Name}’ handled successfully.”;
    });
app.Run();

The consuming endpoint is reachable through the /handle URL and expects a Message object in its body. It then uses the injected implementation of the IMessageHandler interface and passes it the message. If we run any of the HTTP requests in the ChainOfResponsibility.http file, we get a successful result similar to this:

Message ‘AlarmTriggered’ handled successfully.

The problem is that even if we send an invalid message, there is no way to know from the consumer, so it is always valid even when no handler picks up the message.To handle this scenario, let’s add a fourth handler (terminal handler) that notifies the consumers of invalid requests:

public class DefaultHandler : IMessageHandler
{
    public void Handle(Message message)
    {
        throw new NotSupportedException(
            $”Messages named ‘{message.Name}’ are not supported.”);
    }
}

This new terminal handler throws an exception that notifies the consumers about the error.

We can create custom exceptions to make differentiating between system and application errors easier. In this case, throwing a system exception is good enough. In a real-world application, I recommend creating a custom exception that represents the end of the chain and contains the relevant information for the consumers to react to it according to your use case.

Next, let’s register it in our chain (highlighted):

builder.Services.AddSingleton<IMessageHandler>(
    new AlarmTriggeredHandler(
        new AlarmPausedHandler(
            new AlarmStoppedHandler(
                new DefaultHandler()
            ))));

If we send a POST request with the name SomeUnhandledMessageName, the endpoint now yields the following exception:

System.NotSupportedException: Messages named ‘SomeUnhandledMessageName’ are not supported.
  
at ChainOfResponsibility.DefaultHandler.Handle(Message message) in C12\src\ChainOfResponsibility\DefaultHandler.cs:line 7
   at ChainOfResponsibility.AlarmStoppedHandler.Handle(Message message) in C12\src\ChainOfResponsibility\AlarmStoppedHandler.cs:line 19
   at ChainOfResponsibility.AlarmPausedHandler.Handle(Message message) in C12\src\ChainOfResponsibility\AlarmPausedHandler.cs:line 19
   at ChainOfResponsibility.AlarmTriggeredHandler.Handle(Message message) in C12\src\ChainOfResponsibility\AlarmTriggeredHandler.cs:line 19
   at Program.<>c.<<Main>$>b__0_0(Message message, IMessageHandler messageHandler) in C12\src\ChainOfResponsibility\Program.cs:line 22
   at lambda_method1(Closure, Object, HttpContext, Object)
   at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass100_2.<<HandleRequestBodyAndCompileRequestDelegateForJson>b__2>d.MoveNext()
— End of stack trace from previous location —
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
HEADERS
=======
Host: localhost:10001
Content-Type: application/json
traceparent: 00-5d737fdbb1018d5b7d060b74baf26111-2805f137fe1541af-00
Content-Length: 77

So far, so good, but the experience is not great, so let’s add a try-catch block to handle this in the endpoint:

app.MapPost(
    “/handle”,
    (Message message, IMessageHandler messageHandler) =>
    {
        try
        {
            messageHandler.Handle(message);
            return $”Message ‘{message.Name}’ handled successfully.”;
        }
        catch (NotSupportedException ex)
        {
            return ex.Message;
        }
    });

Now, when we send an invalid message, the API gently returns the following message to us:

Messages named ‘SomeUnhandledMessageName’ are not supported.

Of course, when you expect machines to consume your APIs, you should produce a data structure that is easier to parse, like using JSON.

And voilà. We have built a simple Chain of Responsibility that handles messages.

Leave a Reply

Your email address will not be published. Required fields are marked *



          Copyright © 2015-2024 | About | Terms of Service | Privacy Policy