In the world of web development, interacting with external APIs is a common task. However, debugging and understanding the communication between your application and these APIs can sometimes be challenging. In this blog post, we will explore a practical approach to enhance the logging capabilities of the HttpClient in an ASP.NET Core application.

1-Implementing LogEnabledHttpClient:

We begin describing the code by an extension method named LogEnabledHttpClient. The provided code includes an extension method AddLogEnabledHttpClient for the IServiceCollection. This method configures an HttpClient to log information about each request and response using a custom LoggingDelegatingHandler. Let's break down the key components.

The provided algorithm is an extension method in C# that extends the functionality of IServiceCollection for configuring an HttpClient with logging capabilities. It adds a transient service for a custom logging delegating handler, which intercepts HTTP requests and responses for logging purposes. The method then configures an HttpClient named "LogEnabledClient" and adds the previously registered logging delegating handler to it. Additionally, it conditionally registers a logger setting (ConsoleLoggerSetting for DEBUG mode and ApplicationInsightsLoggerSetting for other modes) as a singleton. Finally, it retrieves the appropriate logger setting based on the conditional registration and applies its settings to the IServiceCollection before returning it. The algorithm aims to facilitate the configuration of an HttpClient with logging capabilities, allowing flexibility in choosing different logging settings based on the application's mode.

public static class ConfigureLogEnabledHttpClient
{
  public static IServiceCollection AddLogEnabledHttpClient
    (this IServiceCollection serviceCollection)
  {
    serviceCollection.AddTransient<LoggingDelegatingHandler>();
    serviceCollection.AddHttpClient("LogEnabledClient")
      .AddHttpMessageHandler<LoggingDelegatingHandler>();

    #if DEBUG
    {
      serviceCollection.AddSingleton<ILoggerSetting,
        ConsoleLoggerSetting>();
    }
    #else
    {
      serviceCollection.AddSingleton<ILoggerSetting,
        ApplicationInsightsLoggerSetting>();
    }       
    #endif

    var loggerSetting = serviceCollection.BuildServiceProvider()
      .GetRequiredService<ILoggerSetting>();

    loggerSetting.ApplySettings(serviceCollection);

    return serviceCollection;
  }
}

2-LoggingDelegatingHandler:

In C#, a DelegatingHandler is a class that can be used in the context of the HttpClient class to provide a flexible way to process HTTP requests and responses. It is part of the System.Net.Http namespace and is commonly used in scenarios where you need to perform some logic before or after an HTTP request is sent or after an HTTP response is received. The DelegatingHandler class is an abstract class that you can derive from to create your own custom handlers. When you create a handler that inherits from DelegatingHandler, you can override the SendAsync method to intercept and modify the HTTP request and response messages.

The LoggingDelegatingHandler is a class that extends DelegatingHandler, allowing us to intercept HTTP requests and responses. In its SendAsync method, it captures details such as the request and response messages, their content, and then logs this information using an injected logger.

internal sealed class LoggingDelegatingHandler : DelegatingHandler
{
  private readonly ILogger<LoggingDelegatingHandler> _logger;

  public LoggingDelegatingHandler(ILogger<LoggingDelegatingHandler> logger)
  {
    _logger = logger;
  }

  protected override async Task<HttpResponseMessage> SendAsync
    (HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken)
  {
    var request = httpRequestMessage.ToString();
    var requestContent = string.Empty;
    if (httpRequestMessage.Content != null)
    {
      requestContent =
        await httpRequestMessage.Content.ReadAsStringAsync();
    }

    var httpResponseMessage = await base.SendAsync
      (httpRequestMessage, cancellationToken);

    var response = httpResponseMessage.ToString();
    var responseContent = string.Empty;
    if (httpResponseMessage.Content != null)
    {
      responseContent =
        await httpResponseMessage.Content.ReadAsStringAsync();
    }

    var logBody =
      $"\nHttp Request Information:\n" +
      $"{request}\n\n" +
      $"Http Request Content Information:\n" +
      $"{requestContent}\n\n" +
      $"Http Response Information:\n" +
      $"{response}\n\n" +
      $"Http Response Content Information:\n" +
      $"{responseContent}\n\n";

    _logger.LogInformation(logBody);

    return httpResponseMessage;
  }
}

3-ILoggerSetting:

To provide flexibility in logging configuration, the code introduces an interface ILoggerSetting with two implementations: ConsoleLoggerSetting and ApplicationInsightsLoggerSetting. These classes configure the logging providers based on the application's environment. The former sends log data to the console window, while the latter sends the log data to Azure Application Insights.

internal interface ILoggerSetting
{
  void ApplySettings(IServiceCollection serviceCollection);
}

internal sealed class ConsoleLoggerSetting : ILoggerSetting
{
  public void ApplySettings(IServiceCollection serviceCollection)
  {
    serviceCollection.AddLogging((loggingBuilder) =>
    {
      loggingBuilder.ClearProviders();
      loggingBuilder.AddConsole();
    });
  }
}
internal sealed class ConsoleLoggerSetting : ILoggerSetting
{
  public void ApplySettings(IServiceCollection serviceCollection)
  {
    serviceCollection.AddLogging((loggingBuilder) =>
    {
      loggingBuilder.ClearProviders();
      loggingBuilder.AddConsole();
    });
  }
}
internal sealed class ApplicationInsightsLoggerSetting : ILoggerSetting
{
  public void ApplySettings(IServiceCollection serviceCollection)
  {
    serviceCollection.AddLogging((loggingBuilder) =>
    {
      loggingBuilder.ClearProviders();
      loggingBuilder.AddConsole();
      loggingBuilder.AddAzureWebAppDiagnostics();
    });

    serviceCollection.Configure<AzureFileLoggerOptions>(options =>
    {
      options.FileName = "your-project-name-diagnostics-";
      options.FileSizeLimit = 50 * 1024;
      options.RetainedFileCountLimit = 5;
    });
  }
}

4-Usage in HomeController:

The HomeController class showcases how to use the log-enabled HttpClient within a controller. By injecting IHttpClientFactory, an instance of HttpClient configured with logging is created using the CreateLogEnabledClient extension method.

public static class HttpClientFactoryExtensions
{
  public static HttpClient CreateLogEnabledClient
    (this IHttpClientFactory serviceCollection)
  {
    return serviceCollection.CreateClient("LogEnabledClient");
  }
}
public class HomeController : Controller
{
  private readonly IHttpClientFactory _httpClientFactory;

  public HomeController(IHttpClientFactory httpClientFactory)
  {
    _httpClientFactory = httpClientFactory;
  }

  public async Task<IActionResult> Index()
  {
    using var HttpClient = _httpClientFactory.CreateLogEnabledClient();

    var response = await HttpClient.GetAsync("https://www.google.com");

    if (response.IsSuccessStatusCode)
    {
      string content = await response.Content.ReadAsStringAsync();
      Console.WriteLine($"Response from Google:\n{content}");
    }
    else
    {
      Console.WriteLine($"Request failed with status code: {response.StatusCode}");
    }


    return View();
  }    
}

5-Integrating in Program.cs:

In the Program class, the AddLogEnabledHttpClient extension method is added during the service registration phase.

public class Program
{
  public static void Main(string[] args)
  {
    var builder = WebApplication.CreateBuilder(args);
    .
    .
    builder.Services.AddHttpClient();
    builder.Services.AddLogEnabledHttpClient();
    .
    .
    app.Run();
  }
}