Decorator Design Pattern

Adding Telemetry to HttpClient in ASP.NET Core

Master the Decorator Pattern to enhance HttpClient functionality with telemetry, logging, and caching capabilities while maintaining clean, maintainable code architecture.

Introduction to the Decorator Pattern .alert.alert-info.mb-4
Pattern Overview

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 implementation of enhanced functionality.

Live Demo

The current demonstration is hosted on my WebSpark site:

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's 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.

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 platform-independent implementation.

Offers consistent functionality across platforms while leveraging platform-specific optimizations.

Telemetry Importance

Telemetry involves collecting, transmitting, and analyzing application performance data. It's crucial for monitoring performance, identifying issues, and understanding usage patterns.

Interface Design

The decorator pattern relies on well-defined interfaces that both concrete and decorator classes implement. Our implementation uses two key interfaces:

IHttpGetCallService

Defines methods for making HTTP requests and returning response data using generics.

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; }
}
Generic Type Benefits

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: concrete and decorator implementations that work together seamlessly.

Concrete Implementation

The concrete implementation provides core functionality - the original object being decorated with basic HTTP request capabilities.

Key Features:
  • Error handling with null checks
  • JSON deserialization with fallback
  • HttpClient factory integration
  • Comprehensive logging
public async Task&lt;HttpGetCallResults&lt;T&gt;&gt; GetAsync&lt;T&gt;(
  HttpGetCallResults&lt;T&gt; 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&lt;T&gt;(content);
  }
  catch (Exception ex)
  {
    _logger.LogCritical(ex.Message);
    callResults.ErrorMessage = ex.Message;
  }

  return callResults;
}

Decorator Implementation

The decorator wraps the concrete implementation, adding telemetry functionality while maintaining the same interface.

Telemetry Features:
  • Performance timing with Stopwatch
  • Completion timestamp tracking
  • Exception handling and logging
  • Transparent call forwarding
public async Task&lt;HttpGetCallResults&lt;T&gt;&gt; GetAsync&lt;T&gt;(
  HttpGetCallResults&lt;T&gt; callResults)
{
  Stopwatch sw = new();
  sw.Start();

  var response = new HttpGetCallResults&lt;T&gt;(callResults);

  try
  {
    response = await _service.GetAsync&lt;T&gt;(callResults);
  }
  catch (Exception ex)
  {
    _logger.LogCritical("Telemetry:GetAsync:Exception",
      ex.Message);
  }

  sw.Stop();
  response.ElapsedMilliseconds = sw.ElapsedMilliseconds;
  response.CompletionDate = DateTime.Now;

  return response;
}

Integration & Configuration

Configure the decorator pattern in your Program.cs file using dependency injection to seamlessly integrate telemetry capabilities.

Dependency Injection Setup
// Configure HttpClient with timeout and headers
builder.Services.AddHttpClient("webspark.markhazleton.com",
  client =&gt;
{
  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&lt;IHttpGetCallService&gt;(
  serviceProvider =&gt;
{
  var logger = serviceProvider
    .GetRequiredService&lt;ILogger&lt;HttpGetCallService&gt;&gt;();
  var telemetryLogger = serviceProvider
    .GetRequiredService&lt;ILogger&lt;HttpGetCallServiceTelemetry&gt;&gt;();
  var httpClientFactory = serviceProvider
    .GetRequiredService&lt;IHttpClientFactory&gt;();

  IHttpGetCallService baseService =
    new HttpGetCallService(logger, httpClientFactory);
  IHttpGetCallService telemetryService =
    new HttpGetCallServiceTelemetry(telemetryLogger, baseService);

  return telemetryService;
});
Razor Page Implementation

Example implementation using the decorated service in a Razor Page to fetch jokes from an API:

public class JokeModel : PageModel
{
  private readonly ILogger&lt;JokeModel&gt; _logger;
  private readonly IHttpGetCallService _service;

  public HttpGetCallResults&lt;Joke&gt; JokeResult { get; set; }
  public Joke TheJoke { get; set; } = new Joke();

  public JokeModel(ILogger&lt;JokeModel&gt; logger,
    IHttpGetCallService getCallService)
  {
    _logger = logger;
    _service = getCallService;
  }

  public async Task OnGet(CancellationToken ct = default)
  {
    JokeResult = new HttpGetCallResults&lt;Joke&gt;
    {
      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

Debugging & Auditing

Automatically log all HTTP requests and responses including headers, status codes, and payloads.

  • Request/response logging
  • Header inspection
  • Status code tracking
  • Payload monitoring
Caching Decorator

Performance Optimization

Improve performance by caching responses and serving from cache when possible.

  • Response caching
  • Cache invalidation
  • TTL management
  • Memory optimization
Retry Decorator

Resilience & Reliability

Automatically retry failed requests using Polly library with configurable policies.

  • Exponential backoff
  • Circuit breaker
  • Timeout handling
  • Policy configuration
Decorator Composition

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.

Source Code & Demo

Complete source code and working examples are available on GitHub:

Conclusion

Pattern Benefits
  • Dynamic behavior addition
  • No sub-classing required
  • Flexible object composition
  • Single responsibility principle
  • Open/closed principle adherence
Implementation Advantages
  • Clean separation of concerns
  • Easy to test and maintain
  • Interchangeable components
  • Enhanced functionality without modification
  • Scalable architecture

The Decorator Design Pattern provides a powerful and flexible way to enhance object functionality dynamically while maintaining clean, maintainable code architecture.

Key Takeaways

By implementing the decorator pattern with HttpClient, we've 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