Summary: in this tutorial, you’ll learn how to use extend the behavior of an object dynamically without using inheritance.
Introduction to the C# Decorator pattern
The Decorator pattern is a structural pattern that allows you to extend or modify the behavior of an object without changing the original implementation of the object.
The following UML diagram illustrates the Decorator pattern:
The Decorator pattern consists of the following elements:
Component
: This is the interface that defines operations an object can perform. TheComponent
can be an interface or an abstract class. TheComponent
defines an object that will be decorated.ConcreteComponent
: This is the class that implements theComponent
interface.Decorator
: This is an abstract class that implements theComponent
interface and contains a reference to theComponent
object.ConcreteDecorator
: This is the class that extends theDecorator
class and adds additional behavior to theComponent
object.
In this diagram, the Decorator
class inherits from the Component
. But it uses inheritance to achieve type matching only, not to reuse the functionality of the Component
.
C# Decorator pattern example
Let’s take an example to understand how the Decorator pattern works.
Developing a program that calls an API
Suppose you need to develop a program that calls the API from the following URL:
https://jsonplaceholder.typicode.com/posts/1
Code language: C# (cs)
The API returns a JSON object with the four fields userId
, id
, title
, and body
.
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
Code language: C# (cs)
The following shows the steps for creating the program that calls the above API endpoint:
First, create a Post
class that has the four corresponding fields:
public class Post
{
public int UserId { get; set; }
public int Id { get; set; }
public string? Title { get; set; }
public string? Body { get; set; }
public override string ToString() => $"{Id} - {Title}";
}
Code language: C# (cs)
In the Post
class, the ToString()
method returns a string that consists of the Id
and Title
of the Post
.
Second, create an interface called IPostService
that has one method
. The GetPost
method gets a post by an id and returns a GetPost
Post
object:
public interface IPostService
{
Task<Post?> GetPost(int postId);
}
Code language: C# (cs)
Third, create a PostService
class that implements the IPostService
interface:
public class PostService : IPostService
{
private readonly HttpClient client;
public PostService()
{
client = new HttpClient();
}
public async Task<Post?> GetPost(int postId)
{
var url = $"https://jsonplaceholder.typicode.com/posts/{postId}";
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync();
var post = JsonSerializer.Deserialize<Post?>(
responseBody,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
return post;
}
else
{
throw new Exception($"Error: {response.StatusCode}");
}
}
}
Code language: C# (cs)
In the PostService
class, the GetPost
method uses the HttpClient
to call the API to get the JSON response, and deserializes it to the Post
object using the JsonSerializer
, and returns the Post
. The method raises an exception if an error occurs.
Finally, use the PostService
class to call the API and display the Post
to the console:
public class Program
{
public static async Task Main(string[] args)
{
var postService = new PostService();
try
{
var post = await postService.GetPost(1);
Console.WriteLine(post);
}
catch (Exception)
{
throw;
}
}
}
Code language: C# (cs)
Putting it all together.
using System.Text.Json;
namespace Decorator;
public class Post
{
public int UserId { get; set; }
public int Id { get; set; }
public string? Title { get; set; }
public string? Body { get; set; }
public override string ToString() => $"{Id} - {Title}";
}
public interface IPostService
{
Task<Post?> GetPost(int postId);
}
public class PostService : IPostService
{
private readonly HttpClient client;
public PostService()
{
client = new HttpClient();
}
public async Task<Post?> GetPost(int postId)
{
var url = $"https://jsonplaceholder.typicode.com/posts/{postId}";
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync();
var post = JsonSerializer.Deserialize<Post?>(
responseBody,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
return post;
}
else
{
throw new Exception($"Error: {response.StatusCode}");
}
}
}
public class Program
{
public static async Task Main(string[] args)
{
var postService = new PostService();
try
{
var post = await postService.GetPost(1);
Console.WriteLine(post);
}
catch (Exception)
{
throw;
}
}
}
Code language: C# (cs)
Output:
1 - sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Code language: C# (cs)
The program works as expected.
Now, you receive a new requirement that you need to log the API call. To do that you can modify the PostService
class.
But, if you do so, you’ll violate the single responsibility principle. The PostService
class should be responsible for calling the API and returning a Post
. And it should not handle the logging functionality.
To meet the new requirements without changing the PostService
class, you can use the Decorator pattern by extending the PostService
object dynamically.
Adding a decorator object
We’ll define two new classes:
PostServiceDecorator
class which serves as theDecorator
classPostServiceLoggingDecorator
class which acts as theConcreteDecorator
class:
Here’s the new UML diagram:
Note that we don’t include the Post
class in the diagram to focus more on the Decorator pattern.
First, define a new PostServiceDecorator
abstract class that implements the
interface and has an IPostService
instance:IPostService
public abstract class PostServiceDecorator : IPostService
{
protected readonly IPostService postService;
public PostServiceDecorator(IPostService postService)
{
this.postService = postService;
}
public abstract Task<Post?> GetPost(int postId);
}
Code language: C# (cs)
Second, define the PostLoggingDecorator
class that implements the PostServiceDecorator
class:
public class PostServiceLoggingDecorator : PostServiceDecorator
{
public PostServiceLoggingDecorator(IPostService postService)
: base(postService) { }
public async override Task<Post?> GetPost(int postId)
{
Console.WriteLine($"Calling the API to get the post with ID: {postId}");
var stopwatch = Stopwatch.StartNew();
try
{
var post = await postService.GetPost(postId);
Console.WriteLine($"It took {stopwatch.ElapsedMilliseconds} ms to call the API");
return post;
}
catch (Exception ex)
{
Console.WriteLine($"GetPostAsync threw exception: {ex.Message}");
throw;
}
finally
{
stopwatch.Stop();
}
}
}
Code language: C# (cs)
The
method of GetPost()
PostServiceLoggingDecorator
calls the
method of the GetPost()
IPostService
instance. It also uses the StopWatch object to measure the time for calling the API and logs some information to the console.
In other words, the GetPost()
method of the PostServiceLoggingDecorator
class adds the logging functionality to the GetPost()
method of the IPostService
object.
Third, modify the Program
class to use the PostLoggingDecorator
class:
public class Program
{
public static async Task Main(string[] args)
{
IPostService postService = new PostService();
var postServiceLogging = new PostServiceLoggingDecorator(postService);
try
{
var post = await postServiceLogging.GetPost(1);
Console.WriteLine(post);
}
catch (Exception)
{
throw;
}
}
}
Code language: C# (cs)
In the Main()
method, we create an instance of the PostService
and pass it to the constructor of the PostServiceLoggingDecorator
.
The PostLoggingDecorator
acts as a decorator for the
object by adding the logging functionality to the PostService
object.PostService
Put it all together.
using System.Diagnostics;
using System.Text.Json;
namespace Decorator;
public class Post
{
public int UserId { get; set; }
public int Id { get; set; }
public string? Title { get; set; }
public string? Body { get; set; }
public override string ToString() => $"{Id} - {Title}";
}
public interface IPostService
{
Task<Post?> GetPost(int postId);
}
public class PostService : IPostService
{
private readonly HttpClient client;
public PostService()
{
client = new HttpClient();
}
public async Task<Post?> GetPost(int postId)
{
var url = $"https://jsonplaceholder.typicode.com/posts/{postId}";
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync();
var post = JsonSerializer.Deserialize<Post?>(
responseBody,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
return post;
}
else
{
throw new Exception($"Error: {response.StatusCode}");
}
}
}
public abstract class PostServiceDecorator : IPostService
{
protected readonly IPostService postService;
public PostServiceDecorator(IPostService postService)
{
this.postService = postService;
}
public abstract Task<Post?> GetPost(int postId);
}
public class PostServiceLoggingDecorator : PostServiceDecorator
{
public PostServiceLoggingDecorator(IPostService postService)
: base(postService) { }
public async override Task<Post?> GetPost(int postId)
{
Console.WriteLine($"Calling the API to get the post with ID: {postId}");
var stopwatch = Stopwatch.StartNew();
try
{
var post = await postService.GetPost(postId);
Console.WriteLine($"It took {stopwatch.ElapsedMilliseconds} ms to call the API");
return post;
}
catch (Exception ex)
{
Console.WriteLine($"GetPostAsync threw exception: {ex.Message}");
throw;
}
finally
{
stopwatch.Stop();
}
}
}
public class Program
{
public static async Task Main(string[] args)
{
IPostService postService = new PostService();
var postServiceLogging = new PostServiceLoggingDecorator(postService);
try
{
var post = await postServiceLogging.GetPost(1);
Console.WriteLine(post);
}
catch (Exception)
{
throw;
}
}
}
Code language: C# (cs)
In this example, we start with the PostService
object:
And decorate it with the PostServiceLoggingDecorator
object by adding the logging functionality:
Adding more decorator object
Suppose, you want to improve the performance of the PostService
class by caching the result of the API call.
For example, if a post with id 1 is requested for the first time, you can directly call the API and save it into a cache. But if the same post is requested again, you can retrieve the Post
with id 1 from the cache instead of making a call to the API.
To do this, you can add a new decorator class called PostServiceCache
that perform caching:
First, define the PostServiceCacheDecorator
that extends the PostServiceDecorator
:
public class PostServiceCacheDecorator : PostServiceDecorator
{
private readonly Dictionary<int, Post> cache;
public PostServiceCacheDecorator(IPostService postService) : base(postService)
{
cache = new Dictionary<int, Post>();
}
public async override Task<Post?> GetPost(int postId)
{
// get post from the cache
if (cache.TryGetValue(postId, out var post))
{
// demo purpose
Console.WriteLine($"Getting the post with id {postId} from the cache");
return post;
}
// otherwise call the API
post = await postService.GetPost(postId);
if (post != null)
{
cache[postId] = post;
}
return post;
}
}
Code language: C# (cs)
The PostServiceCacheDecorator
class has a member cache
that serves as a cache. The type of the cache
is Dictionary<int, Post>
which allows you to look up Post
by id.
The
method of the GetPost()
PostServiceCacheDecorator
class checks the cache for the requested id and returns the Post
if it is already in the cache. Otherwise, it uses the
method of the GetPost()
PostService
object to call the API and adds the result into the cache.
Second, modify the Program
to use the PostServiceCacheDecorator
that caches the result of the API call if the user requests the same post again:
public class Program
{
public static async Task Main(string[] args)
{
IPostService postService = new PostService();
var postServiceLogging = new PostServiceLoggingDecorator(postService);
var postServiceCache = new PostServiceCacheDecorator(postServiceLogging);
try
{
var post = await postServiceCache.GetPost(1);
Console.WriteLine(post);
// request the same post second time
Console.WriteLine("Getting the same post again:");
post = await postServiceCache.GetPost(1);
Console.WriteLine(post);
}
catch (Exception)
{
throw;
}
}
}
Code language: C# (cs)
Output:
Calling the API to get the post with ID: 1
It took 1698 ms to call the API
1 - sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Getting the same post again:
Getting the post with id 1 from the cache
1 - sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Code language: C# (cs)
The output shows that the second call to the GetPost
method does not call the API but gets the result from the cache.
In this example, the PostServiceLoggingDecorator decorates the PostService and the PotServiceCacheDecorator decorates the PostServiceLoggingDecorator class.
If you want to, you can add more decorator classes and they decorate each other to add more functionality to the decorated objects.
Also, you can mix and match the decorator based on the requirements. This makes your code more flexible.
Decorator pattern in .NET
.NET uses the decorator pattern in some libraries. For example, the BufferedStream
, GZipStream
, and CryptoStream
classes are the decorators of the Stream
class. The FileStream
, MemoryStream
, and NetworkStream
are concrete Stream
classes.
Note that the Stream
and FileStream
classes have more methods that the ones listed on the diagram. We put out the Read
and Write
method only for simplification purposes.
Summary
- Use the C# decorator pattern to extend the behavior of an object dynamically at runtime without using inheritance.