.NET
...see more

To test HttpClient handlers effectively, you need to inspect the internal handler chain that .NET builds at runtime. Since this chain is stored in a private field, reflection is the only reliable method to access it. The approach is safe, does not modify production code, and gives you full visibility into the pipeline.

The process begins by resolving your service from the DI container. If your service stores the HttpClient in a protected field, you can access it using reflection:

var field = typeof(MyClient)
    .GetField("_httpClient", BindingFlags.Instance | BindingFlags.NonPublic);

var httpClient = (HttpClient)field.GetValue(serviceInstance);

Next, retrieve the private _handler field from HttpMessageInvoker:

var handlerField = typeof(HttpMessageInvoker)
    .GetField("_handler", BindingFlags.Instance | BindingFlags.NonPublic);

var current = handlerField.GetValue(httpClient);

Finally, walk through the entire handler chain:

var handlers = new List<DelegatingHandler>();

while (current is DelegatingHandler delegating)
{
    handlers.Add(delegating);
    current = delegating.InnerHandler;
}

With this list, you can assert the presence of your custom handlers:

Assert.Contains(handlers, h => h is HttpRetryHandler);
Assert.Contains(handlers, h => h is HttpLogHandler);

This gives your test real confidence that the HttpClient pipeline is constructed correctly—exactly as it will run in production.

...see more

Issue

Libraries often expose many raw exceptions, depending on how internal HTTP or retry logic is implemented. This forces library consumers to guess which exceptions to catch and creates unstable behavior.

Cause

Exception strategy is not treated as part of the library’s public contract. Internal exceptions leak out, and any change in handlers or retry logic changes what callers experience.

Resolution

Define a clear exception boundary:

  1. Internally
    Catch relevant exceptions (HttpRequestException, timeout exceptions, retry exceptions).

  2. Log them
    Use the unified logging method.

  3. Expose only a custom exception
    Throw a single exception type, such as ServiceClientException, at the public boundary.

Code Example

catch (Exception ex)
{
    LogServiceException(ex);
    throw new ServiceClientException("Service request failed.", ex);
}

This approach creates a predictable public API, hides implementation details, and ensures your library remains stable even as the internal HTTP pipeline evolves.

...see more

The main reason regular tests cannot inspect HttpClient handlers is simple: the pipeline is private. The HttpClient instance created by IHttpClientFactory stores its entire message-handler chain inside a non-public field named _handler on its base class HttpMessageInvoker.

This means:

  • there is no public property to read the handler list
  • DI registration only confirms setup, not actual construction
  • mocks cannot expose the real pipeline
  • even typed clients hide the underlying handler chain

So while Visual Studio’s debugger can show the handler sequence, your code cannot. This is why common testing approaches fail: they operate at the service level, not the internal pipeline level.

A service class typically stores a protected or private HttpClient instance:

 
protected readonly HttpClient _httpClient;

Even if your test resolves this service, the handler pipeline remains invisible.

To validate the runtime configuration—exactly as it will behave in production—you must inspect the pipeline directly. Since .NET does not expose it, the only practical method is to use reflection. The next Snipp explains how to implement this in a clean and repeatable way.

...see more

Issue

HTTP calls fail for many reasons: timeouts, throttling, network issues, or retry exhaustion. Logging only one exception type results in missing or inconsistent diagnostic information.

Cause

Most implementations log only HttpRequestException, ignoring other relevant exceptions like retry errors or cancellation events. Over time, this makes troubleshooting difficult and logs incomplete.

Resolution

Use a single unified logging method that handles all relevant exception types. Apply specific messages for each category while keeping the logic in one place.

