Decorator Pattern
Intent
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Problem
You want to add behavior to objects without:
- Modifying their code
- Creating a complex inheritance hierarchy
- Affecting other objects of the same class
Classic Implementation
// Component interface
public interface ICoffee
{
string GetDescription();
decimal GetCost();
}
// Concrete component
public class SimpleCoffee : ICoffee
{
public string GetDescription() => "Simple coffee";
public decimal GetCost() => 2.00m;
}
// Decorator base class
public abstract class CoffeeDecorator : ICoffee
{
protected readonly ICoffee _coffee;
protected CoffeeDecorator(ICoffee coffee)
{
_coffee = coffee;
}
public virtual string GetDescription() => _coffee.GetDescription();
public virtual decimal GetCost() => _coffee.GetCost();
}
// Concrete decorators
public class MilkDecorator : CoffeeDecorator
{
public MilkDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => $"{_coffee.GetDescription()}, milk";
public override decimal GetCost() => _coffee.GetCost() + 0.50m;
}
public class SugarDecorator : CoffeeDecorator
{
public SugarDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => $"{_coffee.GetDescription()}, sugar";
public override decimal GetCost() => _coffee.GetCost() + 0.25m;
}
public class WhippedCreamDecorator : CoffeeDecorator
{
public WhippedCreamDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => $"{_coffee.GetDescription()}, whipped cream";
public override decimal GetCost() => _coffee.GetCost() + 0.75m;
}
public class CaramelDecorator : CoffeeDecorator
{
public CaramelDecorator(ICoffee coffee) : base(coffee) { }
public override string GetDescription() => $"{_coffee.GetDescription()}, caramel";
public override decimal GetCost() => _coffee.GetCost() + 0.60m;
}
// Usage - Stack decorators
ICoffee coffee = new SimpleCoffee();
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");
// Simple coffee: $2.00
coffee = new MilkDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");
// Simple coffee, milk: $2.50
coffee = new SugarDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");
// Simple coffee, milk, sugar: $2.75
coffee = new WhippedCreamDecorator(coffee);
Console.WriteLine($"{coffee.GetDescription()}: ${coffee.GetCost()}");
// Simple coffee, milk, sugar, whipped cream: $3.50
// One-liner creation
ICoffee fancy = new CaramelDecorator(
new WhippedCreamDecorator(
new MilkDecorator(
new SimpleCoffee())));
Real-World Example: Repository Decorators
// Repository interface
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
// Base implementation
public class Repository<T> : IRepository<T> where T : class
{
protected readonly DbContext _context;
public Repository(DbContext context)
{
_context = context;
}
public virtual async Task<T> GetByIdAsync(int id)
=> await _context.Set<T>().FindAsync(id);
public virtual async Task<List<T>> GetAllAsync()
=> await _context.Set<T>().ToListAsync();
public virtual async Task AddAsync(T entity)
{
await _context.Set<T>().AddAsync(entity);
await _context.SaveChangesAsync();
}
public virtual async Task UpdateAsync(T entity)
{
_context.Set<T>().Update(entity);
await _context.SaveChangesAsync();
}
public virtual async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_context.Set<T>().Remove(entity);
await _context.SaveChangesAsync();
}
}
}
// Caching decorator
public class CachedRepository<T> : IRepository<T> where T : class
{
private readonly IRepository<T> _inner;
private readonly IDistributedCache _cache;
private readonly TimeSpan _expiration;
public CachedRepository(
IRepository<T> inner,
IDistributedCache cache,
TimeSpan? expiration = null)
{
_inner = inner;
_cache = cache;
_expiration = expiration ?? TimeSpan.FromMinutes(5);
}
public async Task<T> GetByIdAsync(int id)
{
var cacheKey = $"{typeof(T).Name}:{id}";
var cached = await _cache.GetStringAsync(cacheKey);
if (!string.IsNullOrEmpty(cached))
return JsonSerializer.Deserialize<T>(cached);
var entity = await _inner.GetByIdAsync(id);
if (entity != null)
{
await _cache.SetStringAsync(cacheKey,
JsonSerializer.Serialize(entity),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = _expiration
});
}
return entity;
}
public async Task<List<T>> GetAllAsync()
{
// Could cache this too with appropriate key
return await _inner.GetAllAsync();
}
public async Task AddAsync(T entity)
{
await _inner.AddAsync(entity);
// No cache invalidation needed for add
}
public async Task UpdateAsync(T entity)
{
await _inner.UpdateAsync(entity);
// Invalidate cache - implementation depends on how to get ID
}
public async Task DeleteAsync(int id)
{
await _inner.DeleteAsync(id);
var cacheKey = $"{typeof(T).Name}:{id}";
await _cache.RemoveAsync(cacheKey);
}
}
// Logging decorator
public class LoggingRepository<T> : IRepository<T> where T : class
{
private readonly IRepository<T> _inner;
private readonly ILogger<LoggingRepository<T>> _logger;
public LoggingRepository(
IRepository<T> inner,
ILogger<LoggingRepository<T>> logger)
{
_inner = inner;
_logger = logger;
}
public async Task<T> GetByIdAsync(int id)
{
_logger.LogDebug("Getting {Type} with ID {Id}", typeof(T).Name, id);
var sw = Stopwatch.StartNew();
var result = await _inner.GetByIdAsync(id);
sw.Stop();
_logger.LogDebug("Retrieved {Type} in {Elapsed}ms",
typeof(T).Name, sw.ElapsedMilliseconds);
return result;
}
public async Task<List<T>> GetAllAsync()
{
_logger.LogDebug("Getting all {Type}", typeof(T).Name);
var sw = Stopwatch.StartNew();
var result = await _inner.GetAllAsync();
sw.Stop();
_logger.LogDebug("Retrieved {Count} {Type} in {Elapsed}ms",
result.Count, typeof(T).Name, sw.ElapsedMilliseconds);
return result;
}
public async Task AddAsync(T entity)
{
_logger.LogInformation("Adding new {Type}", typeof(T).Name);
await _inner.AddAsync(entity);
_logger.LogInformation("Added {Type}", typeof(T).Name);
}
public async Task UpdateAsync(T entity)
{
_logger.LogInformation("Updating {Type}", typeof(T).Name);
await _inner.UpdateAsync(entity);
_logger.LogInformation("Updated {Type}", typeof(T).Name);
}
public async Task DeleteAsync(int id)
{
_logger.LogInformation("Deleting {Type} with ID {Id}", typeof(T).Name, id);
await _inner.DeleteAsync(id);
_logger.LogInformation("Deleted {Type} with ID {Id}", typeof(T).Name, id);
}
}
// Retry decorator
public class RetryRepository<T> : IRepository<T> where T : class
{
private readonly IRepository<T> _inner;
private readonly int _maxRetries;
private readonly TimeSpan _delay;
public RetryRepository(IRepository<T> inner, int maxRetries = 3)
{
_inner = inner;
_maxRetries = maxRetries;
_delay = TimeSpan.FromSeconds(1);
}
public async Task<T> GetByIdAsync(int id)
=> await ExecuteWithRetryAsync(() => _inner.GetByIdAsync(id));
public async Task<List<T>> GetAllAsync()
=> await ExecuteWithRetryAsync(() => _inner.GetAllAsync());
public async Task AddAsync(T entity)
=> await ExecuteWithRetryAsync(async () =>
{
await _inner.AddAsync(entity);
return true;
});
public async Task UpdateAsync(T entity)
=> await ExecuteWithRetryAsync(async () =>
{
await _inner.UpdateAsync(entity);
return true;
});
public async Task DeleteAsync(int id)
=> await ExecuteWithRetryAsync(async () =>
{
await _inner.DeleteAsync(id);
return true;
});
private async Task<TResult> ExecuteWithRetryAsync<TResult>(Func<Task<TResult>> action)
{
var attempts = 0;
while (true)
{
try
{
return await action();
}
catch (Exception) when (++attempts < _maxRetries)
{
await Task.Delay(_delay * attempts);
}
}
}
}
// Compose decorators
IRepository<Product> repository = new Repository<Product>(context);
repository = new LoggingRepository<Product>(repository, logger);
repository = new CachedRepository<Product>(repository, cache);
repository = new RetryRepository<Product>(repository);
// Now every operation is: Retry β Cache β Log β Database
DI Registration for Decorators
// Manual decoration
services.AddScoped<Repository<Product>>();
services.AddScoped<IRepository<Product>>(sp =>
{
var context = sp.GetRequiredService<DbContext>();
var cache = sp.GetRequiredService<IDistributedCache>();
var logger = sp.GetRequiredService<ILogger<LoggingRepository<Product>>>();
IRepository<Product> repo = new Repository<Product>(context);
repo = new LoggingRepository<Product>(repo, logger);
repo = new CachedRepository<Product>(repo, cache);
return repo;
});
// Or use a library like Scrutor
services.AddScoped<IRepository<Product>, Repository<Product>>();
services.Decorate<IRepository<Product>, LoggingRepository<Product>>();
services.Decorate<IRepository<Product>, CachedRepository<Product>>();
.NET Stream Decorators
.NET uses Decorator extensively with streams:
// Base stream
using var fileStream = File.OpenRead("data.txt");
// Add buffering decorator
using var bufferedStream = new BufferedStream(fileStream);
// Add compression decorator
using var gzipStream = new GZipStream(bufferedStream, CompressionMode.Compress);
// Add encryption decorator
using var cryptoStream = new CryptoStream(gzipStream, encryptor, CryptoStreamMode.Write);
// Write through all decorators
await cryptoStream.WriteAsync(data);
// Data flows: CryptoStream β GZipStream β BufferedStream β FileStream
Decorator vs Inheritance
// WITH Inheritance - Combinatorial explosion
public class Coffee { }
public class CoffeeWithMilk : Coffee { }
public class CoffeeWithSugar : Coffee { }
public class CoffeeWithMilkAndSugar : Coffee { }
public class CoffeeWithWhippedCream : Coffee { }
public class CoffeeWithMilkAndWhippedCream : Coffee { }
// ... many more combinations
// WITH Decorator - Compose at runtime
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee); // Add milk
coffee = new SugarDecorator(coffee); // Add sugar
// Any combination possible without new classes
Interview Tips
Common Questions:
- βExplain Decorator pattern with an exampleβ
- βHow is Decorator different from inheritance?β
- βWhere have you seen Decorator in .NET?β
Key Points:
- Decorator wraps objects to add behavior
- Implements same interface as wrapped object
- Decorators can be stacked in any order
- More flexible than inheritance
- Great for cross-cutting concerns (logging, caching, retry)
Decorator vs Proxy:
- Decorator: Adds functionality
- Proxy: Controls access (lazy loading, security)
.NET Examples:
Streamwrappers (BufferedStream,GZipStream,CryptoStream)HttpClientHandlerdecorators- ASP.NET Core middleware (similar concept)