PANiXiDA.Core.Presentation.Http is a reusable ASP.NET Core HTTP presentation package for PANiXiDA applications.
It provides common Minimal API endpoint conventions, API versioning, OpenAPI setup, Problem Details handling, health checks, request logging, exception handling, forwarded headers configuration, and helpers for mapping PANiXiDA.Core.ResultPattern results to HTTP responses.
AddHttpregisters the default HTTP presentation services.ICurrentUserexposes the authenticated HTTP caller to Application handlers.- Optional OpenIddict introspection validates Bearer access tokens through Identity.
UseHttpadds the default middleware pipeline and maps source-generated endpoint registrations.- HTTP JSON contracts enforce required constructor parameters, nullable annotations, and strict number handling.
- Module assemblies can be mapped to separate OpenAPI documents and Scalar sources for each API version through the
HttpModulesconfiguration section. - Health checks are registered by
AddHttpand exposed byUseHttpat a configurable path, defaulting to/health. IEndpointGroupdefines route, resource name, and API version metadata for Minimal API endpoint groups.IEndpoint<TGroup>defines route, name, and summary metadata for endpoints that belong to a specific group.- The bundled source generator registers endpoints in deterministic type-name order.
EndpointConstants.EndpointPrefixdefines/api/v{version:apiVersion}.ResultHttpMappermapsResultandResult<T>toIResult.
- .NET 10 SDK 10.0.401 or later.
- ASP.NET Core Minimal API application.
Full Native AOT support is currently blocked by API Versioning's OpenAPI/MVC integration.
Callers must supply non-null values for non-nullable API parameters.
Reference the package in each endpoint project with its analyzer assets enabled.
<ItemGroup>
<PackageReference Include="PANiXiDA.Core.Presentation.Http" Version="5.1.1" />
</ItemGroup>using PANiXiDA.Core.Presentation.Http.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttp(builder.Configuration);
builder.Services.AddValidation();
var app = builder.Build();
app.UseHttp(typeof(Program).Assembly);
app.Run();Call AddValidation() in each endpoint/DTO assembly to generate validation metadata for its DTO properties. AddHttp registers the shared validation services.
UseHttp configures forwarded headers → request logging → exception handling → HTTPS redirection → routing → CORS → authentication → authorization → antiforgery.
AddHttp registers the base CORS, authentication and authorization services; UseHttp always adds their middleware. The host configures schemes and policies with AddCors, AddAuthentication and AddAuthorization; CORS uses the default policy or endpoint policies.
Do not register routing, CORS, authentication, authorization or antiforgery middleware separately from UseHttp.
AddHttp registers antiforgery services; UseHttp runs antiforgery after authentication and authorization.
Minimal API form binding ([FromForm], IFormFile) requires a valid token and its cookie by default; missing or invalid tokens return 400.
JSON endpoints without antiforgery metadata are unaffected. Bearer authentication does not automatically bypass form validation.
Use .DisableAntiforgery() only on endpoints that do not rely on browser cookies, such as endpoints restricted to Bearer authentication.
The host issues tokens through IAntiforgery.GetAndStoreTokens and can customize AntiforgeryOptions with AddAntiforgery(options => ...) before or after AddHttp.
AddHttp registers IHttpContextAccessor and a scoped ICurrentUser from
PANiXiDA.Core.Application.Authentication.Abstractions, preserving an existing
ICurrentUser registration. Inject ICurrentUser into application handlers.
The adapter combines claims from all authenticated identities in HttpContext.User,
ignoring unauthenticated identities. Without an authenticated identity, it exposes
an anonymous caller with no claims or permissions. UserId parses
sub (or ClaimTypes.NameIdentifier when sub is absent) as a GUID. UserName
uses the configured name claim type, falling back to name; roles use the configured
role claim type and role, without duplicates. TryGetClaimValue<T> parses the first
matching claim with invariant culture and exact claim-type matching. Null, empty,
or whitespace claimType values return false.
HasPermission matches individual permission values exactly; roles do not grant permissions.
AccessToken reads the saved access token from the successful authentication result.
It returns null when the caller is unauthenticated or no saved access token is available.
Explicit authentication policies preserve the token of the first authenticated
identity in the combined principal, including Cookie/Bearer policies.
Configure token validation as described below or use the host's authentication.
Run authentication before invoking handlers. Background consumers must supply their
own ICurrentUser for handlers that require authorization.
AddHttp enables OpenIddict introspection when OpenIddictValidationOptions is present:
{
"OpenIddictValidationOptions": {
"Issuer": "https://identity.example.com/",
"Audiences": ["panixida-api"],
"ClientId": "panixida-api"
}
}Supply OpenIddictValidationOptions:ClientSecret from your secret store.
Register this confidential client in Identity with introspection permission.
For OpenIddict Identity, include its ClientId in the token's audiences so introspection
returns user claims. ValidateOnStart() runs OpenIddict's built-in configuration checks.
Use an HTTPS issuer and configure Audiences to restrict token recipients; an empty list
disables audience validation. Without this section, AddHttp preserves host authentication,
including UseLocalServer in an Identity host.
builder.Services.AddHttp(builder.Configuration);
var app = builder.Build();
app.UseHttp();
app.MapGet("/protected", () => Results.Ok()).RequireAuthorization();
app.Run();Send Authorization: Bearer <access_token>. Discovery locates Identity's introspection
endpoint; each authenticated request checks token activity, expiry, and configured audiences.
Revoked tokens are rejected on the next request. Cookie, query, and form tokens are ignored.
Invalid tokens return 401, failed policies return 403, and Identity failures deny access.
Public endpoints remain public. Identity availability affects authenticated requests.
Use roles and policies for endpoint access, or IRequireAuthorization on Application handlers.
Tokens from client_credentials may have a string sub: ICurrentUser.UserId is then null;
read the subject with TryGetClaimValue<string>("sub", out var subject).
The package configures these forwarded headers by default:
ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedHost |
ForwardedHeaders.XForwardedProtoThe package also clears the default loopback-only KnownIPNetworks and KnownProxies restrictions so applications behind Kubernetes ingress or Gateway API proxies can process forwarded headers without per-service proxy registration.
Configure the standard ForwardedHeadersOptions values through the ForwardedHeaders section.
{
"ForwardedHeaders": {
"ForwardedHeaders": "XForwardedFor, XForwardedHost, XForwardedProto",
"ForwardLimit": 2,
"RequireHeaderSymmetry": true,
"AllowedHosts": [
"api.example.com"
]
}
}KnownIPNetworks and the legacy KnownNetworks key accept objects such as { "Prefix": "10.0.0.0", "PrefixLength": 8 }. Both fields are required; an explicit PrefixLength: 0 is allowed. Invalid addresses and networks are rejected. Restart the application to apply forwarded-header configuration changes.
For stricter trust boundaries, configure ForwardedHeadersOptions directly after AddHttp.
using Microsoft.AspNetCore.HttpOverrides;
using System.Net;
builder.Services.AddHttp(builder.Configuration);
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.KnownProxies.Add(IPAddress.Parse("10.0.0.10"));
});AddHttp registers ASP.NET Core health check services, and UseHttp maps the health check endpoint at /health by default.
Override the path in application configuration:
{
"HealthCheckOptions": {
"Path": "/status/health"
}
}The path must be nonblank and start with /; invalid values prevent startup. Only the configured path is mapped.
Services can add their own checks after AddHttp.
using Microsoft.Extensions.Diagnostics.HealthChecks;
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy());An endpoint group owns a route prefix, resource name, API version, and the call to map endpoints that belong to the group.
using Asp.Versioning;
using Microsoft.AspNetCore.Routing;
using PANiXiDA.Core.Presentation.Http.Endpoints;
public sealed class OrdersEndpointGroup : IEndpointGroup
{
public string Route { get; } = "/orders";
public string Name { get; } = "Orders";
public ApiVersion ApiVersion { get; } = new(1, 0);
public void Map(IEndpointRouteBuilder endpoints)
{
EndpointMapper.MapGroupEndpoints<OrdersEndpointGroup>(endpoints);
}
}The final route prefix is /api/v{version}/orders.
Groups that require a custom root route can map their endpoints through an explicit RouteGroupBuilder.
Custom groups retain their HTTP module metadata for OpenAPI documents.
public void Map(IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("/connect")
.WithTags(Name);
EndpointMapper.MapGroupEndpoints<OAuthEndpointGroup>(
group,
endpoints.ServiceProvider);
}An endpoint implements IEndpoint<TGroup>, where TGroup is the endpoint group it belongs to.
Endpoint metadata is declared as public properties so it can be required by the interface and applied by EndpointMapper.
Keep each group and its endpoints in the same assembly. Use non-generic public or internal classes with one public constructor; for multiple constructors, mark one with [ActivatorUtilitiesConstructor]. Dependencies are resolved from DI, including keyed services and optional parameters.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using PANiXiDA.Core.Presentation.Http.Endpoints;
public sealed class GetOrderEndpoint : IEndpoint<OrdersEndpointGroup>
{
public string Route { get; } = "/{id:guid}";
public string Name { get; } = "GetOrder";
public string Summary { get; } = "Gets an order by identifier.";
public void Map(EndpointMapBuilder builder)
{
builder.MapGet(builder.Route, (Guid id) =>
{
return TypedResults.Ok(new OrderResponse(id));
});
}
}
public sealed record OrderResponse(Guid Id);Use standard ASP.NET Core mapping methods, for example builder.MapGet(builder.Route, Handle) or builder.MapMethods(builder.Route, ["HEAD", "OPTIONS"], Handle). Name and summary are applied automatically; standard endpoint conventions can override them.
Successful results are mapped through the provided success factory.
using Microsoft.AspNetCore.Http;
using PANiXiDA.Core.Presentation.Http.Helpers;
using PANiXiDA.Core.ResultPattern;
public static IResult GetOrder(Guid id)
{
Result<OrderResponse> result = Result.Success(new OrderResponse(id));
return result.ToHttpResult(value =>
{
return TypedResults.Ok(value);
});
}Failed results are mapped to ProblemDetails or ValidationProblem.
using Microsoft.AspNetCore.Http;
using PANiXiDA.Core.Presentation.Http.Helpers;
using PANiXiDA.Core.ResultPattern;
public static IResult CreateOrder()
{
Result result = Result.Failure(Error.Validation("Email is required").WithField("Email"));
return result.ToHttpProblem();
}Invalid HTTP requests represented by BadHttpRequestException, including JSON body binding failures, preserve their framework status code and are mapped to ProblemDetails.
Client aborts (OperationCanceledException or IOException with a canceled RequestAborted) use status 499 if the response has not started. Other unhandled exceptions use status 500.
Nonempty AggregateException trees containing only OperationCanceledException (including TaskCanceledException) also use 499 when RequestAborted is canceled and the response has not started. Empty or mixed aggregates and unknown wrappers remain errors.
In Development, error ProblemDetails responses include the exception message in detail.
HTTP completion logs include the final status and request context: 4xx (including 499) are Warning, 5xx are Error, and other responses are Information. Exceptions handled by this package are attached to the completion log with their stack trace. Client aborts handled directly by ASP.NET Core (OperationCanceledException, including TaskCanceledException, or IOException) have no exception attached to the completion log. Exception handlers only produce the HTTP response.
Completion logging also covers CORS preflight, authentication/authorization failures, and short-circuited endpoints. Scopes capture the route after routing and the authenticated sub claim (falling back to NameIdentifier) after authentication. These snapshots stay unchanged during deferred log export; completion logs capture the final request context.
| Error type | HTTP status | Title |
|---|---|---|
Validation |
400 | One or more validation errors occurred. |
NotFound |
404 | Resource not found |
Conflict |
409 | Conflict |
Unauthorized |
401 | Unauthorized |
Forbidden |
403 | Forbidden |
Failure |
400 | Request failed |
Unexpected |
500 | Server error |
Validation error fields are used as ValidationProblem keys. If a validation error has no field, the key is general.
AddHttp enables RespectRequiredConstructorParameters, RespectNullableAnnotations, and strict number handling for Minimal API JSON requests and responses. Missing constructor parameters without defaults and explicit null for non-nullable reference properties or parameters produce HTTP 400.
Use defaults for optional constructor parameters:
public sealed record CreateRequest(
string Name,
string? RequiredNote,
int? Shots = null);name and requiredNote must be present. name must not be null; requiredNote may be null. shots may be omitted or set to null. For property-based DTOs, use required or [JsonRequired] to require presence.
Non-nullable reference properties in responses must contain non-null values. Validate collection elements, generic members, and business rules separately. Send numbers as JSON numbers; enums use strings. These settings do not affect form/query binding or separately configured serializers.
In Development, UseHttp exposes:
- OpenAPI documents at
/openapi/v1.json,/openapi/v2.json, and so on for the mapped API versions; - Scalar API reference at
/scalar.
OpenAPI registration also enables Scalar transformers for Scalar-specific document extensions.
AddHttp adds an HTTP Bearer security requirement to operations whose resolved authorization
policy accepts a Bearer authentication scheme. This enables token input and authenticated requests
in Scalar. Enter the access token without the Bearer prefix; Scalar sends it as
Authorization: Bearer <access_token>.
Endpoint and route-group metadata (RequireAuthorization, [Authorize], and explicit policies)
are combined with named, default, and fallback policies through ASP.NET Core's policy provider.
When the resulting policy does not select authentication schemes, the default authenticate scheme
is used. AllowAnonymous endpoints are excluded. Cookie-only policies and other unmapped schemes
do not receive a Bearer requirement, even when Bearer is the host's default scheme.
The standard schemes Bearer, BearerToken, and OpenIddict.Validation.AspNetCore are recognized automatically.
Declare additional Bearer scheme names in ScalarConfiguration:BearerAuthenticationSchemes.
For an OpenIddict host using the server handler for UserInfo:
{
"ScalarConfiguration": {
"BearerAuthenticationSchemes": [
"OpenIddict.Server.AspNetCore"
]
}
}Names are case-sensitive and must match the host's authentication registrations. Include a forwarding policy scheme only if it accepts Bearer tokens. The library does not infer protocols from custom handlers or forwarding selectors, and it does not generate documentation for Cookie or certificate authentication. The Bearer component is added only to documents with matching operations, including versioned and unversioned module documents.
The scheme list defaults to an empty array. Each configured scheme name must be nonblank; invalid options are rejected at startup.
The host configures authentication schemes and authorization policies; AddHttp and UseHttp
provide the base services and middleware. The OpenAPI configuration only maps authentication
scheme names to OpenAPI Bearer security; it does not register handlers or
change server-side access checks. Both JWT and opaque tokens are supported without prescribing a
token format.
Accept SortingParameters directly with [AsParameters] and pass it to the application layer:
using Microsoft.AspNetCore.Http;
using PANiXiDA.Core.Application.Querying.Sorting;
app.MapGet("/users", ([AsParameters] SortingParameters sorting) =>
{
return TypedResults.Ok(sorting.Fields);
});Requests use repeated query parameters: ?Fields=name:desc&Fields=department.name:asc.
Omitting Fields produces empty sorting. Directions default to asc.
AddHttp describes sorting as an optional string array with style: form and explode: true
in every OpenAPI document. Swagger, Scalar, and generated clients can send the repeated values;
client generators expose Fields as a string collection. JSON request and response schemas remain unchanged.
Applications composed from multiple presentation modules expose one OpenAPI document per module and API version.
Register the presentation assemblies in code and configure their document names and display titles in appsettings.json.
UseHttp automatically maps endpoint groups from registered module assemblies.
using PANiXiDA.Core.Presentation.Http.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttp(
builder.Configuration,
typeof(IdentityPresentationAssembly).Assembly,
typeof(CompendiumPresentationAssembly).Assembly);
var app = builder.Build();
app.UseHttp();
app.Run();Each key under HttpModules must match the simple name returned by Assembly.GetName().Name.
Both Name and Title are required for every registered module assembly.
{
"HttpModules": {
"PANiXiDA.TacticalHeroes.Identity.Presentation": {
"Name": "identity",
"Title": "Identity API"
},
"PANiXiDA.TacticalHeroes.Compendium.Presentation": {
"Name": "compendium",
"Title": "Compendium API"
}
}
}For Identity endpoints in versions 1 and 2 and Compendium endpoints in version 1, this configuration exposes:
/openapi/identity-v1.jsonwith the titleIdentity API v1;/openapi/identity-v2.jsonwith the titleIdentity API v2;/openapi/compendium-v1.jsonwith the titleCompendium API v1;/scalarwith a selector for these three documents.
Routes without a version appear in every document of their module. A module with only unversioned routes uses a common document, such as /openapi/identity.json. Empty modules produce no documents.
Final document names must be unique ignoring case, including common module documents and versioned documents. A presentation assembly can belong to only one module.
Without modules, documents follow visible API versions (v1, v2, and so on); when only unversioned endpoints are visible, they use the configured default version (v1 by default).
The Scalar browser tab title and favicon can be configured from application configuration. If either value is missing or blank, Scalar keeps its corresponding default.
{
"ScalarConfiguration": {
"Title": "Orders API Reference",
"Favicon": "/favicon.ico"
}
}Place local icons in the host's wwwroot (Microsoft.NET.Sdk.Web).
In Development, a nonblank Favicon makes UseHttp call MapStaticAssets().ShortCircuit() for all host assets.
These assets must be public: middleware after routing, including authorization and CORS, is skipped.
UseHttp ensures forwarded headers, request logging, exception handling, and HTTPS redirection run before static assets.
Host-specific middleware needed for assets must run before UseHttp; for HSTS behind a proxy, call UseForwardedHeaders() then UseHsts() before it.
With CreateSlimBuilder, also call builder.WebHost.UseStaticWebAssets() in Development before Build().
Outside Development, OpenAPI, Scalar, and automatic asset mapping are disabled.
The package configures URL segment API versioning:
/api/v1/orders
The default API version is 1.0, and the version must be present in the route.
src/
PANiXiDA.Core.Presentation.Http.Generators/
PANiXiDA.Core.Presentation.Http/
Configurations/
DependencyInjection/
Endpoints/
Helpers/
Middlewares/
tests/
PANiXiDA.Core.Presentation.Http.UnitTests/
Run the standard validation before publishing:
dotnet restore
dotnet format
dotnet build --configuration Release
dotnet test --configuration Release
dotnet pack --configuration ReleaseRun coverage:
dotnet test --configuration Release -- --coverage --coverage-output coverage.xml --coverage-output-format xmlThe source files under src/PANiXiDA.Core.Presentation.Http are covered by unit tests. Coverage excludes generated files under obj/ from ASP.NET Core and validation source generators.
Every pull request and push to main runs formatting, tests, and mandatory
SonarQube analysis. Publishing from main starts only after the SonarQube
Quality Gate succeeds.
The NuGet package includes:
- compiled library for
net10.0; - the Roslyn generator for
netstandard2.0, packaged underanalyzers/dotnet/cs; - XML documentation;
- README;
- package icon;
- Source Link metadata;
- symbols package when packed with repository settings.
This project is licensed under the Apache-2.0 license. See LICENSE for details.