< Summary

Information
Class: AsiBackbone.AspNetCore.Correlation.HttpContextGovernanceRequestCorrelationResolver
Assembly: AsiBackbone.AspNetCore
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.AspNetCore/Correlation/HttpContextGovernanceRequestCorrelationResolver.cs
Line coverage
100%
Covered lines: 56
Uncovered lines: 0
Coverable lines: 56
Total lines: 159
Line coverage: 100%
Branch coverage
89%
Covered branches: 61
Total branches: 68
Branch coverage: 89.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%66100%
ResolveRequestCorrelation()75%44100%
ResolveCorrelationId(...)100%1414100%
NormalizeClientCorrelationId(...)100%88100%
ResolveTraceId(...)100%44100%
ResolveMetadata(...)90.63%3232100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.AspNetCore/Correlation/HttpContextGovernanceRequestCorrelationResolver.cs

#LineLine coverage
 1using System.Diagnostics;
 2using AsiBackbone.AspNetCore.DependencyInjection;
 3using AsiBackbone.Core;
 4using Microsoft.AspNetCore.Http;
 5using Microsoft.AspNetCore.Routing;
 6using Microsoft.Extensions.Options;
 7using Microsoft.Extensions.Primitives;
 8
 9namespace AsiBackbone.AspNetCore.Correlation;
 10
 11/// <summary>
 12/// Resolves safe request correlation data from the current ASP.NET Core HTTP context.
 13/// </summary>
 14/// <remarks>
 15/// Client-supplied correlation identifiers are ignored by default. A host may explicitly trust configured headers when
 16/// a trusted ingress removes caller-supplied values and writes its own. Trusted values are accepted only when their
 17/// trimmed value is printable and does not exceed <see cref="GovernanceIdentifierLimits.MaximumLength" /> characters.
 18/// The server-owned <see cref="HttpContext.TraceIdentifier" /> is always retained in safe request metadata.
 19/// </remarks>
 20public sealed class HttpContextGovernanceRequestCorrelationResolver : IHttpGovernanceRequestCorrelationResolver
 21{
 22    private readonly IHttpContextAccessor httpContextAccessor;
 23    private readonly AspNetCoreGovernanceOptions options;
 24
 25    /// <summary>
 26    /// Initializes a new instance of the <see cref="HttpContextGovernanceRequestCorrelationResolver" /> class.
 27    /// </summary>
 28    /// <param name="httpContextAccessor">The ASP.NET Core HTTP context accessor.</param>
 29    /// <param name="options">The request correlation options.</param>
 4730    public HttpContextGovernanceRequestCorrelationResolver(
 4731        IHttpContextAccessor httpContextAccessor,
 4732        IOptions<AspNetCoreGovernanceOptions> options)
 33    {
 4734        this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
 4735        this.options = options?.Value ?? throw new ArgumentNullException(nameof(options));
 4736        this.options.Validate();
 4737    }
 38
 39    /// <inheritdoc />
 40    public GovernanceHttpRequestCorrelation ResolveRequestCorrelation()
 41    {
 4242        HttpContext? httpContext = httpContextAccessor.HttpContext;
 43
 4244        return httpContext is null
 4245            ? new GovernanceHttpRequestCorrelation(traceId: Activity.Current?.Id)
 4246            : new GovernanceHttpRequestCorrelation(
 4247            ResolveCorrelationId(httpContext),
 4248            ResolveTraceId(httpContext),
 4249            ResolveMetadata(httpContext));
 50    }
 51
 52    private string? ResolveCorrelationId(HttpContext httpContext)
 53    {
 4154        if (options.TrustInboundCorrelationIdHeaders)
 55        {
 8056            foreach (string headerName in options.CorrelationIdHeaderNames)
 57            {
 3058                if (string.IsNullOrWhiteSpace(headerName))
 59                {
 60                    continue;
 61                }
 62
 2963                if (!httpContext.Request.Headers.TryGetValue(headerName, out StringValues values))
 64                {
 65                    continue;
 66                }
 67
 5068                foreach (string? candidate in values)
 69                {
 1470                    string? normalizedCandidate = NormalizeClientCorrelationId(candidate);
 1471                    if (normalizedCandidate is not null)
 72                    {
 473                        return normalizedCandidate;
 74                    }
 75                }
 76            }
 77        }
 78
 3779        return options.UseHttpContextTraceIdentifierAsCorrelationId
 3780            ? httpContext.TraceIdentifier
 3781            : null;
 482    }
 83
 84    private static string? NormalizeClientCorrelationId(string? value)
 85    {
 1486        if (string.IsNullOrWhiteSpace(value))
 87        {
 288            return null;
 89        }
 90
 1291        string normalizedValue = value.Trim();
 1292        if (normalizedValue.Length > GovernanceIdentifierLimits.MaximumLength)
 93        {
 194            return null;
 95        }
 96
 53597        foreach (char character in normalizedValue)
 98        {
 26099            if (char.IsControl(character))
 100            {
 7101                return null;
 102            }
 103        }
 104
 4105        return normalizedValue;
 106    }
 107
 108    private static string? ResolveTraceId(HttpContext httpContext)
 109    {
 41110        return Activity.Current?.Id ?? httpContext.TraceIdentifier;
 111    }
 112
 113    private Dictionary<string, string> ResolveMetadata(HttpContext httpContext)
 114    {
 41115        Dictionary<string, string> metadata = new(StringComparer.Ordinal)
 41116        {
 41117            [GovernanceHttpRequestMetadataKeys.TraceIdentifier] = httpContext.TraceIdentifier
 41118        };
 119
 41120        if (options.IncludeRequestMethod && !string.IsNullOrWhiteSpace(httpContext.Request.Method))
 121        {
 2122            metadata[GovernanceHttpRequestMetadataKeys.Method] = httpContext.Request.Method.Trim();
 123        }
 124
 41125        if (options.IncludeRequestPath && httpContext.Request.Path.HasValue)
 126        {
 1127            metadata[GovernanceHttpRequestMetadataKeys.Path] = httpContext.Request.Path.Value;
 128        }
 129
 41130        Endpoint? endpoint = httpContext.GetEndpoint();
 41131        var routeEndpoint = endpoint as RouteEndpoint;
 132
 41133        if (options.IncludeEndpointDisplayName && !string.IsNullOrWhiteSpace(endpoint?.DisplayName))
 134        {
 4135            metadata[GovernanceHttpRequestMetadataKeys.EndpointDisplayName] = endpoint.DisplayName.Trim();
 136        }
 137
 41138        if (options.IncludeRoutePattern && !string.IsNullOrWhiteSpace(routeEndpoint?.RoutePattern.RawText))
 139        {
 2140            metadata[GovernanceHttpRequestMetadataKeys.RoutePattern] = routeEndpoint.RoutePattern.RawText.Trim();
 141        }
 142
 41143        if (options.IncludeRouteValues)
 144        {
 88145            foreach (KeyValuePair<string, object?> routeValue in httpContext.Request.RouteValues)
 146            {
 4147                if (routeValue.Value is null || string.IsNullOrWhiteSpace(routeValue.Key))
 148                {
 149                    continue;
 150                }
 151
 2152                metadata[$"{GovernanceHttpRequestMetadataKeys.RouteValuePrefix}{routeValue.Key.Trim()}"] =
 2153                    routeValue.Value.ToString()?.Trim() ?? string.Empty;
 154            }
 155        }
 156
 41157        return metadata;
 158    }
 159}