Csag.Blueprint.Web
0.5.0
dotnet add package Csag.Blueprint.Web --version 0.5.0
NuGet\Install-Package Csag.Blueprint.Web -Version 0.5.0
<PackageReference Include="Csag.Blueprint.Web" Version="0.5.0" />
<PackageVersion Include="Csag.Blueprint.Web" Version="0.5.0" />
<PackageReference Include="Csag.Blueprint.Web" />
paket add Csag.Blueprint.Web --version 0.5.0
#r "nuget: Csag.Blueprint.Web, 0.5.0"
#:package Csag.Blueprint.Web@0.5.0
#addin nuget:?package=Csag.Blueprint.Web&version=0.5.0
#tool nuget:?package=Csag.Blueprint.Web&version=0.5.0
Csag.Blueprint.Web
Overview
This package provides the shared web-layer composition for CSAG Blueprint-based applications.
It owns reusable validated options, builder and middleware composition helpers, FastEndpoints/Swagger setup, OAuth integration, correlation and request-culture infrastructure, and readiness support that applications can plug into their host.
Core areas
Options and validation
The package ships reusable option types and validators for:
- database startup behavior
- API/security settings
- cache provider settings
- feature flags
- localization settings
Use:
services.AddBlueprintDefaultValidatedOptions(configuration);
This registers the package-owned validated options rooted under the Blueprint configuration section. It also
sets the session lifetime of Csag.Blueprint.Infrastructure (BlueprintSessionOptions.Lifetime) from
Blueprint:Security:SessionExpirationHours, the single setting the session cookie, its ticket and its tracking row
expire by; see the Infrastructure package's Cookie sessions. In the same way it sets
BlueprintServiceAccountOptions — signing key, issuer, audience, token lifetime and lockout — from
Blueprint:Security:Jwt and Blueprint:Security:ServiceAccountLockout; see the Infrastructure package's
Service-account credentials.
Builder composition
AddBlueprintServices(this WebApplicationBuilder) wires shared web services such as:
- HTTPS redirection / HSTS / security headers setup
- CORS configuration
- Google OAuth authentication
- FastEndpoints registration
- Swagger/OpenAPI registration
- distributed cache registration
- anti-forgery services
Middleware composition
The package provides two main composition helpers:
app.UseBlueprintSecurityHeaders();
app.UseBlueprintMiddleware();
UseBlueprintMiddleware() applies the shared request pipeline:
- HTTP audit logging
- correlation ID middleware
- CORS
- the refusal of a request that carries both a session cookie and a bearer token (400 ProblemDetails)
- authentication
- tenant middleware
- request localization
- authorization
Applications may still append app-specific middleware before endpoint mapping.
Reusable middleware and services
| Component | Purpose |
|---|---|
CorrelationIdMiddleware |
Adds/propagates correlation IDs per request. |
SingleCredentialMiddleware |
Refuses a request that carries both the session cookie and a bearer token with a 400 ProblemDetails carrying the correlation ID, before authentication. Each request is authenticated by exactly one scheme. |
TenantMiddleware |
Establishes the ambient tenant context for the request. Delegates how the tenant is determined to ITenantResolver. |
HttpAuditMiddleware |
Records an audit event for a denied request (401 or 403 by default; configurable). UseBlueprintMiddleware() always registers it; ConfigureBlueprintAuditLogging turns it on. See Audit enrichment. |
CorrelationIdDelegatingHandler |
Propagates correlation IDs to outbound HTTP requests. |
SessionClaimRequestCultureProvider |
Resolves request culture from claims and Accept-Language. |
CultureNormalizationHelper |
Matches and validates requested cultures/languages. |
StartupCompletedHealthCheck |
Reusable readiness gate used with startup orchestration. |
Audit enrichment
ConfigureBlueprintAuditLogging adds the user ID, the email address, the display name, and the
correlation ID to each EF and HTTP audit event alike. It reads the three user values from the
claims on the request, not from the database. A service account has no email address, so its email
value is null and its display name is the account name from its token.
The provider writes UserId, TenantId, and CorrelationId to columns. The email address and the
display name stay in the JsonData column, at $.UserEmail and $.UserDisplayName. Reading either
value requires parsing the JSON data of the row.
UseBlueprintMiddleware() always registers HttpAuditMiddleware, first, so that it wraps
everything else in the pipeline: authentication and authorization, and also the exception handler
and status code pages. The ASP.NET Core authorization middleware does not call the next middleware
for a denied request, so a middleware registered later would never run for one. A middleware
registered after the exception handler would never see a status code that an unhandled exception
produced, so it could never audit one. An app does not need to register HttpAuditMiddleware
itself. Remove any manual app.UseMiddleware<HttpAuditMiddleware>() call; a duplicate registration
records two events per audited request.
ConfigureBlueprintAuditLogging configures both audit features, the EF Core events and the HTTP
request events, from this one call; both are on by default. Its configure callback exposes the same
HttpAuditOptions instance the middleware reads, as BlueprintAuditOptions.HttpAudit; set
HttpAudit.Enabled = false there to turn HTTP request auditing off.
Until ConfigureBlueprintAuditLogging runs, or when that flag is false, the middleware does
nothing but call the next middleware. Once on, it writes an event for a request whose final status
code is 401 or 403 by default. Call services.Configure<HttpAuditOptions>(o => ...) to audit a
different set. It also writes the address in
HttpContext.Connection.RemoteIpAddress, as resolved by ForwardedHeadersMiddleware. That is the
nearest hop, which is the caller only when nothing between the caller and this app appends its own
X-Forwarded-For entry — see the forwarded-headers note under Rate limiting below. A request outside the audited set writes no event: the EF Core interceptor
already covers the writes, and GCP and Application Insights already record general request data.
HttpAuditMiddleware wraps the app's exception handler rather than running inside it, so it can
also audit a status code the handler itself produces, for example a 500 added to
HttpAuditOptions.AuditedStatusCodes. It catches and logs its own failure instead of throwing,
because sitting outside the exception handler means an unhandled exception here would reach the
ASP.NET Core default error handling instead of the app's configured one.
Tenant resolution (the addressing seam)
ITenantResolver decides which tenant an incoming request belongs to. The package ships
ClaimsTenantResolver as the default, which reads the tenant from the authenticated session's
TenantId claim — "session-resolved" addressing, where the tenant is a property of who you are
signed in as rather than of the URL you requested.
Other generic addressing strategies — a vanity subdomain (acme.example.com), a path segment
(/t/acme), a header-driven tenant — belong in this package as additional ITenantResolver
implementations; if the one you need is missing, add it here rather than in your application. A
custom, app-local implementation remains possible for an addressing scheme that is genuinely
app-specific and where no generic resolver makes sense. Either way the default is registered with
TryAddScoped, so a resolver registered before AddBlueprintServices wins and the package default
never has to be unregistered:
builder.Services.AddScoped<ITenantResolver, MyAppSpecificTenantResolver>();
builder.AddBlueprintServices();
Two things to know before switching addressing strategy:
TenantMiddlewareruns afterUseAuthentication/UseAuthorization, because the default resolver needs the authenticated principal. A host- or path-based resolver does not, and moving the middleware earlier is what enables per-tenant branding and per-tenant identity-provider routing on the sign-in page.- Sign-in currently derives the tenant and writes it into the session ticket. Once the URL is the source of truth that relationship inverts, so session composition needs revisiting too. The resolver is the seam, not the whole job.
The resolver returns Guid?; null means "no tenant context", which is a normal state for anonymous
requests, platform-scope endpoints, and users who belong to no tenant.
FastEndpoints and Swagger
The package owns:
- FastEndpoints registration helpers
- conventional endpoint routing/naming helpers
- Swagger/OpenAPI registration helpers
UseFastEndpointsWithConventions gives every authenticated endpoint that declares no scheme of its own exactly one:
the session cookie (CookieAuthMode = OptOut), Bearer (JwtAuthMode = OptOut), or, with both, SessionOrBearer,
which picks the bearer token when the request carries one and the session cookie otherwise. An endpoint that declares
its own schemes with AuthSchemes() keeps exactly those.
Applications still own their endpoint classes, DTOs, validators, and policies.
Rate limiting
RateLimiterExtensions supplies the two pieces a host needs to wire up ASP.NET Core rate limiting;
the policies and their limits stay with the host.
UseBlueprintRejectionResponse() shapes the rejection: 429 Too Many Requests instead of the
framework's 503 Service Unavailable default (which an upstream load balancer reads as a backend
fault), an RFC 9457 ProblemDetails body carrying the correlation ID, and Retry-After when the
limiter reports it.
TryGetClientPartitionKey(header, out key) resolves the caller: the named edge-stamped header when
configured and parseable, else Connection.RemoteIpAddress, else nothing — the rule of
ClientAddressHttpContextExtensions.TryGetClientAddress in Csag.Blueprint.Infrastructure, which session tracking
records client addresses with too. It reports failure rather
than inventing a key, and a caller it cannot identify must be routed to
RateLimitPartition.GetNoLimiter — substituting a placeholder key puts every unidentifiable caller
in one bucket, so the first of them to exceed the limit rejects them all.
The key is opaque, not an address. An IPv4-mapped IPv6 address collapses to its IPv4 form so one
caller cannot occupy two partitions, and a native IPv6 address is truncated to its /64 prefix and
returned as 2001:db8:85a3:8d3::/64. IPv6 is allocated by prefix, not by address: the smallest block
normally routed to a customer is a /64, and its 2^64 addresses are all theirs to send from at no
cost, so keying on the full /128 enforces nothing against a caller who varies the source address
within their own prefix, while an IPv4 caller behind NAT shares one bucket with their whole office.
Truncation does not stop an attacker who rents several prefixes; it prices the evasion at one prefix
per bucket instead of at nothing. HttpAuditMiddleware records Connection.RemoteIpAddress itself,
so audit events keep the exact address.
Prefer an edge-stamped header. RemoteIpAddress is the nearest hop, not the caller: ForwardLimit 1
reads the rightmost X-Forwarded-For entry, and an edge that appends its own — a Google external
load balancer sends <client>,<balancer> — leaves that edge's address rightmost. Behind a further
reverse proxy it is that proxy's egress address: one constant shared by every caller. Reading the
header from the left instead is worse, because the entries a client wrote are kept there unverified.
The header must be one the edge writes itself and overwrites inbound, so a client cannot forge it.
All of that assumes the app is reachable only through that edge, which UseBlueprintSecurityHeaders
requires rather than enforces. It registers ForwardedHeadersMiddleware with KnownProxies and
KnownIPNetworks empty — Cloud Run and similar platforms front the app from internal addresses that
are not on the default loopback list — and emptying both turns the middleware's known-proxy check
off, so it applies whatever the immediate peer sent. A caller able to open a connection to the app
directly is that peer: it writes X-Forwarded-For itself, which makes the rightmost entry its own
value, and a forged X-Forwarded-Proto: https satisfies the HTTPS redirection and HSTS middleware.
Block direct ingress at the platform. The options are built inline rather than read from
IOptions<ForwardedHeadersOptions>, so a host cannot narrow them by configuration, and registering a
second ForwardedHeadersMiddleware is not a substitute: each run consumes the entries it reads.
Ownership Boundary
This package owns reusable web composition, not the application host itself.
The consuming application still owns:
Program.cs- endpoint implementations
- app-specific validators and option extensions
- host-specific runtime services
- concrete authentication/authorization decisions at the app level
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Audit.EntityFramework.Core (>= 33.0.0)
- Audit.NET (>= 33.0.0)
- Audit.NET.SqlServer (>= 33.0.0)
- ClosedXML (>= 0.105.1)
- Csag.Blueprint.Infrastructure (>= 0.5.0)
- FastEndpoints (>= 8.3.0)
- FastEndpoints.Swagger (>= 8.3.0)
- FluentValidation (>= 12.1.1)
- Microsoft.ApplicationInsights.AspNetCore (>= 2.23.0)
- Microsoft.ApplicationInsights.NLogTarget (>= 2.23.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.12)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 10.0.12)
- Microsoft.AspNetCore.DataProtection.EntityFrameworkCore (>= 10.0.12)
- Microsoft.AspNetCore.Identity.EntityFrameworkCore (>= 10.0.12)
- Microsoft.Data.SqlClient (>= 7.0.3)
- Microsoft.EntityFrameworkCore (>= 10.0.12)
- Microsoft.EntityFrameworkCore.SqlServer (>= 10.0.12)
- Microsoft.Extensions.Caching.SqlServer (>= 10.0.12)
- Microsoft.Extensions.Caching.StackExchangeRedis (>= 10.0.12)
- Neolution.Extensions.Caching.Distributed (>= 3.0.0-beta.0)
- Neolution.Extensions.Configuration.GoogleSecrets (>= 1.6.0)
- NLog.Web.AspNetCore (>= 6.2.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Csag.Blueprint.Web:
| Package | Downloads |
|---|---|
|
Csag.Blueprint.Testing
Test infrastructure with Testcontainers, integration test base classes, and AutoFixture support for CSAG Blueprint applications. |
GitHub repositories
This package is not used by any popular GitHub repositories.