Table of Contents
- 2.1 Introduction to Decorator Pattern
- 2.2 Presentation of the example project
- 2.3 Source Code and Downloads
- 3.1 Why use a Decorator for logging
- 3.2 Structure of a Decorator — The two fundamental requirements
- 3.3 WeatherServiceLoggingDecorator — Full implementation
- 3.4 Wiring Decorator in Controller
- 4.1 WeatherServiceCachingDecorator — Implementation
- 4.2 Composition of Onion Decorators
- 4.3 Execution trace — The layering effect
- 5.1 IoC Container — Concepts
- 5.2 .NET Core — Lambda Function Approach
- 5.3 .NET Core — Approach with Scrutor
- 5.4 .NET Framework — Autofac
- 7.1 Cross-cutting concerns
- 7.2 Data modification
- 7.3 Extract an interface with Visual Studio
- 7.4 The Adapter pattern as a complementary solution
1. Course Overview
This course presents one of the classic Gang of Four patterns: the Decorator. The Decorator pattern allows you to dynamically add functionality to classes without changing the target class. It does this by creating a wrapper around the original class, giving you an extension point to inject new behaviors.
What you will learn in this course:
- How the Decorator Pattern works
- Code and apply two Decorator classes by yourself
- See where the Decorator Pattern is used in .NET itself
Prerequisites: You must be comfortable with object-oriented concepts in C#, particularly the use of interfaces.
2. The Decorator pattern — Fundamental concepts
2.1 Introduction to Decorator Pattern
The Decorator Pattern is a Gang of Four structural design pattern that allows you to dynamically add behavior to a class without making changes to it. Adding this behavior via the Decorator pattern allows you to maintain a strong separation of concerns in your code base, while giving you the flexibility to add, remove, or reorder behaviors as needed.
How the pattern works:
- First you have a concrete class (the
Component) that you want to decorate — it implements a common interface. - Your Decorator class implements this same interface.
- In the implementations of Decorator methods, you can inject any new behavior you want.
- The Decorator defines a constructor that takes an object implementing the same interface — this allows it to receive the decorated object and act as a wrapper around it.
Visual structure:
[Client]
|
v
[CachingDecorator] ← implémente IWeatherService
|
v
[LoggingDecorator] ← implémente IWeatherService
|
v
[WeatherService] ← implémente IWeatherService (concrète)
Since the Decorator class implements the same interface as the original Component object, there is no need to modify the calling code. So you can override this whole object structure where you previously used a simple WeatherService.
Onion Analogy: You are not limited to just one Decorator at a time. You can define and use multiple Decorators together, forming a layered structure around your original object, like an onion. Each Decorator class can define different behaviors, thus maintaining a strong separation of concerns.
2.2 Presentation of the example project
The sample project is an ASP.NET MVC web application that serves as a container for the code we will write. The app displays current weather conditions for location on the home page. By default, it shows Chicago weather, but you can enter any city.
Core class: WeatherService — it implements the IWeatherService interface and calls the OpenWeatherMap API to get actual weather data based on a provided city name.
What we will implement:
WeatherServiceLoggingDecorator— decorates the service with loggingWeatherServiceCachingDecorator— decorates the service with caching
Code evolution in the controller:
Front (without Decorator):
_weatherService = new WeatherService(apiKey);
After (with two Decorators):
IWeatherService innerService = new WeatherService(apiKey);
IWeatherService withLoggingDecorator = new WeatherServiceLoggingDecorator(innerService, logger);
IWeatherService withCachingDecorator = new WeatherServiceCachingDecorator(withLoggingDecorator, memoryCache);
_weatherService = withCachingDecorator;
This example clearly illustrates why each of our Decorator classes must define a constructor that takes an object of type IWeatherService: this is what allows us to build this structure from the inside out.
2.3 Source Code and Downloads
Source code is available in course downloads or from the GitHub repository. There is a version for .NET Core and a version for .NET Framework. In terms of implementing the Decorator Pattern, the code is identical except for the part where you need to configure your Decorator classes to work with an IoC Container.
OpenWeatherMap API key: To run the application, you will need an OpenWeatherMap API key. Register for free at openweathermap.org/price, then add your key:
- For ASP.NET Core: in the
appsettings.jsonfile - For .NET Framework: in the
web.configfile
3. Creating your first Decorator
3.1 Why use a Decorator for logging
The first Decorator to implement is a logging Decorator. It will track each time the service is called, what parameters are passed to it, what the response is, and how long each call takes.
Why not put this code directly in WeatherService?
Two main problems:
-
Readability and error risk: If you start adding a lot of logging code to
WeatherService, the class becomes harder to read, and it is harder to understand exactly what it does. By adding more logic, we risk accidentally introducing an error into the service code. -
Violation of the Single Responsibility Principle: If we put the logging code in
WeatherService, this class now manages several concerns: data recovery AND logging. If we want to add more concerns afterwards (like caching), we make this problem worse.
The right approach: Create a Decorator class responsible for logging, which keeps each class narrowly focused on a single concern.
3.2 Structure of a Decorator — The two fundamental requirements
Requirement 1: Implement the same interface
The Decorator class must implement the same interface (or extend the same base class) as the classes it will decorate. It must also provide at least one default implementation for each method in the interface — even if it’s just a pass-through.
Requirement 2: Take an instance of the object to decorate in its constructor
The Decorator class must accept an instance of the decorated object (here IWeatherService) in its constructor. It is this object that the Decorator will wrap.
3.3 WeatherServiceLoggingDecorator — Full implementation
The class is placed in the WeatherInterface folder because it can be applied to any implementation of IWeatherService. Decorators are based on the IWeatherService abstraction, not on any particular concrete implementation.
using System;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using DecoratorDesignPattern.WeatherInterface;
namespace DecoratorDesignPattern.WeatherInterface
{
public class WeatherServiceLoggingDecorator : IWeatherService
{
private readonly IWeatherService _innerWeatherService;
private readonly ILogger<WeatherServiceLoggingDecorator> _logger;
public WeatherServiceLoggingDecorator(
IWeatherService innerWeatherService,
ILogger<WeatherServiceLoggingDecorator> logger)
{
_innerWeatherService = innerWeatherService;
_logger = logger;
}
public CurrentWeather GetCurrentWeather(string location)
{
_logger.LogInformation("GetCurrentWeather called for {location}", location);
var stopwatch = Stopwatch.StartNew();
var result = _innerWeatherService.GetCurrentWeather(location);
stopwatch.Stop();
_logger.LogInformation(
"GetCurrentWeather for {location} completed in {elapsed}ms. Success={success}",
location, stopwatch.ElapsedMilliseconds, result.Success);
return result;
}
public LocationForecast GetForecast(string location)
{
_logger.LogInformation("GetForecast called for {location}", location);
var stopwatch = Stopwatch.StartNew();
var result = _innerWeatherService.GetForecast(location);
stopwatch.Stop();
_logger.LogInformation(
"GetForecast for {location} completed in {elapsed}ms. Success={success}",
location, stopwatch.ElapsedMilliseconds, result.Success);
return result;
}
}
}
Key points to note:
- The member variable
_innerWeatherServicekeeps a reference to the decorated object. - The code is executed before and after the call to the internal service (line
_innerWeatherService.GetCurrentWeather(location)), which is exactly the essence of the Decorator pattern. - We use
Stopwatchto measure execution time. - We could modify the values that go into the internal call, modify the response in some way, or take some other action.
3.4 Wiring the Decorator in the controller
To tell the application to use this Decorator, we modify the HomeController:
Before (Clip 3 start — initial project):
public HomeController(ILoggerFactory loggerFactory, IConfiguration configuration, IMemoryCache memoryCache)
{
_loggerFactory = loggerFactory;
_logger = _loggerFactory.CreateLogger<HomeController>();
String apiKey = configuration.GetValue<String>("AppSettings:OpenWeatherMapApiKey");
_weatherService = new WeatherService(apiKey);
}
After (Clip 3 end — Decorator logging added):
public HomeController(ILoggerFactory loggerFactory, IConfiguration configuration)
{
_loggerFactory = loggerFactory;
_logger = _loggerFactory.CreateLogger<HomeController>();
String apiKey = configuration.GetValue<String>("AppSettings:OpenWeatherMapApiKey");
_weatherService = new WeatherServiceLoggingDecorator(
new WeatherService(apiKey),
_loggerFactory.CreateLogger<WeatherServiceLoggingDecorator>());
}
Now, when calling GetCurrentWeather on _weatherService, the call first goes through the Decorator before reaching the WeatherService. This is the essence of the Decorator pattern: the Decorator will wrap one or more other objects and allow you to intercept all method calls and process logic there.
4. Using Multiple Decorators Together
4.1 WeatherServiceCachingDecorator — Implementation
The second Decorator implements caching: if the same weather conditions or forecasts are requested for the same city in a given period, we use a value taken from the cache instead of making another call to the API.
This is another classic use of the Decorator Pattern: to cache a call that goes to an external service or takes a long time to execute. This gives a good separation of concerns and a very elegant solution.
using System;
using Microsoft.Extensions.Caching.Memory;
using DecoratorDesignPattern.WeatherInterface;
namespace DecoratorDesignPattern.WeatherInterface
{
public class WeatherServiceCachingDecorator : IWeatherService
{
private readonly IWeatherService _innerWeatherService;
private readonly IMemoryCache _memoryCache;
public WeatherServiceCachingDecorator(
IWeatherService innerWeatherService,
IMemoryCache memoryCache)
{
_innerWeatherService = innerWeatherService;
_memoryCache = memoryCache;
}
public CurrentWeather GetCurrentWeather(string location)
{
string cacheKey = $"CurrentWeather-{location}";
if (_memoryCache.TryGetValue<CurrentWeather>(cacheKey, out var currentWeather))
{
// La valeur est en cache — court-circuit, on retourne directement
return currentWeather;
}
else
{
// Pas en cache — appel au service interne
currentWeather = _innerWeatherService.GetCurrentWeather(location);
_memoryCache.Set(cacheKey, currentWeather, TimeSpan.FromMinutes(30));
return currentWeather;
}
}
public LocationForecast GetForecast(string location)
{
string cacheKey = $"Forecast-{location}";
if (_memoryCache.TryGetValue<LocationForecast>(cacheKey, out var forecast))
{
return forecast;
}
else
{
forecast = _innerWeatherService.GetForecast(location);
_memoryCache.Set(cacheKey, forecast, TimeSpan.FromMinutes(30));
return forecast;
}
}
}
}
Important point: In some cases, when you intercept a call, you do not need to go any further. If the value is in the cache, we make a short circuit and return directly without calling the internal service. This is a perfectly valid thing in a Decorator.
4.2 Composition of Onion Decorators
Clip 4 end — HomeController with both Decorators:
public HomeController(ILoggerFactory loggerFactory, IConfiguration configuration, IMemoryCache memoryCache)
{
_loggerFactory = loggerFactory;
_logger = _loggerFactory.CreateLogger<HomeController>();
_memoryCache = memoryCache;
String apiKey = configuration.GetValue<String>("AppSettings:OpenWeatherMapApiKey");
// Construction de l'intérieur vers l'extérieur — comme un oignon
IWeatherService innerService = new WeatherService(apiKey);
IWeatherService withLoggingDecorator = new WeatherServiceLoggingDecorator(
innerService,
_loggerFactory.CreateLogger<WeatherServiceLoggingDecorator>());
IWeatherService withCachingDecorator = new WeatherServiceCachingDecorator(
withLoggingDecorator,
_memoryCache);
_weatherService = withCachingDecorator;
}
Objects are constructed from the inside out by passing each object to the constructor of the next object. This way of writing code is more readable than nesting constructors.
4.3 Execution trace — The layering effect
When debugging, the call trace follows this order:
CachingDecorator.GetCurrentWeather→ empty cache → pass insideLoggingDecorator.GetCurrentWeather→ log start, chrono call → pass insideWeatherService.GetCurrentWeather→ makes the API call → returns the result- Return to
LoggingDecorator→ log the duration → return the result - Return to
CachingDecorator→ caches the result → returns
Second call (hot cache):
CachingDecorator.GetCurrentWeather→ cached data for Chicago → returns immediately- The
LoggingDecoratorandWeatherServiceare never called
This illustrates the great strength of the Decorator Pattern: you can add, delete or reorder these behaviors as needed, without modifying the client code.
5. Using Decorators with Dependency Injection
These days, we create objects less and less manually with new — we instead use Dependency Injection to create our objects. Let’s see how to use the Decorator Pattern with DI.
5.1 IoC Container — Concepts
With Dependency Injection, the controller receives an IWeatherService object injected into its constructor — creating an appropriate object is now the responsibility of the IoC container (Inversion of Control Container).
HomeController refactored with DI (starting point — Clip 5 start):
// Dans Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddMemoryCache();
String apiKey = Configuration.GetValue<String>("AppSettings:OpenWeatherMapApiKey");
services.AddScoped<IWeatherService>(serviceProvider => new WeatherService(apiKey));
}
5.2 .NET Core — Lambda function approach
We can extend the lambda function to include our Decorators:
// Dans Startup.cs — Clip 5 end - Lambda Function
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddMemoryCache();
services.AddScoped<IWeatherService>(serviceProvider =>
{
String apiKey = Configuration.GetValue<String>("AppSettings:OpenWeatherMapApiKey");
var logger = serviceProvider.GetService<ILogger<WeatherServiceLoggingDecorator>>();
var memoryCache = serviceProvider.GetService<IMemoryCache>();
IWeatherService concreteService = new WeatherService(apiKey);
IWeatherService withLoggingDecorator = new WeatherServiceLoggingDecorator(concreteService, logger);
IWeatherService withCachingDecorator = new WeatherServiceCachingDecorator(withLoggingDecorator, memoryCache);
return withCachingDecorator;
});
}
Now, when HomeController or any other code needs an IWeatherService, this function is executed and returns a WeatherService surrounded by its Decorators.
5.3 .NET Core — Approach with Scrutor
If you prefer smoother syntax, you can install the Scrutor NuGet package:
Install-Package Scrutor
Scrutor provides a Decorate extension method which allows for more readable configuration:
// Dans Startup.cs — Clip 5 end - Scrutor
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddMemoryCache();
String apiKey = Configuration.GetValue<String>("AppSettings:OpenWeatherMapApiKey");
services.AddScoped<IWeatherService>(serviceProvider => new WeatherService(apiKey));
services.Decorate<IWeatherService>((inner, provider) =>
new WeatherServiceLoggingDecorator(
inner,
provider.GetService<ILogger<WeatherServiceLoggingDecorator>>()));
services.Decorate<IWeatherService>((inner, provider) =>
new WeatherServiceCachingDecorator(
inner,
provider.GetService<IMemoryCache>()));
}
Explanation of the Decorate method: It takes a lambda function with two arguments:
inner: the object that the Decorator will wrap (the decorated object)provider: the IoC container, which we need to obtain instances ofloggerandmemoryCache
Both approaches (lambda function and Scrutor) work equally well. Scrutor offers slightly more readable syntax.
5.4 .NET Framework — Autofac
If you are using ASP.NET MVC 5, you are probably using Autofac or Ninject as your IoC container. Consult the documentation for the library you are using to see how to wire Decorators.
Example with Autofac:
using Autofac;
using Autofac.Integration.Mvc;
using System.Web.Mvc;
namespace DecoratorDesignPattern.App_Start
{
public class AutofacConfig
{
public static void ConfigureAutofac()
{
var builder = new ContainerBuilder();
// Enregistrement des contrôleurs MVC
builder.RegisterControllers(typeof(MvcApplication).Assembly);
builder.RegisterModelBinders(typeof(MvcApplication).Assembly);
builder.RegisterModelBinderProvider();
// Enregistrement du service concret
builder.RegisterType<WeatherService>()
.Named<IWeatherService>("weatherService")
.WithParameter("apiKey", ConfigurationManager.AppSettings["OpenWeatherMapApiKey"]);
// Décoration avec le LoggingDecorator
builder.RegisterDecorator<IWeatherService>(
(c, inner) => new WeatherServiceLoggingDecorator(inner, c.Resolve<ILogger>()),
fromKey: "weatherService")
.Named<IWeatherService>("withLogging");
// Décoration avec le CachingDecorator
builder.RegisterDecorator<IWeatherService>(
(c, inner) => new WeatherServiceCachingDecorator(inner, c.Resolve<Cache>()),
fromKey: "withLogging");
var container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}
}
}
Key Point: You can use Decorators even when you use Dependency Injection. You get the same wrapped object structure, just configured differently.
6. The Decorator Pattern in .NET itself
The Decorator Pattern is not only used in the code we write as developers, but also in .NET itself. An excellent example is the Stream class and related classes for working with streams.
6.1 The hierarchy of Streams
Level 1 — The Stream abstract class (the abstraction):
At the top of the hierarchy, we find the Stream class. It is an abstract class that requires implementing several methods, the most important of which are Read and Write. Although it is an abstract class, it is equivalent to the interface used in our previous examples. It defines the abstraction that each implementation must respect.
Level 2 — Concrete implementations (Components):
FileStream— reads and writes bytes to/from filesMemoryStream— reads and writes bytes to/from memoryNetworkStream— reads and writes bytes to/from a network connection
These classes do the actual work of reading and writing. Each is equivalent to the WeatherService class seen in the previous example.
Level 3 — The Decorators:
BufferedStream— buffers data when reading or writingGZipStream— decompresses or compresses data during processingCryptoStream— encrypts or decrypts data
These classes all extend Stream, all take a Stream object in their constructor, and all modify the stream in some way. They are Decorators, even though the word “decorator” does not appear in their name.
They are Decorators because they:
- Extend the same common base class
Stream - Take an instance of a
Streamobject in their constructor - Add new behaviors to the stream they wrap
- Works with any concrete stream (file, memory, network)
6.2 Example code with CryptoStream and BufferedStream
This program takes a string and writes it to a file using CryptoStream to encrypt the data and BufferedStream to improve performance:
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;
namespace StreamsAndDecorators
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Enter some text to be encrypted to a file and then decrypted:");
string text = Console.ReadLine();
if (String.IsNullOrEmpty(text))
{
Console.WriteLine("You must enter some text!");
return;
}
String filename = Path.GetTempFileName();
using (Rijndael rijndael = Rijndael.Create())
{
byte[] cryptoKey = rijndael.Key;
byte[] cryptoIV = rijndael.IV;
// Écriture avec chiffrement
using (FileStream fileStream = new FileStream(filename, FileMode.Append, FileAccess.Write))
{
ICryptoTransform encryptor = rijndael.CreateEncryptor(rijndael.Key, rijndael.IV);
// Structure en oignon : CryptoStream enveloppe BufferedStream qui enveloppe FileStream
using (Stream stream = new CryptoStream(new BufferedStream(fileStream), encryptor, CryptoStreamMode.Write))
{
using (StreamWriter writer = new StreamWriter(stream))
{
writer.Write(text);
}
}
}
Console.WriteLine("This is the encrypted data");
string encryptedData = Convert.ToBase64String(File.ReadAllBytes(filename));
Console.WriteLine(encryptedData);
// Lecture avec déchiffrement
using (FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read))
{
ICryptoTransform decryptor = rijndael.CreateDecryptor(rijndael.Key, rijndael.IV);
using (Stream stream = new CryptoStream(new BufferedStream(fileStream), decryptor, CryptoStreamMode.Read))
{
using (StreamReader reader = new StreamReader(stream))
{
string decryptedData = reader.ReadToEnd();
Console.WriteLine("This is the decrypted data");
Console.WriteLine(decryptedData);
}
}
}
}
}
}
}
Onion structure observed:
[StreamWriter]
|
v
[CryptoStream] ← Decorator : chiffre les données
|
v
[BufferedStream] ← Decorator : tamponne les données
|
v
[FileStream] ← Concrète : écrit sur le disque
Here we see the same wrapping behavior as before: CryptoStream and BufferedStream wrap our FileStream and add their respective functionalities. The Bottom Line: The Decorator Pattern is a pattern that you will see not only in your own code, but also in frameworks and libraries in the .NET ecosystem.
7. Use cases and best practices
7.1 Cross-cutting concerns
One of the most common places Decorators are used is to apply cross-cutting concerns. These are concerns like:
- Logging — Trace calls, settings, responses and duration
- Performance tracking — Measure execution time
- Caching — Cache expensive call results
- Authorization — Check access rights before executing a method
These concerns are called “transversal” because they are found throughout the application, but are not part of the main business behavior. The Decorator Pattern keeps them neatly separated into dedicated classes.
7.2 Changing data
Another use is to modify data sent to and from a component. For example :
- Encrypt and decrypt some fields of an object before passing them to a component
- Convert values (e.g. US units to metric system)
Both problems can easily be solved with a Decorator class.
7.3 Extract an interface with Visual Studio
One of the requirements of the Decorator Pattern is to have a common interface. But what if the class you want to add functionality to doesn’t implement a common interface?
Solution: Extract an interface from the class and have this interface implemented by the class and its Decorators.
Visual Studio makes this easy with built-in refactoring tools:
- Keyboard shortcut:
Ctrl+R, thenCtrl+I - Menu:
Edit > Refactor > Extract Interface
7.4 The Adapt pattern as a complementary solution
What if you have a class that you don’t own, or that you can’t modify to implement an interface?
Solution: Use another design pattern, the Adapter pattern, to put a class in front of your component, and have the Adapter implement the interface you extracted.
In this case, the Adapter class will be a simple wrapper around your Component class, but it will give you a class that you control, thus serving as an extension point on which you can build your Decorator classes.
8. Structure of the demonstration project
8.1 IWeatherService Interface
namespace DecoratorDesignPattern.WeatherInterface
{
public interface IWeatherService
{
CurrentWeather GetCurrentWeather(String location);
LocationForecast GetForecast(String location);
}
}
8.2 Data models
CurrentWeather:
public class CurrentWeather
{
public bool Success { get; set; }
public String ErrorMessage { get; set; }
public LocationData Location { get; set; }
public DateTime ObservationTime { get; set; }
public DateTime ObservationTimeUtc { get; set; }
public WeatherData CurrentConditions { get; set; }
public DateTime FetchTime { get; set; }
public class LocationData
{
public String Name { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
}
public class WeatherData
{
public String Conditions { get; set; }
public String ConditionsDescription { get; set; }
public double Visibility { get; set; }
public int CloudCover { get; set; }
public double Temperature { get; set; }
public double Pressure { get; set; }
public double Humidity { get; set; }
public double WindSpeed { get; set; }
public CompassDirection WindDirection { get; set; }
public double WindDirectionDegrees { get; set; }
public double RainfallOneHour { get; set; }
}
}
LocationForecast:
public class LocationForecast
{
public LocationForecast()
{
Forecasts = new List<WeatherForecast>();
}
public bool Success { get; set; }
public String ErrorMessage { get; set; }
public ForecastLocation Location { get; set; }
public DateTime FetchTime { get; set; }
public List<WeatherForecast> Forecasts { get; private set; }
}
public class WeatherForecast
{
public DateTime ForecastTime { get; set; }
public String Conditions { get; set; }
public String ConditionsDescription { get; set; }
public int CloudCover { get; set; }
public double Temperature { get; set; }
public double Pressure { get; set; }
public double Humidity { get; set; }
public double WindSpeed { get; set; }
public CompassDirection WindDirection { get; set; }
public double WindDirectionDegrees { get; set; }
public double ExpectedRainfall { get; set; }
public double ExpectedSnowfall { get; set; }
}
8.3 WeatherService — Concrete implementation
The concrete implementation in the OpenWeatherMap folder calls the OpenWeatherMap REST API and maps the results into standard objects:
public class WeatherService : IWeatherService
{
private RestClient _apiClient;
private string _apiKey;
public WeatherService(String apiKey)
{
_apiKey = apiKey;
_apiClient = new RestClient("http://api.openweathermap.org");
}
public CurrentWeather GetCurrentWeather(string location)
{
var request = new RestRequest("data/2.5/weather");
request.AddParameter("q", location);
request.AddParameter("units", "imperial");
request.AddParameter("appid", _apiKey);
var response = _apiClient.Get(request);
if (response.IsSuccessful)
{
var currentConditions = JsonConvert.DeserializeObject<CurrentConditionsResponse>(response.Content);
return MapCurrentConditionsResponse(currentConditions);
}
else
{
switch (response.StatusCode)
{
case HttpStatusCode.NotFound:
return new CurrentWeather()
{
Success = false,
ErrorMessage = $"Weather data for {location} could not be found"
};
case HttpStatusCode.BadRequest:
return new CurrentWeather()
{
Success = false,
ErrorMessage = "Bad request. Make sure you have an API key."
};
default:
return new CurrentWeather()
{
Success = false,
ErrorMessage = "Error calling OpenWeatherMap API."
};
}
}
}
public LocationForecast GetForecast(string location)
{
var request = new RestRequest("data/2.5/forecast");
request.AddParameter("q", location);
request.AddParameter("units", "imperial");
request.AddParameter("appid", _apiKey);
var response = _apiClient.Get(request);
if (response.IsSuccessful)
{
var forecastData = JsonConvert.DeserializeObject<ForecastResponse>(response.Content);
return MapForecastResponse(forecastData);
}
// ... gestion des erreurs similaire
}
}
Project folder structure:
DecoratorDesignPattern/
├── Controllers/
│ └── HomeController.cs
├── Models/
│ └── ErrorViewModel.cs
├── OpenWeatherMap/
│ ├── CurrentConditionsResponse.cs
│ ├── ForecastResponse.cs
│ └── WeatherService.cs ← Implémentation concrète
├── WeatherInterface/
│ ├── CompassDirection.cs
│ ├── CurrentWeather.cs
│ ├── IWeatherService.cs ← Interface commune
│ ├── LocationForecast.cs
│ ├── WeatherServiceLoggingDecorator.cs ← Decorator 1
│ └── WeatherServiceCachingDecorator.cs ← Decorator 2
├── Views/
│ └── Home/
│ ├── ApiKey.cshtml
│ ├── Forecast.cshtml
│ └── Index.cshtml
└── appsettings.json
Demo progress (numbered folders):
| File | Description |
|---|---|
1 - Clip 3 start | Initial project without Decorator |
2 - Clip 3 end | LoggingDecorator added |
3 - Clip 4 end | Added CachingDecorator |
4 - Clip 5 start | Refactoring to DI (initial state) |
5 - Clip 5 end - Lambda Function | DI with lambda function |
5 - Clip 5 end - Scrutor | DI with Scrutor |
6 - DotNet Streams | Example with .NET streams |
9. Summary and conclusion
The big ideas of the Decorator Pattern
1. Program towards an abstraction
Using a common interface or base class helps us create loosely coupled software by allowing us to program towards an abstraction. In the case of the Decorator Pattern, this ability also gives us an extension point for our classes.
2. The onion structure
You can create Decorator classes that wrap around Component objects and even around each other. Through these Decorator classes, we can inject functionality into the component’s method calls.
3. Separation of concerns and flexibility
Being able to layer objects in this onion structure and intercept and modify method calls is a very powerful idea because it allows us to separate concerns and dynamically add new functionality as new needs emerge.
Summary of steps to implement a Decorator
- Identify the common interface (or extract it if it does not exist)
- Create the Decorator class which:
- Implements the same interface as the class to be decorated
- Sets a member variable for the decorated object
- Accept decorated object in its constructor
- Provides implementation for all interface methods
- Inject behavior before and/or after internal service calls
- Wire the Decorators in the client code or in the IoC container
Comparison table of wiring approaches
| Context | Approach | Example |
|---|---|---|
| Manual instantiation | nested new | new CachingDecorator(new LoggingDecorator(new Service())) |
| .NET Core DI | Lambda function | services.AddScoped<IService>(sp => ...) |
| .NET Core DI | Scrutor | services.Decorate<IService>((inner, sp) => ...) |
| .NET Framework | Autofac | builder.RegisterDecorator<IService>(...) |
When to use the Decorator Pattern
- Need to add cross-cutting concerns (logging, caching, auth, perf)
- Need to modify incoming or outgoing data from a component
- Need to add behaviors without modifying existing classes
- Need the flexibility to resequence or disable behaviors
When to consider alternatives
- If the target class has no interface: extract an interface or use the pattern Adapter
- If the behavior to be added is very strongly linked to business logic: reconsider whether a Decorator is the right approach
Search Terms
c-sharp · design · patterns · decorator · testing · architecture · c# · .net · development · pattern · decorators · approach · concepts · core · data · fundamental · interface · wiring