private void LogServiceException(Exception ex)
{
    switch (ex)
    {
        case HttpRequestException httpEx:
            LogHttpRequestException(httpEx);
            break;

        case RetryException retryEx:
            _logger.LogError("Retry exhausted. Last status: {Status}. Exception: {Ex}",
                retryEx.StatusCode, retryEx);
            break;

        case TaskCanceledException:
            _logger.LogError("Request timed out. Exception: {Ex}", ex);
            break;

        case OperationCanceledException:
            _logger.LogError("Operation was cancelled. Exception: {Ex}", ex);
            break;

        default:
            _logger.LogError("Unexpected error occurred. Exception: {Ex}", ex);
            break;
    }
}

private void LogHttpRequestException(HttpRequestException ex)
{
    if (ex.StatusCode == HttpStatusCode.NotFound)
        _logger.LogError("Resource not found. Exception: {Ex}", ex);
    else if (ex.StatusCode == HttpStatusCode.TooManyRequests)
        _logger.LogError("Request throttled. Exception: {Ex}", ex);
    else
        _logger.LogError("HTTP request failed ({Status}). Exception: {Ex}",
            ex.StatusCode, ex);
}

Centralizing logic ensures consistent, clear, and maintainable logging across all error paths.

...see more

When configuring HttpClient using AddHttpClient(), developers often attach important features using message handlers. These handlers form a step-by-step pipeline that processes outgoing requests. Examples include retry logic, request logging, or authentication.

The problem appears when you want to test that the correct handlers are attached. It is common to write integration tests that resolve your service from the DI container, call methods, and inspect behavior. But this does not confirm whether the handler chain is correct.

A handler can silently fail to attach due to a typo, incorrect registration, or a missing service. You may have code like this:

services.AddHttpClient("ClientService")
    .AddHttpMessageHandler<HttpRetryHandler>()
    .AddHttpMessageHandler<HttpLogHandler>();

But you cannot verify from your test that the constructed pipeline includes these handlers. Even worse, Visual Studio can display the handler chain in the debugger, but this ability is not accessible through public APIs.

Without a direct way to look inside the pipeline, teams cannot automatically verify one of the most important parts of their application’s networking stack. The next Snipp explains why this limitation exists.

...see more

Creating reliable HTTP client services is a challenge for many .NET developers. Network timeouts, throttling, retries, and unexpected exceptions often lead to inconsistent logging, unclear error messages, and unstable public APIs. This Snipp gives an overview of how to design a clean, predictable, and well-structured error-handling strategy for your HTTP-based services.

Readers will learn why custom exceptions matter, how to log different failure types correctly, and how to build a stable exception boundary that hides internal details from users of a library. Each child Snipp focuses on one topic and includes practical examples. Together, they offer a clear blueprint for building services that are easier to debug, test, and maintain.

The overall goal is simple: Create a .NET service that logs clearly, behaves consistently, and protects callers from internal complexity.

...see more

When registering services, you must decide how long they should live in your application:

  • Singleton: One instance for the entire application. Best for stateless or shared services.
  • Scoped: One instance per request or operation. Best for services tied to a unit of work.
  • Transient: A new instance every time it’s requested. Best for lightweight and short-lived services.

General advice:

  • Use Singleton if the service is stateless or used in background tasks.
  • Use Scoped if it relies on request-specific data (like database contexts).
  • Use Transient if it should always be fresh and independent.

Choosing the right lifetime prevents resource leaks, avoids threading issues, and makes your application more reliable.

...see more

When building background services in .NET, it’s helpful to include structured logging for better traceability and diagnostics. One common pattern is using logging scopes to include context like the service or task name in each log entry.

Instead of manually providing this context everywhere, you can simplify the process by automatically extracting it based on the class and namespace — making the code cleaner and easier to maintain.


✅ Goal

Replace this verbose pattern:

_logger.BeginScope(LoggingScopeHelper.CreateScope("FeatureName", "WorkerName"));

With a simple, reusable version:

_logger.BeginWorkerScope();

Implementation

🔧 1. Logger Extension

public static class LoggerExtensions
{
    public static IDisposable BeginWorkerScope(this ILogger logger)
    {
        var scopeData = LoggingScopeHelper.CreateScope();
        return logger.BeginScope(scopeData);
    }
}

