Understanding Decorator Pattern
The Decorator Pattern is a structural design pattern used to dynamically add new behaviors or responsibilities to an object at runtime without altering its structure or modifying its existing code. This pattern is part of the Gang of Four (GoF) design patterns and promotes the open/closed principle, which states that classes should be open for extension but closed for modification.
In .NET applications, you can add cross-cutting concerns like logging, caching, or performance monitoring to your services.
Decorator Implementation
First, let’s define an example of an interface for the Book Service.
public interface IBookService
{
Task<Book> GetBookAsync(int bookId);
}
public record Book(int Id);
As the next step, let’s create an implementation.
public class BookService: IBookService
{
public async Task<Book> GetBookAsync(int bookId)
{
return await Task.FromResult(new Book(bookId));
}
}
We want to add operations logging to our service without modifying its logic. Let’s create a LoggingBookDecorator for that.
public class LoggingBookDecorator : IBookService
{
private readonly IBookService _bookService;
private readonly ILogger<LoggingBookDecorator> _logger;
public LoggingBookDecorator(
IBookService bookService,
ILogger<LoggingBookDecorator> logger)
{
_bookService = bookService;
_logger = logger;
}
public async Task<Book> GetBookAsync(int bookId)
{
_logger.LogInformation($"Getting book with ID: {bookId}");
try
{
var result = await _bookService.GetBookAsync(bookId);
_logger.LogInformation($"Successfully retrieved book {bookId}");
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error retrieving book {bookId}");
throw;
}
}
}
Register Decorator
To register a decorator in .NET’s Dependency Injection (DI) container, you must explicitly resolve the dependencies and create the decorator.
First, register the original BookService with the DI container.
Next, register the LoggingBookDecorator. Instead of directly registering it as a type, you use a factory method to resolve the dependencies and wrap the BookService with the LoggingBookDecorator.
builder.Services.AddScoped<BookService>();
builder.Services.AddScoped<IBookService>(provider =>
{
var innerService = provider.GetRequiredService<BookService>();
var logger = provider.GetRequiredService<ILogger<LoggingBookDecorator>>();
return new LoggingBookDecorator(innerService, logger);
});
ScrutorÂ
Scrutor is a popular library that makes decorator registration more convenient.
First, install the Scrutor package:
dotnet add package Scrutor
Then, you can simplify the decorator registration in the DI:
builder.Services.AddScoped<IBookService, BookService>()
.Decorate<IBookService, LoggingBookDecorator>();
You can chain multiple decorators. The order of registration determines the order of execution:
builder.Services.AddScoped<IBookService, BookService>()
.Decorate<IBookService, LoggingBookDecorator>()
.Decorate<IBookService, CachingBookDecorator>();
Conclusion
The decorator pattern, combined with .NET’s built-in DI container, provides a powerful way to enhance services dynamically. Whether you use libraries like Scrutor or manual registration, the approach is straightforward and effective.
Mastering this technique will add another tool to your .NET developer arsenal, enabling you to write more modular and maintainable applications.