< Summary

Information
Class: AsiBackbone.AspNetCore.Endpoints.EndpointGovernanceMiddleware
Assembly: AsiBackbone.AspNetCore
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.AspNetCore/Endpoints/EndpointGovernanceMiddleware.cs
Line coverage
100%
Covered lines: 59
Uncovered lines: 0
Coverable lines: 59
Total lines: 126
Line coverage: 100%
Branch coverage
95%
Covered branches: 21
Total branches: 22
Branch coverage: 95.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
InvokeAsync()100%1414100%
CreateDefaultForbiddenResult(...)87.5%88100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.AspNetCore/Endpoints/EndpointGovernanceMiddleware.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Http;
 2using Microsoft.Extensions.Options;
 3
 4namespace AsiBackbone.AspNetCore.Endpoints;
 5
 6/// <summary>
 7/// Middleware that evaluates AsiBackbone endpoint governance metadata before endpoint execution.
 8/// </summary>
 9public sealed class EndpointGovernanceMiddleware
 10{
 11    // Example: use a precomputed static result for the most common forbidden response
 112    private static readonly IResult LightweightForbiddenResult =
 113        Microsoft.AspNetCore.Http.Results.StatusCode(StatusCodes.Status403Forbidden);
 14    // Use this for default cases instead of constructing new ProblemDetails every time.
 15
 16    private readonly RequestDelegate next;
 17    private readonly EndpointGovernanceOptions options;
 18
 19    /// <summary>
 20    /// Initializes a new instance of the <see cref="EndpointGovernanceMiddleware" /> class.
 21    /// </summary>
 22    /// <param name="next">The next request delegate.</param>
 23    /// <param name="endpointOptions">The endpoint governance options.</param>
 1724    public EndpointGovernanceMiddleware(
 1725        RequestDelegate next,
 1726        IOptions<EndpointGovernanceOptions> endpointOptions)
 27    {
 1728        ArgumentNullException.ThrowIfNull(next);
 1729        ArgumentNullException.ThrowIfNull(endpointOptions);
 30
 1731        this.next = next;
 1732        options = endpointOptions.Value;
 1733    }
 34
 35    /// <summary>
 36    /// Evaluates endpoint governance metadata and either continues execution or writes a failure result.
 37    /// </summary>
 38    /// <param name="httpContext">The current HTTP context.</param>
 39    /// <param name="governanceService">The endpoint governance service.</param>
 40    /// <returns>A task that completes when the middleware has run.</returns>
 41    public async Task InvokeAsync(
 42        HttpContext httpContext,
 43        IEndpointGovernanceService governanceService)
 44    {
 1745        ArgumentNullException.ThrowIfNull(httpContext);
 1746        ArgumentNullException.ThrowIfNull(governanceService);
 47
 48        // Options validation is registered with ValidateOnStart in AddAsiBackboneAspNetCore.
 49        // IOptions<T> is singleton-backed, so keep the validated value from construction rather
 50        // than resolving IOptions<T> again through method injection on every request.
 1751        Endpoint? endpoint = httpContext.GetEndpoint();
 1752        var descriptor = EndpointGovernanceDescriptor.FromEndpoint(endpoint);
 53
 1754        bool endpointAllowsMissingGovernance = endpoint?.Metadata
 1755            .GetMetadata<AllowMissingGovernanceMetadataAttribute>() is not null;
 56
 1757        if (!descriptor.HasGovernanceMetadata)
 58        {
 859            if (options.RequireGovernanceMetadata && !endpointAllowsMissingGovernance)
 60            {
 61                // A null endpoint and an endpoint carrying no governance metadata both fail closed here, but they
 62                // have different causes and different fixes. A null endpoint usually means this middleware runs
 63                // before endpoint routing, in which case every request looks ungoverned; reporting it under the same
 64                // stage as "this endpoint declares no metadata" hides a pipeline-ordering fault as a metadata gap.
 465                string decisionStage = endpoint is null
 466                    ? "aspnetcore.endpoint.governance.unresolved_endpoint"
 467                    : "aspnetcore.endpoint.governance.metadata";
 68
 469                IResult missingGovernanceResult = CreateDefaultForbiddenResult(
 470                    httpContext,
 471                    options,
 472                    descriptor,
 473                    result: null,
 474                    decisionStage: decisionStage);
 75
 476                await missingGovernanceResult.ExecuteAsync(httpContext).ConfigureAwait(false);
 477                return;
 78            }
 79
 480            await next(httpContext).ConfigureAwait(false);
 481            return;
 82        }
 83
 984        EndpointGovernanceResult result = await governanceService
 985            .EvaluateAsync(httpContext, descriptor, httpContext.RequestAborted)
 986            .ConfigureAwait(false);
 87
 988        if (result.CanExecute)
 89        {
 190            await next(httpContext).ConfigureAwait(false);
 191            return;
 92        }
 93
 894        IResult failureResult = result.FailureResult ?? CreateDefaultForbiddenResult(
 895            httpContext,
 896            options,
 897            descriptor,
 898            result,
 899            "aspnetcore.endpoint.governance.decision");
 100
 8101        await failureResult.ExecuteAsync(httpContext).ConfigureAwait(false);
 17102    }
 103
 104    private static IResult CreateDefaultForbiddenResult(
 105        HttpContext httpContext,
 106        EndpointGovernanceOptions options,
 107        EndpointGovernanceDescriptor descriptor,
 108        EndpointGovernanceResult? result,
 109        string decisionStage)
 110    {
 10111        return AsiBackboneEndpointGovernanceDevelopmentDiagnostics.IsEnabled(httpContext, options)
 10112            ? AsiBackboneEndpointGovernanceDevelopmentDiagnostics.CreateProblem(
 10113                httpContext,
 10114                options,
 10115                descriptor,
 10116                result?.Decision,
 10117                decisionStage,
 10118                title: "Endpoint governance blocked execution.",
 10119                detail: "Endpoint governance blocked this request before the selected endpoint executed.",
 10120                statusCode: StatusCodes.Status403Forbidden)
 10121            : options.DefaultForbiddenResultFactory is null
 10122            ? LightweightForbiddenResult
 10123            : options.DefaultForbiddenResultFactory(httpContext)
 10124                ?? throw new InvalidOperationException($"{nameof(EndpointGovernanceOptions.DefaultForbiddenResultFactory
 125    }
 126}