The other day, while working on a ticket at my job, I was tasked with adding a validation step to the checkout process for a specific platform. Initially, I thought this would be a simple change. I added the validation logic to the shared CheckoutProcessCommandHandler
class, which looked like this:
class CheckoutProcessCommand : ICommandHandler
{
public void Handle()
{
if (platform == 'specific-platform' && !Validate())
{
throw new InvalidOperationException();
}
// Other checkout logic...
}
}
After creating a pull request (PR), I assumed the ticket was complete. However, a reviewer suggested using a decorator pattern and registering it in the platform's Autofac module instead. The challenge? There were multiple ICommandHandler
implementations registered in the Autofac module, and I needed a way to apply the decorator to a single class only.
The decorator pattern is a structural design pattern that allows you to dynamically add new functionality to an object by wrapping it in one or more decorator classes. Importantly, it does this without altering the object's original structure.
Reusability
: You can create reusable components to extend functionality.
Maintainability
: Keeps the original codebase clean and focused on core logic.
Flexibility
: Allows dynamic composition of behaviors at runtime.
Here’s a simple example of how to register a decorator in Autofac. In this example, we have two classes implementing ICommandHandler
: CheckoutProcessCommand
and PaymentProcessCommandHandler
. Additionally, there’s a decorator class, CommandHandlerDecorator
, which adds functionality by printing an additional message.
ICommandHandler.cs
:
interface ICommandHandler
{
void Handle();
}
CheckoutProcessCommand.cs
:
class CheckoutProcessCommand : ICommandHandler
{
public void Handle()
{
Console.WriteLine($"{nameof(CheckoutProcessCommand)}.{nameof(Handle)}");
}
}
PaymentProcessCommandHandler.cs
:
class PaymentProcessCommandHandler : ICommandHandler
{
public void Handle()
{
Console.WriteLine($"{nameof(PaymentProcessCommandHandler)}.{nameof(Handle)}");
}
}
CommandHandlerDecorator.cs
:
class CommandHandlerDecorator(ICommandHandler innerHandler) : ICommandHandler
{
public void Handle()
{
Console.WriteLine($"{nameof(CommandHandlerDecorator)}.{nameof(Handle)}");
innerHandler.Handle();
}
}
Here’s how we register the command handlers and the decorator:
var builder = new ContainerBuilder();
builder.RegisterType<CheckoutProcessCommand>().As<ICommandHandler>();
builder.RegisterType<PaymentProcessCommandHandler>().As<ICommandHandler>();
builder.RegisterDecorator<CommandHandlerDecorator, ICommandHandler>();
var container = builder.Build();
foreach (var handler in container.Resolve<IEnumerable<ICommandHandler>>())
{
handler.Handle();
}
When you run the application, the output will look like this:
CommandHandlerDecorator.Handle
CheckoutProcessCommand.Handle
CommandHandlerDecorator.Handle
PaymentProcessCommandHandler.Handle
Notice that CommandHandlerDecorator.Handle
is executed before each command handler’s message. This happens because the decorator is applied to all instances implementing ICommandHandler
. However, this isn’t the behavior we want—we need to target a specific implementation.
After consulting the Autofac documentation on Keyed Service, I found a solution to apply the decorator to a specific implementation. Here’s the updated code:
var builder = new ContainerBuilder();
builder.RegisterType<CheckoutProcessCommand>().As<ICommandHandler>();
builder.RegisterType<PaymentProcessCommandHandler>().Keyed<ICommandHandler>(nameof(PaymentProcessCommandHandler));
builder.RegisterDecorator<ICommandHandler>(innerInstance => new CommandHandlerDecorator(innerInstance), nameof(PaymentProcessCommandHandler));
var container = builder.Build();
foreach (var handler in container.Resolve<IEnumerable<ICommandHandler>>())
{
handler.Handle();
}
CommandHandlerDecorator.Handle
CheckoutProcessCommand.Handle
CommandHandlerDecorator.Handle
PaymentProcessCommandHandler.Handle
The decorator is now only applied to PaymentProcessCommandHandler
, as desired.
Using Autofac’s keyed services allows for precise control over which classes to apply decorators to. This approach helped me implement the required functionality without affecting other parts of the system. My PR was approved, and I was finally able to close the ticket!
Note: this blog post was edited by ChatGPT for a better reading experience.