StructuralC#verifiedVerified
Decorator Pattern in C#
Attaches additional responsibilities to an object dynamically by wrapping it in decorator objects that share the same interface.
How to Implement the Decorator Pattern in C#
1Step 1: Define the component interface
public interface IDataSource
{
string Read();
void Write(string data);
}2Step 2: Concrete component
public class FileDataSource(string filename) : IDataSource
{
public string Read() => $"[Data from {filename}]";
public void Write(string data) =>
Console.WriteLine($"Writing to {filename}: {data}");
}3Step 3: Base decorator forwards to wrapped component
public abstract class DataSourceDecorator(IDataSource source) : IDataSource
{
public virtual string Read() => source.Read();
public virtual void Write(string data) => source.Write(data);
}4Step 4: Concrete decorators add behavior
public class EncryptionDecorator(IDataSource source)
: DataSourceDecorator(source)
{
public override string Read() =>
$"Decrypt({base.Read()})";
public override void Write(string data) =>
base.Write($"Encrypt({data})");
}
public class CompressionDecorator(IDataSource source)
: DataSourceDecorator(source)
{
public override string Read() =>
$"Decompress({base.Read()})";
public override void Write(string data) =>
base.Write($"Compress({data})");
}
// Usage:
// IDataSource source = new FileDataSource("data.txt");
// source = new EncryptionDecorator(source);
// source = new CompressionDecorator(source);
// source.Write("hello"); // Compress(Encrypt(hello))using System.Diagnostics;
using Microsoft.Extensions.Logging;
// [step] Define the HTTP client interface
public interface IHttpClient
{
Task<HttpResponse> SendAsync(
HttpRequest request, CancellationToken ct = default);
}
public record HttpRequest(string Method, string Url,
Dictionary<string, string>? Headers = null, string? Body = null);
public record HttpResponse(int StatusCode, string Body,
Dictionary<string, string> Headers, TimeSpan Duration);
// [step] Base HTTP client
public class BasicHttpClient : IHttpClient
{
public async Task<HttpResponse> SendAsync(
HttpRequest request, CancellationToken ct = default)
{
await Task.Delay(10, ct); // Simulate HTTP call
return new HttpResponse(200, "OK", [], TimeSpan.FromMilliseconds(10));
}
}
// [step] Logging decorator adds request/response tracing
public class LoggingHttpClient(
IHttpClient inner, ILogger logger) : IHttpClient
{
public async Task<HttpResponse> SendAsync(
HttpRequest request, CancellationToken ct = default)
{
logger.LogInformation("{Method} {Url}", request.Method, request.Url);
var sw = Stopwatch.StartNew();
var response = await inner.SendAsync(request, ct);
logger.LogInformation("{Method} {Url} -> {Status} ({Duration}ms)",
request.Method, request.Url,
response.StatusCode, sw.ElapsedMilliseconds);
return response;
}
}
// [step] Retry decorator adds resilience
public class RetryHttpClient(
IHttpClient inner, int maxRetries,
ILogger logger) : IHttpClient
{
public async Task<HttpResponse> SendAsync(
HttpRequest request, CancellationToken ct = default)
{
for (var attempt = 0; attempt <= maxRetries; attempt++)
{
var response = await inner.SendAsync(request, ct);
if (response.StatusCode < 500) return response;
if (attempt < maxRetries)
{
var delay = TimeSpan.FromMilliseconds(
Math.Pow(2, attempt) * 100);
logger.LogWarning(
"Retry {Attempt}/{Max} for {Url} after {Delay}ms",
attempt + 1, maxRetries, request.Url,
delay.TotalMilliseconds);
await Task.Delay(delay, ct);
}
}
return await inner.SendAsync(request, ct);
}
}
// [step] Caching decorator adds response caching
public class CachingHttpClient(
IHttpClient inner, TimeSpan ttl) : IHttpClient
{
private readonly Dictionary<string, (HttpResponse Response, DateTime Expiry)>
_cache = [];
public async Task<HttpResponse> SendAsync(
HttpRequest request, CancellationToken ct = default)
{
if (request.Method == "GET")
{
var key = request.Url;
if (_cache.TryGetValue(key, out var entry)
&& entry.Expiry > DateTime.UtcNow)
return entry.Response;
var response = await inner.SendAsync(request, ct);
_cache[key] = (response, DateTime.UtcNow + ttl);
return response;
}
return await inner.SendAsync(request, ct);
}
}
// Usage:
// IHttpClient client = new BasicHttpClient();
// client = new RetryHttpClient(client, 3, logger);
// client = new CachingHttpClient(client, TimeSpan.FromMinutes(5));
// client = new LoggingHttpClient(client, logger);Decorator Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Decorator Pattern in the Real World
“Think of adding espresso shots and syrups to a coffee order. A plain coffee is the base component. Each addition—an espresso shot, vanilla syrup, oat milk—is a decorator that wraps the previous cup, adding its own cost and flavor. You can combine them in any order without the café needing a separate menu item for every combination.”