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.
What is the Decorator Pattern?
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.
Why Use the Decorator Pattern?
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.
Registering a Decorator in Autofac
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.
Interface Definition
ICommandHandler.cs
:
interface ICommandHandler
{
void Handle();
}
Command Handler Implementations
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)}");
}
}
The Decorator
CommandHandlerDecorator.cs
:
class CommandHandlerDecorator(ICommandHandler innerHandler) : ICommandHandler
{
public void Handle()
{
Console.WriteLine($"{nameof(CommandHandlerDecorator)}.{nameof(Handle)}");
innerHandler.Handle();
}
}
Autofac Registration
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();
}
Output
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.
The Solution: Keyed Services in Autofac
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();
}
Updated Output
CommandHandlerDecorator.Handle
CheckoutProcessCommand.Handle
CommandHandlerDecorator.Handle
PaymentProcessCommandHandler.Handle
The decorator is now only applied to PaymentProcessCommandHandler
, as desired.
Conclusion
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!
The original post could be found here
Note: this blog post was edited by ChatGPT for a better reading experience.
Top comments (0)