Back to blog

Decorator Design Pattern - Adding Telemetry to HttpClient

January 12, 202512 min read

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
  1. Mastering Git Repository Organization
  2. CancellationToken for Async Programming
  3. Git Flow Rethink: Reevaluating Continuous in CI/CD
  4. Understanding System Cache: A Comprehensive Guide
  5. Guide to Redis Local Instance Setup
  6. Fire and Forget for Enhanced Performance
  7. Building Resilient .NET Applications with Polly
  8. The Singleton Advantage: Managing Configurations in .NET
  9. Troubleshooting and Rebuilding My JS-Dev-Env Project
  10. Decorator Design Pattern - Adding Telemetry to HttpClient
  11. Generate Wiki Documentation from Your Code Repository
  12. TaskListProcessor - Enterprise Async Orchestration for .NET
  13. Architecting Agentic Services in .NET 9: Semantic Kernel
  14. NuGet Packages: Benefits and Challenges
  15. My Journey as a NuGet Gallery Developer and Educator
  16. Harnessing the Power of Caching in ASP.NET
  17. The Building of React-native-web-start
  18. TailwindSpark: Ignite Your Web Development
  19. Creating a PHP Website with ChatGPT
  20. Evolving PHP Development
  21. Modernizing Client Libraries in a .NET 4.8 Framework Application
  22. Building Git Spark: My First npm Package Journey
  23. 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
  • IHttpClientFactory integration
  • 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.HttpClientUtility

Single-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:

PackageSizePurpose
WebSpark.HttpClientUtility163 KBCore HTTP utilities, telemetry, resilience, caching
WebSpark.HttpClientUtility.Crawler75 KBWeb 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

Further Reading