Decorator Design Pattern - Adding Telemetry to HttpClient
Master the Decorator Pattern to enhance HttpClient functionality with telemetry, logging, and caching capabilities while maintaining clean, maintainable code architecture in ASP.NET Core.
Development Series — 23 articles
- Mastering Git Repository Organization
- CancellationToken for Async Programming
- Git Flow Rethink: Reevaluating Continuous in CI/CD
- Understanding System Cache: A Comprehensive Guide
- Guide to Redis Local Instance Setup
- Fire and Forget for Enhanced Performance
- Building Resilient .NET Applications with Polly
- The Singleton Advantage: Managing Configurations in .NET
- Troubleshooting and Rebuilding My JS-Dev-Env Project
- Decorator Design Pattern - Adding Telemetry to HttpClient
- Generate Wiki Documentation from Your Code Repository
- TaskListProcessor - Enterprise Async Orchestration for .NET
- Architecting Agentic Services in .NET 9: Semantic Kernel
- NuGet Packages: Benefits and Challenges
- My Journey as a NuGet Gallery Developer and Educator
- Harnessing the Power of Caching in ASP.NET
- The Building of React-native-web-start
- TailwindSpark: Ignite Your Web Development
- Creating a PHP Website with ChatGPT
- Evolving PHP Development
- Modernizing Client Libraries in a .NET 4.8 Framework Application
- Building Git Spark: My First npm Package Journey
- Dave's Top Ten: Git Stats You Should Never Track
Introduction to the Decorator Pattern
The Decorator Design Pattern is a structural design pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting other objects from the same class.
The Decorator Pattern provides a flexible alternative to sub-classing by dynamically wrapping objects in decorator objects that add new functionality while preserving the original interface.
This pattern works by creating a base class/interface that defines the basic behavior and then defining decorator classes that add additional behavior. Each decorator wraps an instance of the base interface and provides its own enhanced implementation.
A live demonstration is hosted at AsyncSpark on WebSpark.
Core Concepts & Definitions
Design Patterns
Design patterns are proven solutions to common software design problems, discovered and refined by experienced developers over time. First documented by the "Gang of Four" in 1994, these patterns enable more maintainable, reusable, and scalable code.
HTTP Protocol
Hypertext Transfer Protocol (HTTP) is the foundation of data communication for the World Wide Web. It is a request-response protocol where clients send requests to servers and receive responses containing requested information.
HTTP Client
An HTTP client is a software component that allows programs to send HTTP requests and receive responses. It is essential for interacting with web-based applications and APIs using the HTTP protocol.
HttpClient Class
The .NET HttpClient class provides a high-level API for making HTTP requests with a platform-independent implementation. It offers consistent functionality across platforms while leveraging platform-specific optimizations.
Telemetry
Telemetry involves collecting, transmitting, and analyzing application performance data. It is crucial for monitoring performance, identifying issues, and understanding usage patterns in production systems.
Interface Design
The decorator pattern relies on well-defined interfaces that both concrete and decorator classes implement. This implementation uses two key interfaces.
IHttpGetCallService
Defines methods for making HTTP requests and returning strongly-typed response data:
public interface IHttpGetCallService
{
Task<HttpGetCallResults<T>> GetAsync<T>(
HttpGetCallResults<T> callResults
);
}IHttpGetCallResults
Defines properties for HTTP call response data including timing, errors, and results:
public interface IHttpGetCallResults<T>
{
DateTime? CompletionDate { get; set; }
long ElapsedMilliseconds { get; set; }
string? ErrorMessage { get; set; }
int Id { get; set; }
int Iteration { get; set; }
string RequestPath { get; set; }
T? ResponseResults { get; set; }
int Retries { get; set; }
}The HttpGetCallResults<T> generic implementation provides compile-time type checking, improved IntelliSense, and strongly-typed response handling, making code cleaner and more maintainable.
Service Implementations
The Decorator Pattern involves two implementation types: a concrete implementation and one or more decorator implementations that wrap it.
Concrete Implementation
The concrete implementation provides core functionality — the original object being decorated — with basic HTTP request capabilities:
public async Task<HttpGetCallResults<T>> GetAsync<T>(
HttpGetCallResults<T> callResults)
{
if (callResults == null)
{
throw new ArgumentNullException(nameof(callResults));
}
try
{
using var httpClient = _clientFactory.CreateClient();
var request = new HttpRequestMessage(
HttpMethod.Get, callResults.RequestPath);
var response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
callResults.ResponseResults =
JsonSerializer.Deserialize<T>(content);
}
catch (Exception ex)
{
_logger.LogCritical(ex.Message);
callResults.ErrorMessage = ex.Message;
}
return callResults;
}Key features of the concrete implementation:
- Error handling with null checks
- JSON deserialization with fallback
IHttpClientFactoryintegration- Comprehensive logging
Decorator Implementation
The decorator wraps the concrete implementation, adding telemetry functionality while maintaining the same interface:
public async Task<HttpGetCallResults<T>> GetAsync<T>(
HttpGetCallResults<T> callResults)
{
Stopwatch sw = new();
sw.Start();
var response = new HttpGetCallResults<T>(callResults);
try
{
response = await _service.GetAsync<T>(callResults);
}
catch (Exception ex)
{
_logger.LogCritical("Telemetry:GetAsync:Exception", ex.Message);
}
sw.Stop();
response.ElapsedMilliseconds = sw.ElapsedMilliseconds;
response.CompletionDate = DateTime.Now;
return response;
}Telemetry features added by the decorator:
- Performance timing with
Stopwatch - Completion timestamp tracking
- Exception handling and logging
- Transparent call forwarding to the wrapped service
Integration & Configuration
Configure the decorator pattern in Program.cs using dependency injection to seamlessly integrate telemetry capabilities.
Dependency Injection Setup
// Configure HttpClient with timeout and headers
builder.Services.AddHttpClient("webspark.markhazleton.com",
client =>
{
client.Timeout = TimeSpan.FromMilliseconds(1500);
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("User-Agent",
"webspark.markhazleton.com");
client.DefaultRequestHeaders.Add("X-Request-ID",
Guid.NewGuid().ToString());
});
// Register decorator pattern with telemetry wrapper
builder.Services.AddSingleton<IHttpGetCallService>(
serviceProvider =>
{
var logger = serviceProvider
.GetRequiredService<ILogger<HttpGetCallService>>();
var telemetryLogger = serviceProvider
.GetRequiredService<ILogger<HttpGetCallServiceTelemetry>>();
var httpClientFactory = serviceProvider
.GetRequiredService<IHttpClientFactory>();
IHttpGetCallService baseService =
new HttpGetCallService(logger, httpClientFactory);
IHttpGetCallService telemetryService =
new HttpGetCallServiceTelemetry(telemetryLogger, baseService);
return telemetryService;
});Razor Page Implementation
Example using the decorated service in a Razor Page to fetch jokes from an external API:
public class JokeModel : PageModel
{
private readonly ILogger<JokeModel> _logger;
private readonly IHttpGetCallService _service;
public HttpGetCallResults<Joke> JokeResult { get; set; }
public Joke TheJoke { get; set; } = new Joke();
public JokeModel(ILogger<JokeModel> logger,
IHttpGetCallService getCallService)
{
_logger = logger;
_service = getCallService;
}
public async Task OnGet(CancellationToken ct = default)
{
JokeResult = new HttpGetCallResults<Joke>
{
RequestPath = "https://v2.jokeapi.dev/joke/Any?safe-mode"
};
JokeResult = await _service.GetAsync(JokeResult, ct);
TheJoke = JokeResult?.ResponseResults ?? new Joke
{
error = true
};
}
}Additional Use Cases & Extensions
The decorator pattern's flexibility allows for multiple enhancements to HttpClient functionality beyond telemetry.
Logging Decorator
Automatically log all HTTP requests and responses for debugging and auditing:
- Request/response logging
- Header inspection
- Status code tracking
- Payload monitoring
Caching Decorator
Improve performance by caching responses and serving from cache when possible:
- Response caching
- Cache invalidation
- TTL management
- Memory optimization
Retry Decorator
Automatically retry failed requests using Polly library with configurable policies:
- Exponential backoff
- Circuit breaker
- Timeout handling
- Policy configuration
These decorators can be combined and layered to create a powerful, feature-rich HTTP client with telemetry, logging, caching, and retry capabilities all working together seamlessly.
WebSpark.HttpClientUtility NuGet Package
The decorator pattern work described in this article evolved into a published NuGet package: WebSpark.HttpClientUtility. See the project page for full details.
The package is a drop-in HttpClient wrapper for .NET 8–10 that bundles the decorator layers demonstrated here — telemetry, retry, caching, and logging — into a single library. One registration call replaces 50+ lines of manual DI setup.
Installation
dotnet add package WebSpark.HttpClientUtilitySingle-Line Registration
builder.Services.AddHttpClientUtility();Key Features
- Intuitive Request/Response Model: Type-safe interactions with
HttpGetCallResults<T> - Built-in Resilience: Integrated Polly retry and circuit breaker patterns
- Response Caching: Configurable in-memory caching for API responses
- Correlation Tracking: Automatic correlation ID generation for distributed tracing
- Structured Logging: Rich contextual logging throughout the request lifecycle
- OpenTelemetry Support: Full observability integration for monitoring
- Trimming & AOT Ready: Compatible with Native AOT and IL trimming
- Source Link: Full debugging support into library source
Package Architecture
Starting with v2.0, the library is split into two focused packages:
| Package | Size | Purpose |
|---|---|---|
WebSpark.HttpClientUtility | 163 KB | Core HTTP utilities, telemetry, resilience, caching |
WebSpark.HttpClientUtility.Crawler | 75 KB | Web crawling, sitemap generation, robots.txt compliance |
The crawler package extends the core with website crawling (depth control), XML sitemap generation, HTML parsing via HtmlAgilityPack, real-time progress via SignalR, and CSV export.
Supported Frameworks
Targets .NET 8.0, 9.0, and 10.0 LTS. The package ships with 237+ unit tests — 711 test runs across all three frameworks — with a 100% pass rate. Zero-warning builds and baseline package validation prevent breaking changes.
This package is the direct result of applying the decorator pattern at scale. Each capability — telemetry, retry, caching — is its own composable decorator layer, exactly as described in this article.
Source Code
Complete source code and working examples are available on GitHub:
Conclusion
Pattern Benefits
- Dynamic behavior addition without subclassing
- Flexible object composition
- Single responsibility principle adherence
- Open/closed principle adherence
Implementation Advantages
- Clean separation of concerns
- Easy to test and maintain
- Interchangeable, composable components
- Enhanced functionality without modifying existing code
- Scalable architecture
By implementing the decorator pattern with HttpClient, we demonstrated how to:
- Add telemetry capabilities without modifying core functionality
- Create reusable, composable components
- Maintain clean separation between concerns
- Build scalable, maintainable applications
- Apply proven design patterns to real-world scenarios