🧠 2. Logging Scope Helper

public static class LoggingScopeHelper
{
    public static Dictionary<string, object> CreateScope()
    {
        var stackTrace = new System.Diagnostics.StackTrace();
        var frames = stackTrace.GetFrames();

        string featureName = "Unknown";
        string workerName = "Unknown";

        foreach (var frame in frames ?? Array.Empty<StackFrame>())
        {
            var method = frame.GetMethod();
            var type = method?.DeclaringType;

            if (type == null || type.Name.StartsWith("<") || type.Namespace?.StartsWith("System") == true)
                continue;

            workerName = type.Name;

            var ns = type.Namespace;
            var segments = ns?.Split('.');
            if (segments?.Length >= 3)
            {
                featureName = segments[2]; // Assuming format: Company.App.Feature.Workers
            }

            break;
        }

        return new Dictionary<string, object>
        {
            ["Feature"] = featureName,
            ["Worker"] = workerName
        };
    }
}

Example Usage in a Worker

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    using (_logger.BeginWorkerScope())
    {
        // Your background task logic
    }
}

This ensures that every log written within the scope will automatically include "Feature" and "Worker" values, without manually specifying them.

...see more

Combining base URLs with relative paths in .NET can lead to errors if not handled carefully. This guide shows a safe and reusable way to do it using two methods: one that works with Uri objects and another that accepts strings for convenience.

Why This Is Useful

  • Prevents runtime errors from invalid or missing inputs
  • Provides fallback behavior
  • Easy to reuse in any .NET application

Implementation

Here's the core method that safely combines a Uri with a relative path:

public static string SafeCombineUrl(Uri? baseUri, string relativePath)
{
    try
    {
        if (string.IsNullOrWhiteSpace(relativePath))
        {
            return baseUri?.ToString() ?? string.Empty;
        }

        if (baseUri == null)
        {
            return relativePath;
        }

        return new Uri(baseUri, relativePath).ToString();
    }
    catch (Exception ex)
    {
        return $"[InvalidUrl={ex.Message}]";
    }
}

To make it easier to use with strings, add this helper method:

public static string SafeCombineUrl(string baseUri, string relativePath)
{
    try
    {
        Uri? baseUriObj = null;

        if (!string.IsNullOrWhiteSpace(baseUri))
        {
            baseUriObj = new Uri(baseUri, UriKind.Absolute);
        }

        return SafeCombineUrl(baseUriObj, relativePath);
    }
    catch (Exception ex)
    {
        return $"[InvalidBaseUri={ex.Message}]";
    }
}

How It Works

  • If the relativePath is empty, it returns the base URL.
  • If the base is missing, it returns the relative path.
  • If both are valid, it combines them.
  • If there's an error, it returns a helpful message.
...see more

When working with JsonElement in C#, calling methods like TryGetProperty on a default or uninitialized JsonElement can cause runtime exceptions. This usually happens when the JsonElement has not been properly assigned a value.

To avoid this issue, always check whether the JsonElement is valid before accessing its properties. A safe way to do this is by checking its ValueKind.

Here’s a safer extension method that returns a string property only if it exists and the element is valid:

public static string? GetStringProperty(this JsonElement element, string propertyName)
{
    if (element.ValueKind == JsonValueKind.Undefined)
        return null;

    return element.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.String
        ? prop.GetString()
        : null;
}

This ensures that your code won’t throw an InvalidOperationException when the JsonElement is default.

Use this method when reading from JSON documents where property existence isn’t guaranteed.

Add to Set
  • .NET
  • Agile
  • AI
  • ASP.NET Core
  • Azure
  • C#
  • Cloud Computing
  • CSS
  • EF Core
  • HTML
  • JavaScript
  • Microsoft Entra
  • PowerShell
  • Quotes
  • React
  • Security
  • Software Development
  • SQL
  • Technology
  • Testing
  • Visual Studio
  • Windows
Actions
 
Sets