DRN.Framework.Hosting 0.10.0-preview002

Prefix Reserved
This is a prerelease version of DRN.Framework.Hosting.
dotnet add package DRN.Framework.Hosting --version 0.10.0-preview002
                    
NuGet\Install-Package DRN.Framework.Hosting -Version 0.10.0-preview002
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DRN.Framework.Hosting" Version="0.10.0-preview002" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DRN.Framework.Hosting" Version="0.10.0-preview002" />
                    
Directory.Packages.props
<PackageReference Include="DRN.Framework.Hosting" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DRN.Framework.Hosting --version 0.10.0-preview002
                    
#r "nuget: DRN.Framework.Hosting, 0.10.0-preview002"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DRN.Framework.Hosting@0.10.0-preview002
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DRN.Framework.Hosting&version=0.10.0-preview002&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=DRN.Framework.Hosting&version=0.10.0-preview002&prerelease
                    
Install as a Cake Tool

master develop Quality Gate Status

Security Rating Maintainability Rating Reliability Rating Vulnerabilities Bugs Lines of Code

DRN.Framework.Hosting

Application shell for DRN web applications with security-first design, structured lifecycle, and type-safe routing.

TL;DR

  • Security defaults: DrnDefaults enforces MFA, nonce-based script CSP, and HSTS outside Development.
  • Startup: DrnProgramBase provides hooks for registration, middleware, and validation.
  • Routing: Typed controller endpoint accessors bind to mapped routes; page accessors generate Razor paths.
  • Local infrastructure: DRN.Framework.Testing provides opt-in Postgres provisioning.
  • Frontend: Razor TagHelpers handle Vite manifests, CSP nonces, and HTMX CSRF headers.

Table of Contents


QuickStart: Beginner

Use a .NET 10 web project with a DRN.Framework.Hosting package reference. Inherit from DrnProgramBase<TProgram> and register application services. Configure Environment and NLog before calling RunAsync; see Configuration.

using DRN.Framework.Hosting.DrnProgram;
using DRN.Framework.Hosting.HealthCheck;
using DRN.Framework.Utils.DependencyInjection;
using DRN.Framework.Utils.Logging;
using DRN.Framework.Utils.Settings;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;

namespace Sample.Hosted;

public class Program : DrnProgramBase<Program>, IDrnProgram
{
    public static async Task Main(string[] args) => await RunAsync(args);

    protected override Task AddServicesAsync(
        WebApplicationBuilder builder,
        IAppSettings appSettings,
        IScopedLog scopedLog)
    {
        builder.Services.AddServicesWithAttributes();
        return Task.CompletedTask;
    }
}

// WeatherForecastControllerBase supplies [AllowAnonymous] and [HttpGet].
[Route("[controller]")]
public class WeatherForecastController : WeatherForecastControllerBase;

QuickStart: Advanced

Use DRN.Framework.Testing for integration tests that create the application pipeline and its configured database dependencies. Place tests using DrnTestContext in an integration test project.

public class WeatherForecastTests
{
    [Theory, DataInline]
    public async Task WeatherForecast_Should_Return_Data(DrnTestContext context)
    {
        var client = await context.ApplicationContext.CreateClientAsync<Program>();
        var response = await client.GetAsync("WeatherForecast");

        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var data = await response.Content.ReadFromJsonAsync<IEnumerable<WeatherForecast>>();
        data.Should().NotBeEmpty();
    }
}

ApplicationContext automatically uses the active xUnit v3 output helper when each application is created, but only while a debugger is attached to preserve the log privacy gate. Do not request ITestOutputHelper as a [DataInline] theory parameter for application logging: AutoFixture supplies an interface substitute, not xUnit's runner-owned helper.

Directory Structure

DRN.Framework.Hosting/
├── DrnProgram/          # DrnProgramBase, options, actions, conventions
├── Endpoints/           # EndpointCollectionBase, PageForBase, type-safe accessors
├── Auth/                # Policies, MFA configuration, requirements
├── BackgroundServices/  # StaticAssetWarmService (pre-warm compressed assets)
├── Consent/             # GDPR cookie consent management
├── Extensions/          # Configuration, controller context, endpoint helpers
├── HealthCheck/         # WeatherForecastControllerBase for quick health checks
├── Identity/            # Identity integration and scoped user middleware
├── Middlewares/         # HttpScopeMiddleware, exception handling, security middlewares
├── Nexus/               # Nexus HTTP request and client helpers
├── RateLimiting/        # Pre-auth and post-auth rate-limit rules
├── TagHelpers/          # Razor TagHelpers (Vite, Nonce, CSRF, Auth-Only, Anon-Only)
├── Utils/               # AppStartupStatus, ServerSettings, Vite manifest, ResourceExtractor
├── Areas/               # Framework-provided Razor Pages (e.g., Error pages)
├── buildTransitive/     # NuGet publish integration
├── wwwroot/             # Framework style and script assets

Lifecycle & Execution Flow

Use configuration hooks for services and options, pipeline hooks for middleware, and DrnProgramActions for startup integration. The diagram shows startup order; the pipeline table lists the complete request sequence.

flowchart TD
    Start(["RunAsync: settings and bootstrap logging"])
    Start --> Builder["ConfigureSwaggerOptions; create builder and load settings"]
    Builder --> Configure["ConfigureApplicationBuilder; AddServicesAsync"]
    Configure --> Callback["Optional configureBuilder callback"]
    Callback --> Created["ApplicationBuilderCreatedAsync"]
    Created --> Build["Build; ConfigureApplication"]
    Build --> Built["ApplicationBuiltAsync"]
    Built --> Validate["ValidateEndpoints; ValidateServicesAsync"]
    Validate --> Validated["ApplicationValidatedAsync"]
    Validated --> Temporary{"Temporary application?"}
    Temporary -->|"Yes"| Return(["Return without starting"])
    Temporary -->|"No"| Run["StartAsync; WaitForShutdownAsync"]
    Run --> Dispose(["Dispose application"])

    %% Phase and decision styles
    classDef step fill:#FFFFFF,stroke:#37474F,stroke-width:2px,color:#263238
    classDef decision fill:#FFE0B2,stroke:#E65100,stroke-width:3px,color:#263238
    class Start,Builder,Configure,Callback,Created,Build,Built,Validate,Validated,Return,Run,Dispose step
    class Temporary decision

The four-argument CreateApplicationAsync(args, appSettings, scopeLog, configureBuilder) overload invokes its builder callback after AddServicesAsync and before ApplicationBuilderCreatedAsync. The three-argument overload omits that callback. Both return a configured application without starting it; RunAsync owns startup and shutdown.

Define at most one concrete DrnProgramActions subclass in the application assembly, with a public parameterless constructor. Its three callbacks run at the points shown above. See local development for an example.

Temporary applications build and configure the request pipeline, then enter the service-validation phase, which honors DrnDevelopmentSettings:SkipValidation. They skip endpoint validation and endpoint-accessor population, and return before the host calls StartAsync.

DrnProgramBase Deep Dive

Override these hooks to customize startup and request processing. Preserve base calls when extending defaults; replacing a hook makes the application responsible for the behavior it removes.

1. Configuration Hooks (Builder Phase)

These hooks register services or customize configuration. Options callbacks run when their options are created. Security-header callbacks run when the policy provider builds its policies.

Category Method Purpose
Builder ConfigureApplicationBuilder Register hosting services and callbacks before AddServicesAsync. Call base to retain DRN wiring.
Logging ConfigureLoggingBuilder Clear default providers, apply logging configuration, and register NLog when its section exists. See NLog for bootstrap requirements.
WebHost ConfigureWebHostBuilder Configure Kestrel options (suppresses Server header, applies optional Kestrel section, registers static web assets).
OpenAPI ConfigureSwaggerOptions Customize Swagger UI title, version, and visibility settings.
MVC ConfigureMvcBuilder Add ApplicationParts, custom formatters, or MVC/Razor options. See Razor Development.
MVC ConfigureMvcOptions Add global filters, conventions, or customize model binding.
Auth ConfigureAuthorizationOptions Define policies; both default and fallback policies require MFA.
Security ConfigureDefaultSecurityHeaders Define global headers (HSTS, CSP, FrameOptions).
Security ConfigureDefaultCsp Customize CSP directives (Script, Image, Style sources).
Security ConfigureDefaultCspBase Customize shared CSP directives such as base URI, form actions, and frame ancestors.
Security ConfigureSecurityHeaderPolicyBuilder Advanced conditional security policies (e.g., per-route CSP).
Cookies ConfigureCookiePolicy Set GDPR consent logic and security attributes for all cookies.
Cookies ConfigureCookieTempDataProvider Configure TempData cookie settings (HttpOnly, IsEssential).
Identity ConfigureIdentityRenewal(IServiceCollection, IAppSettings); ConfigureSecurityStampValidatorOptions(SecurityStampValidatorOptions, IAppSettings, AuthenticationClaimConfig) Register/customize Identity cookie renewal while retaining shared claim configuration.
Infras. ConfigureStaticFileOptions Customize static-file serving, one-year public caching, and HTTPS compression.
Infras. ConfigureForwardedHeadersOptions Configure proxy/load-balancer header forwarding.
Infras. ConfigureRequestLocalizationOptions Configure culture providers and supported cultures.
Infras. ConfigureHostFilteringOptions Configure allowed hosts for host header validation.
Infras. ConfigureResponseCachingOptions Set cache limits; defaults to a 16 MiB maximum body size and case-insensitive paths.
Infras. ConfigureResponseCompressionOptions Configure MIME types and HTTPS eligibility. See compression defaults.
Infras. ConfigureCompressionProviders Configure Brotli and Gzip provider options.
Infras. ConfigureBrotliCompressionLevel, ConfigureGzipCompressionLevel Set provider levels; both default to SmallestSize.
Global AddServicesAsync Required application service registration hook.

Razor Development

DRN uses Razor SDK build-time and publish-time compilation. For local .cshtml iteration, use IDE Hot Reload or dotnet watch instead of Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation; runtime compilation is obsolete in .NET 10 and disables Hot Reload.

References:

2. Pipeline Hooks (Application Phase)

With DrnDefaults, ConfigureApplication installs the following sequence. Rows without a hook name are framework middleware between hooks.

Order Hook or middleware Default behavior
1 ConfigureApplicationPipelineStart Forwarded headers, host filtering, cookie policy, security headers, and HSTS outside Development.
2 ConfigureApplicationPreScopeStart Response caching, response compression, then static files. Caching can store compressed variants.
3 HttpScopeMiddleware Request scope, logging, buffering, exception handling, and response controls.
4 ConfigureApplicationPostScopeStart Empty hook for middleware needing the request scope before routing.
5 UseRouting Select the endpoint and its metadata.
6 PreAuthRateLimitingMiddleware Pre-auth rules when rate limiting is enabled.
7 ConfigureApplicationPreAuthentication Request localization when enabled.
8 UseAuthentication, then ScopedUserMiddleware Authenticate and populate the scoped user.
9 UseRateLimiter Post-auth rules and named policies when rate limiting is enabled.
10 ConfigureApplicationPostAuthentication MFA exemption middleware, then MFA redirection middleware, each when configured.
11 UseAuthorization Enforce endpoint authorization.
12 ConfigureApplicationPostAuthorization Map Swagger and add its UI when enabled.
13 MapApplicationEndpoints Map controllers and Razor Pages. Override to add other endpoints, such as hubs.

Static files served at step 2 bypass the request scope, authentication, authorization, and rate limiting. Serve only public assets through this stage.

3. Verification Hooks

Hook Purpose
ValidateEndpoints Binds and validates controller endpoint accessors against mapped routes and records mapped page endpoints.
ValidateServicesAsync Scans the container for [Attribute] based registrations and ensures they are resolvable at startup via ValidateServicesAddedByAttributesAsync.

For MFA hooks and examples, see MFA.

4. Properties

Property Default Purpose
AppBuilderType DrnDefaults DrnDefaults applies the complete DRN hosting and security pipeline. Empty, Slim, and Default are advanced opt-out modes; the application must configure its required services, middleware, and endpoints.
DrnProgramSwaggerOptions (Object) Toggles Swagger generation. Defaults to IsDevelopmentEnvironment.
NLogOptions (Object) Controls NLog bootstrapping (e.g., replace logger factory).

DrnProgramBase also automatically registers the public IEndpointAccessor dependency-injection contract as a singleton. Consumers can inject IEndpointAccessor to query the endpoint collections (Endpoints, ApiEndpoints, PageEndpoints, and PageEndpointByPaths) populated by ValidateEndpoints before service validation (ValidateServicesAsync) runs.

Configuration

Layering

Later sources override earlier ones. Keep local connection strings in User Secrets rather than committed settings files.

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. User Secrets when the application assembly can be loaded
  4. Environment Variables (ASPNETCORE_, DOTNET_, then unprefixed)
  5. Mounted Directories (default: /appconfig)
  6. Command Line Arguments

Environment is required and must be Development, Staging, or Production. Bootstrap resolves it from appsettings.json, environment variables, mounted settings, or command-line arguments before loading environment-specific settings or User Secrets. Missing, NotDefined, or unknown values fail startup with ConfigurationException.

Host Filtering

AllowedHosts must be configured outside Development and cannot be *. Development may fall back to * for local convenience; production and staging should use explicit host names such as example.com;api.example.com.

Reference Configurations

NLog (Logging)

Minimal NLog configuration for console output. Add and route a Graylog target if your deployment uses Graylog. Logging providers are cleared and NLog is added during host construction only when the NLog configuration section exists, while RunAsync still requires the section for bootstrap logging.

{
  "NLog": {
    "throwConfigExceptions": true,
    "targets": {
      "async": true,
      "console": {
        "type": "Console",
        "layout": "${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}"
      }
    },
    "rules": [
      { "logger": "*", "minLevel": "Info", "writeTo": "console" }
    ]
  }
}
Kestrel (Server)
{
  "Kestrel": {
    "EndpointDefaults": { "Protocols": "Http1" },
    "Endpoints": {
      "All": { "Url": "http://*:5988" }
    }
  }
}
Forwarded Headers (Reverse Proxy & Gateway)

ConfigureForwardedHeadersOptions configures ASP.NET Core ForwardedHeadersOptions for reverse proxy, load balancer, and gateway header forwarding.

DRN trusts loopback and RFC 1918 networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) with ForwardLimit = 2. This supports private-network proxies. Without trusted forwarding, requests can share the proxy IP and its rate-limit quota.

  • Remove private-network defaults with TrustPrivateNetworks: false:

    {
      "ForwardedHeaders": {
        "TrustPrivateNetworks": false
      }
    }
    

    With no other entries configured, this retains loopback (127.0.0.0/8, ::1/128).

  • Configure networks and proxies:

    {
      "ForwardedHeaders": {
        "ForwardLimit": 2,
        "KnownIPNetworks": [
          "10.244.0.0/16",
          { "BaseAddress": "192.168.1.0", "PrefixLength": 24 }
        ],
        "KnownProxies": [ "10.0.0.100" ]
      }
    }
    

    A nonempty KnownIPNetworks list replaces the network defaults. KnownProxies adds individual proxies and does not clear existing proxies or networks. For an exact allowlist, override the options callback and clear both collections before adding trusted entries.

Security Features

The defaults in this section apply to AppBuilderType.DrnDefaults when the base lifecycle hooks are preserved. Other builder modes require the application to configure its complete security and middleware pipeline.

MFA

MFA is required by default, including endpoints with role or named policies.

Use [AllowAnonymous] for public endpoints such as login. Authenticated endpoints have two separate MFA exemption paths:

Path Requirement
AuthPolicy.MfaExempt Authentication without MFA. Add a scheme restriction if required.
ConfigureMFAExemption An eligible scheme must also be selected by the endpoint policy or default authentication scheme.

For restricted enrollment or challenge access, use the Identity policies below. They add scheme and credential-state checks.

Sample and Nexus register Identity.BearerAndApplication as their default through AddIdentityApiEndpoints. It tries bearer authentication first, then application cookies when no bearer token is present. An invalid bearer token does not fall back to cookies. See the ASP.NET Core implementation.

Configuration

Configure MFA behavior by overriding these hooks in your DrnProgramBase implementation:

// Page accessors from Sample.Hosted.
protected override MfaRedirectionConfig ConfigureMFARedirection()
    => new(
        mfaSetupUrl: Get.Page.User.Management.EnableAuthenticator,
        mfaLoginUrl: Get.Page.User.LoginWith2Fa,
        loginUrl: Get.Page.User.Login,
        logoutUrl: Get.Page.User.Logout,
        appPages: Get.Page.All
    );

// Optional; ordinary Identity applications need no claim override.
protected override AuthenticationClaimConfig ConfigureAuthenticationClaims()
    => AuthenticationClaimConfig.Default;

// Eligible schemes must also be selected for authentication.
protected override MfaExemptionConfig ConfigureMFAExemption()
    => new() { ExemptAuthSchemes = ["ApiKey", "Certificate"] };

ConfigureAuthenticationClaims defaults to Identity claim types and amr=mfa. See Renewal and assurance for custom mappings and their migration requirements.

For example, register ApiKey, add it to the exemption list, and select it in an API policy that requires an API scope. Listing ApiKey alone does not let it access default endpoints using Identity.BearerAndApplication. Programmatic MFA checks without an HTTP policy context require completed MFA.

Shared MFA authorization does not require ASP.NET Core Identity or its database. External providers need trusted authentication handlers and claim mapping. Local browser redirection is opt-in; return null from ConfigureMFARedirection to omit it.

Absent or incomplete authentication configuration

Register an effective authentication, challenge, and forbid scheme through AddAuthentication or endpoint policies. With multiple schemes, select them explicitly. A missing challenge/forbid scheme causes a server configuration error (HTTP 500), not an authentication response. An app can start and serve [AllowAnonymous] endpoints without schemes.

References: MVC registration, scheme selection, authentication errors.

Renewal and assurance

Register the Identity sign-in integration and MFA policies through the standard builder:

services.AddIdentityApiEndpoints<AppUser>()
    .AddSignInManager<DrnSignInManager<AppUser>>();
services.AddDrnIdentityMfaPolicies();

AddDrnIdentityMfaPolicies is in DRN.Framework.Hosting.Identity. IdentityMfaPolicy.Enrollment uses the Identity cookie/bearer composite. BrowserEnrollment and Challenge use application cookies only. Pass identityApiScheme to select an equivalent application-owned composite.

Derive custom sign-in managers from DrnSignInManager<TUser> to retain DRN's renewal behavior. When customizing ConfigureSecurityStampValidatorOptions, set callbacks before calling base. Omit ConfigureIdentityRenewal wiring only when Identity cookie renewal is unused.

Refresh preserves the original authentication age; it does not count as fresh MFA. MfaPrincipal.IsRecent and IsPhishingResistant are opt-in checks requiring supporting evidence from the authentication provider.

For a provider with custom claim names:

protected override AuthenticationClaimConfig ConfigureAuthenticationClaims() => new()
{
    Subject = new("sub"), Name = new("preferred_username"), Email = new("email"),
    Roles = new("roles"), Mfa = new("acr", "urn:example:mfa")
};

Replace the example MFA value with one guaranteed by your provider. Custom subject, name, email, role, and MFA mappings also replace their corresponding aliases. Configure the validating handler to issue these claims and native name/role mappings. This hook does not implement provider login or refresh.

Use this hook rather than changing IdentityOptions.ClaimsIdentity separately. Changing subject types may require reauthentication. See the Utils claim contract.

Audit events

With global MFA enabled, these Information-level events support log filters and alerts.

Event name ID Meaning
MfaAuthorizationChallenge 7401 Authentication challenge.
MfaAuthorizationForbid 7402 Access denied.
MfaAuthorizationExemption 7403 Effective policy or scheme exemption.

HostingLogEvents in DRN.Framework.Hosting.Logging exposes these public EventId fields. See Logging conventions for consumer catalogs and filtering.

MFA decisions use the shared scope-event API:

scopedLog.WithEvent(new ScopeEvent(
    HostingLogEvents.MfaAuthorizationForbid,
    Outcome: "forbid",
    Reason: "mfa_required"));

ScopeEvent comes from DRN.Framework.Utils.Logging. Filter audit events by logger category and event ID; use scoped logs for request diagnostics. Request logs may contain identifiers, so configure retention accordingly.

Identity Revocation Contract

Under the default Identity handlers, credential revocation depends on the credential type:

Credential After a persisted security-stamp change
Refresh token The next /Refresh request rejects it; expiration is checked independently.
Application cookie Rejected on the next request eligible for stamp validation: elapsed time since ticket issuance must be greater than SecurityStampValidatorOptions.ValidationInterval. This is request-driven, not a background deadline.
Opaque bearer access token Remains usable until its own expiration, unless the application adds rejection checks. Stamp rotation alone does not invalidate it.

Cookie timing follows SecurityStampValidator; access-token timing follows BearerTokenHandler. Configured handlers, stores, validation intervals and token lifetimes can change these bounds.

UpdateSecurityStampAsync, factor enable/disable, authenticator-key reset, and password reset rotate the stamp. Recovery-code generation and redemption do not. To revoke sessions after recovery, rotate the stamp and account for outstanding access-token lifetime. See UserManager.

Identity API MFA Setup Flow

With global MFA enabled, password login without an enrolled factor issues a five-minute MfaSetupRequired credential:

  • Cookie requests: Return an empty HTTP 200 response and set a non-persistent cookie with refresh disabled.
  • Bearer requests: Return an HTTP 200 AccessTokenResponse containing the setup access token, ExpiresIn = 300, and an empty RefreshToken.

Use the credential with IdentityManagementControllerBase.TwoFactorAuth to retrieve a shared key and enroll with a valid code. Discard it after enrollment. Call Login again with the password and an authenticator or recovery code to obtain completed MFA.

A setup credential does not grant normal MFA-protected access.

With global MFA disabled, users without a factor can enroll using an ordinary login credential. After enrollment, factor management requires completed MFA; denied operations return HTTP 403. Other management endpoints retain normal MFA requirements. Register the Identity policies as shown in Renewal and assurance.

Remaining MFA work

Fresh step-up/replay protection, recovery workflows, passkeys, provider-specific OIDC integrations, and factor/recovery audit events are not complete. See the implementation roadmap before relying on these capabilities.

Disabling global MFA

In your existing DrnProgramBase subclass, replace both default policies with an authenticated-user policy. Explicit MFA policies still apply. Remove any redirection or exemption overrides, or return null from them.

protected override void ConfigureAuthorizationOptions(AuthorizationOptions options)
{
    base.ConfigureAuthorizationOptions(options);
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
    options.DefaultPolicy = policy;
    options.FallbackPolicy = policy;
}

Browser security

DRN generates a request-specific cryptographic nonce.

  • Baseline: default-src 'none'; script elements require the request nonce. Other directives permit same-origin styles, images, fonts, connections, media, manifests, and workers, plus inline style attributes, data: images and fonts, and blob: workers.
  • Automatic Protection: Inline scripts and inline style elements without a matching nonce are blocked. Inline style attributes remain allowed by the default policy.
  • Usage: Activate and use the NonceTagHelper below to add the request nonce.

Standard security headers are injected into responses:

  • HSTS: Strict-Transport-Security (2 years, includes subdomains) outside Development.
  • FrameOptions: DENY (prevents clickjacking).
  • ContentTypeOptions: nosniff.
  • ReferrerPolicy: strict-origin-when-cross-origin.
  • Cross-Origin: COOP same-origin, COEP credentialless, and CORP same-site.
  • PermissionsPolicy: Secure default directives with fullscreen limited to self.

Cookies use SameSite=Strict. Secure is Always outside Development and SameAsRequest in Development. Antiforgery and TempData cookies are HttpOnly; the global policy does not force it for client-readable cookies.

CheckConsentNeeded = true withholds nonessential response cookies until consent is granted. ConsentContext exposes the current request's preferences. Applications decide which scripts need consent and which cookies are essential.

For example, load an application-owned analytics entry only after analytics consent:

@using DRN.Framework.Hosting.Consent

@if (ConsentContext.ConsentCookie.Values.AnalyticsConsent == true)
{
    <script src="buildwww/app/js/analytics.js"
            crossorigin="anonymous"></script>
}

Route-specific security headers

Leading and trailing slashes are removed from the configured Swagger UI prefix before both middleware routing and CSP selection; /docs/ becomes docs.

When Swagger is enabled, its UI RoutePrefix must be nonempty after trimming slashes, for example swagger, docs/swagger, or api-docs. Validation runs after ConfigureSwaggerUIOptionsAction. A null, empty, or slash-only prefix fails startup with ConfigurationException: a root mount would apply Swagger CSP across the application.

The complete configured subtree is reserved for CspFor.CspPolicySwagger, including application endpoints beneath it. Matching is case-insensitive and respects path segment boundaries. Keep ordinary application pages outside that subtree. Disabling Swagger leaves application policies unchanged.

The Swagger policy allows scripts from 'self' and styles from 'self' 'unsafe-inline' to support inline SVG styles without changing the generated document or its caching. It replaces the shared style-src directive. Self/inline endpoint policies retain their nonce-based styles. Replace CspFor.CspPolicySwagger through builder.AddPolicy after calling base to customize Swagger independently; selecting that name through CspPolicyName does not opt unrelated endpoints into it.

Customize security headers for specific routes by overriding ConfigureSecurityHeaderPolicyBuilder. This example replaces the base policy selector: routes outside /legacy receive the default policy, including Swagger routes. Preserve the base selection rules too if the application uses Swagger or endpoint-selected CSP policies.

protected override void ConfigureSecurityHeaderPolicyBuilder(
    SecurityHeaderPolicyBuilder builder,
    IServiceProvider serviceProvider,
    IAppSettings appSettings)
{
    base.ConfigureSecurityHeaderPolicyBuilder(builder, serviceProvider, appSettings);
    
    // Allow legacy inline scripts on /legacy routes.
    var legacyPolicy = new HeaderPolicyCollection();
    ConfigureDefaultSecurityHeaders(legacyPolicy, serviceProvider, appSettings);
    legacyPolicy.Remove("Content-Security-Policy");
    legacyPolicy.AddContentSecurityPolicy(csp =>
    {
        ConfigureDefaultCspBase(csp);
        csp.AddFrameAncestors().Self();
        csp.AddScriptSrc().Self().UnsafeInline(); // Only for selected legacy routes
    });
    builder.AddPolicy("legacy-inline-csp", legacyPolicy);
    builder.SetPolicySelector(selector =>
        selector.HttpContext.Request.Path.StartsWithSegments("/legacy")
            ? selector.ConfiguredPolicies["legacy-inline-csp"]
            : selector.DefaultPolicy);
}

Rate Limiting

DRN Hosting adds two composable limiter phases:

  • Pre-auth runs after routing and before authentication. It evaluates singleton rules only and uses a coarse IP default to reject obvious abuse before auth and MFA work. Add a custom singleton rule for trusted-header partitioning behind a correctly configured edge proxy.
  • Post-auth runs after ScopedUserMiddleware. It can use singleton and scoped rules, including user, tenant, account, claim, or endpoint partitions.

Defaults are token buckets: 1,000 tokens/minute per pre-auth IP partition and 100 tokens/minute per post-auth user partition. Post-auth uses the authenticated subject and authentication type; requests without a usable authenticated subject fall back to IP. Rejections return 429 Too Many Requests; Retry-After is included only when the rejecting limiter supplies retry metadata.

DRN's built-in limiter state is process-local. In horizontally scaled production deployments, enforce coarse limits at the edge (WAF/CDN/API gateway/load balancer) or add a distributed/custom limiter for quotas that must hold across every application instance.

Endpoint metadata behavior:

  • [DisableRateLimiting] bypasses DRN pre-auth and post-auth limiting, plus ASP.NET Core post-auth policies.
  • [EnableRateLimiting("policy-name")] selects ASP.NET Core named post-auth policies. DRN pre-auth remains global; DRN rules with matching PolicyName compose with the named policy.
  • Static files served before routing are naturally outside the limiter path.

When many users share an edge IP, raise the pre-auth quota or use a trusted-header rule. Use scoped post-auth rules for account or tenant quotas, as shown below.

Settings Quick Reference

Configure defaults under DrnAppFeatures:DrnRateLimit. Read them through IAppSettings.Features.RateLimit. Changes require restart. Shared bucket values must be positive; phase overrides can be 0 to inherit them.

Setting group Default Used by Meaning
Disabled false Both phases Disables DRN pre-auth and post-auth rate limiting.
PartitionLogMode KeyedHash Both phases Controls rejected IP and partition logging. See Telemetry for hash format and privacy limits.
TokenLimit, ReplenishmentSeconds, TokensPerPeriod 100, 60, 100 Shared fallback Base token bucket values for both phases.
PreAuthTokenLimit, PreAuthReplenishmentSeconds, PreAuthTokensPerPeriod 1000, 60, 1000 Pre-auth Coarse IP limits before authentication. 0 inherits the shared value.
PostAuthTokenLimit, PostAuthReplenishmentSeconds, PostAuthTokensPerPeriod 0, 0, 0 Post-auth Authenticated user or anonymous IP limits after ScopedUserMiddleware. 0 inherits the shared value.
Rule Extension Points

Add rules by deriving from SingletonRateLimitRule or ScopedRateLimitRule; the base classes include attribute-based DI registration. Direct interface implementations must opt into multi-registration with [Singleton<ISingletonRateLimitRule>(tryAdd: false)] or [Scoped<IScopedRateLimitRule>(tryAdd: false)].

Rules run by ascending Order; framework defaults run last. Matching rules compose through .NET's chained limiter, so tenant + user + IP policies can all apply to one request. ScopedRateLimitRule is post-auth only.

Return value Effect
null Rule does not apply.
RateLimitRuleResult.TokenBucket(key, ...) Applies a token bucket to this partition.
RateLimitRuleResult.AllowRequest("partition-key") Skips later rules in this phase. Earlier limits and native policies still apply.
RateLimitRuleResult.DenyRequest("partition-key") Rejects immediately with 429.
Any result with stopRemainingRules: true Applies this result and skips later rules.

Partition helpers include TokenBucket, FixedWindow, SlidingWindow, ConcurrencyLimiter, and CustomPartition. RateLimitRuleResult.Action is Limit, Allow, or Deny; StopRemainingRules only controls whether later rules compose after this result.

Set PolicyName to match [EnableRateLimiting("policy-name")], or leave it null for a global rule. Empty names are invalid. Native policies registered through AddRateLimiter run alongside DRN rules. A rejecting DRN rule receives OnRejectedAsync; native policy rejections use the ASP.NET Core callback.

Use ShortCircuitOnMatch and lower Order for allow/deny rules that must bypass quota checks. Rules with the same Order evaluate short-circuit rules first; if a short-circuit rule returns null, later rules still evaluate.

Partition option factories are cached by .NET per partition key. Do not capture HttpContext or scoped services inside factory lambdas; pass only immutable values.

Dynamic tenant plans belong in rules, not global settings. Rule evaluation is synchronous, so do not perform database, Redis, or HybridCache I/O inside EvaluatePreAuth / EvaluatePostAuth. Load plan data earlier in the request or maintain an in-memory snapshot refreshed in the background. HybridCache and IDistributedCache can share policy data, but they are not hard distributed counters by themselves.

// Sample.Hosted/Helpers/RateLimitFor.cs
public class RateLimitFor
{
    public string? AccountPartition => Get.Claim.Account.Id == null ? null : $"account:{Get.Claim.Account.Id:N}";
    public string? TenantPartition => Get.Claim.Tenant.Id == null ? null : $"tenant:{Get.Claim.Tenant.Id:N}";
}

public class AccountRateLimitRule(DrnAppFeatures features) : ScopedRateLimitRule
{
    public override RateLimitRuleResult? EvaluatePostAuth(HttpContext context)
    {
        var partitionKey = Get.RateLimit.AccountPartition;
        if (partitionKey == null)
            return null;

        var tokenLimit = features.RateLimit.TokenLimit;
        var period = TimeSpan.FromSeconds(features.RateLimit.ReplenishmentSeconds);
        var tokensPerPeriod = features.RateLimit.TokensPerPeriod;
        return RateLimitRuleResult.TokenBucket(partitionKey, _ => new TokenBucketRateLimiterOptions
        {
            TokenLimit = tokenLimit,
            ReplenishmentPeriod = period,
            TokensPerPeriod = tokensPerPeriod,
            QueueLimit = 0,
            AutoReplenishment = true
        });
    }
}
Telemetry

DRN emits metrics through the DRN.Framework.Hosting.RateLimiting meter:

Metric DRN coverage
drn.rate_limiting.requests Pre-auth lease acquisition attempts
drn.rate_limiting.rejections Pre-auth and post-auth rejections
drn.rate_limiting.active_request_leases Active pre-auth leases
drn.rate_limiting.request_lease.duration Pre-auth lease duration in seconds

All instruments use drn.rate_limiting.phase, aspnetcore.rate_limiting.policy, aspnetcore.rate_limiting.result, and drn.rate_limiting.action. The action is limit, allow, deny, or unknown. drn.rate_limiting.rule is added when a DRN rule is known; native policy rejections have no DRN rule tag. ASP.NET Core supplies post-auth request and lease metrics.

By default, rate-limit-specific IP and partition fields are written as deterministic keyed hashes with a blake3-keyed: prefix. This supports correlation but does not anonymize the complete request log; standard request and user fields may still contain raw identifiers. Treat logs as sensitive, and enable PlainText only for controlled development or a dedicated encrypted audit sink.

Overriding Defaults

Override CreatePreAuthRateLimiter or ConfigurePostAuthRateLimiterOptions in DrnProgramBase to change global algorithms, add named policies, or preserve custom RateLimiterOptions callbacks:

protected override void ConfigurePostAuthRateLimiterOptions(
    RateLimiterOptions options,
    IServiceProvider serviceProvider,
    IAppSettings appSettings)
{
    base.ConfigurePostAuthRateLimiterOptions(options, serviceProvider, appSettings);
    options.AddTokenBucketLimiter("strict", opt =>
    {
        opt.TokenLimit = 10;
        opt.ReplenishmentPeriod = TimeSpan.FromSeconds(60);
        opt.TokensPerPeriod = 10;
        opt.QueueLimit = 0;
    });
}
References

Endpoint Management

DRN provides compile-time-typed accessor members for controller endpoints and Razor Page paths. Controller endpoint accessors are bound to mapped routes and validated at startup. Razor Page accessors are convention-generated path strings; keep them synchronized with page routes.

1. Define Accessors

Create application-owned endpoint and page collections:

public sealed class AppEndpoints : EndpointCollectionBase<Program>
{
    public UserEndpoints User { get; } = new();
}

public sealed class UserEndpoints()
    : ControllerForBase<UserController>("/Api/User/[controller]")
{
    // Property names match controller action method names.
    public ApiEndpoint Login { get; private set; } = null!;
    public ApiEndpoint Profile { get; private set; } = null!;
}

public sealed class AppPages : PageCollectionBase<AppPages>
{
    public UserPages User { get; } = new();
}

public sealed class UserPages : PageForBase
{
    protected override string[] PathSegments { get; } = ["User"];
    public string Login { get; init; } = string.Empty;
}

public static class Get
{
    public static AppEndpoints Endpoint { get; } =
        (AppEndpoints)EndpointCollectionBase<Program>.EndpointCollection!;

    public static AppPages Page { get; } =
        PageCollectionBase<AppPages>.PageCollection;
}

2. Usage in Code

Use the typed accessor members with IDE completion:

// Get the typed endpoint object
ApiEndpoint endpoint = Get.Endpoint.User.Login;

// Return the mapped controller route.
string url = endpoint.Path();

// For an action route containing {id:guid}
Guid userId = Guid.NewGuid();
string profileUrl = Get.Endpoint.User.Profile.Path(userId);
<a asp-page="@Get.Page.User.Login">Log in</a>

Razor TagHelpers

Activate the framework TagHelpers in Pages/_ViewImports.cshtml:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, DRN.Framework.Hosting

Without the DRN directive, Vite resolution, CSP nonces, HTMX CSRF headers, active-page marking, and visibility helpers do not run.

TagHelper Target Purpose
ViteScriptTagHelper <script src="buildwww/..."> Resolves Vite manifest entries and adds subresource integrity (SRI).
ViteLinkTagHelper <link href="buildwww/..."> Resolves Vite manifest entries for CSS assets, adds SRI.
NonceTagHelper <script>, <style>, <link>, <iframe> Automatically injects the request-specific CSP nonce.
CsrfTokenTagHelper hx-post, hx-put, hx-delete, hx-patch, or add-csrf-token Adds RequestVerificationToken to hx-headers. On matched elements, disable-csrf-token opts out.
AuthorizedOnlyTagHelper *[authorized-only] Renders the element only if the user is authenticated.
AnonymousOnlyTagHelper *[anonymous-only] Renders the element only if the user is not authenticated.
PolicyOnlyTagHelper *[policy-only="PolicyName"] Renders the element only when the current request user satisfies the named authorization policy. Accepts an optional policy-resource.
PageAnchorAspPageTagHelper <a asp-page="..."> Adds active fw-bold and aria-current="page" when the link matches the current page.
PageAnchorHrefTagHelper <a href="..."> Adds active fw-bold when the href path matches the Razor page identifier. Custom URL routes may differ.
ScriptDefaultsTagHelper <script> Modern defaults: defer for external scripts, type="module" for inline scripts. Opt-out via defer="false" or explicit type.

Authorization Visibility

Use named policies for claim, role, or resource requirements instead of duplicating authorization rules in markup:

<a policy-only="ManageUsers" asp-page="/Admin/Users">Manage users</a>
<button policy-only="EditDocument" policy-resource="@Model.Document">Edit</button>
<nav authorized-only>Signed-in navigation</nav>
<a anonymous-only asp-page="/User/Login">Sign in</a>

policy-only checks a registered policy against the current user. Invalid policy names raise errors; denied elements are hidden. Supply policy-resource when the policy requires one. The helper does not sign users in or check the linked endpoint; ordinary named policies retain the default MFA requirement.

authorized-only checks ScopeContext.Authenticated; anonymous-only checks its inverse. Both are presence-only markers, so even "false" activates the filter. Write them without values, remove them to omit filtering, and use Razor conditionals for dynamic rendering. Both markers are removed from rendered HTML. If several visibility helpers apply, any one can suppress the element.

These markers do not require completed MFA. On anonymous or MFA-exempt pages, authorized-only can render for users with pending MFA or incomplete setup. Use policy-only for policy-specific visibility. All three helpers control HTML rendering only; enforce endpoint access through server authorization policies.

Vite Manifest Publish Support

Vite manifests under wwwroot/**/.vite/manifest.json are included automatically in publish output.

ViteManifest uses IWebHostEnvironment.WebRootPath, falling back to Path.Combine(environment.ContentRootPath, "wwwroot") when it is unset or whitespace.

When publishing with a custom web root, build targets do not automatically capture manifests outside wwwroot/**; applications must separately include manifests and referenced assets in publish items.

During loading, ViteManifest rejects manifests outside the resolved root and assets outside their output directory or missing from disk with ConfigurationException. Normal startup loads manifests during pipeline configuration; temporary hosts and SkipValidation skip that eager check. Asset hashes populate Integrity as base64 SHA-256 (sha256-...) for ViteScriptTagHelper and ViteLinkTagHelper.

Keep manifests and referenced assets under the application's web root, and verify CSS and JavaScript loading after publish.

Disable the publish item injection when an application owns this behavior itself:

<PropertyGroup>
  <DrnHostingViteManifestPublishItemsEnabled>false</DrnHostingViteManifestPublishItemsEnabled>
</PropertyGroup>

Developer Diagnostics

Request logging and diagnostic buffering apply wherever HttpScopeMiddleware runs. Detailed exception pages and startup reports are Development features.

Logging conventions

EventId is provided by .NET's Microsoft.Extensions.Logging. It holds a numeric ID and an optional name. Use it with a public static <Module>LogEvents catalog. For example, HostingLogEvents.MfaAuthorizationChallenge carries ID 7401 and name MfaAuthorizationChallenge.

  • Define events as public static readonly EventId fields in the owning module's Logging namespace.
  • Give each event a descriptive name and an ID unique within its module. Do not renumber, rename, or reuse published events for different meanings.
  • Filter dedicated logs by logger category and event ID. Numeric IDs are not globally unique across applications or libraries.
  • Use ScopeEvent and IScopedLog.WithEvent instead of event dictionaries. Standard fields are EventId, EventName, EventOutcome, and EventReason.
  • Consumers can define a companion catalog, such as SampleLogEvents. Reuse Hosting definitions only for the same event meaning.

Use IScopedLog for request and operation diagnostics. Direct ILogger calls are appropriate for scope flushing, bootstrap failures, and dedicated audit events. Keep request audit decisions in the scoped log too. Dedicated audit events must not include the full request log.

Every ScopedLog has a stable CorrelationId. Its TraceId is captured from an active W3C activity, or left absent. HTTP TraceIdentifier remains separate. LogScoped emits the primary event ID with the aggregate. Dedicated audit records use EventOutcome, EventReason, nullable TraceId, and CorrelationId. See Utils OpenTelemetry correlation.

Startup Exception Reports

In Development, if the application fails during RunAsync, DRN Hosting attempts to write StartupExceptionReport.html beside the application assembly. Report generation is best effort; when no report can be created, use the startup logs. Production and staging use normal startup logs only.

When generated, the report can include:

  • Full stack traces with source code highlighting (if symbols available).
  • Environment details and configuration snapshots.
  • Scoped logs leading up to the crash.

Custom Error Pages

The framework includes built-in Razor Pages for developer-time exception handling:

  • RuntimeExceptionPage: Detailed breakdown of unhandled exceptions with request state and logs.
  • CompilationExceptionPage: Visualizes Razor or code compilation errors with line-specific highlighting.

Request Body Buffering

Request diagnostics buffer POST, PUT, and PATCH bodies only when Content-Length is known and within the configured limit. Kestrel request-size limits still apply.

Configuration via DrnAppFeatures (in appsettings.json):

Key Type Default Effect
DisableRequestBuffering bool false Disables this diagnostic buffering feature
MaxRequestBufferingSize int 0 (uses 30,000) Max bytes to buffer. Values below 10,000 use the 30,000-byte fallback
{
  "DrnAppFeatures": {
    "DisableRequestBuffering": false,
    "MaxRequestBufferingSize": 50000
  }
}

Skipped reads return a reason, such as an unknown Content-Length or a length above the configured byte limit.

Modern HTTP Standards

HttpScopeMiddleware applies these defaults to responses passing through the request scope:

  • 303 See Other: Middleware converts 302 Found to 303 See Other. For example, a redirect after form submission uses GET for the destination. Use 307 or 308 when the destination must preserve the request method.
  • Secure Caching Default: Dynamic responses that do not set their own Cache-Control receive no-store, no-cache, must-revalidate. Explicit response caching directives take precedence, while static assets opt into public caching.

Static Asset Pre-Warming

StaticAssetWarmService is a best-effort hosted service that requests Vite assets after the host starts so response caching can store Brotli and Gzip variants.

Warm-up uses local server addresses only; non-loopback bindings are skipped.

Compression defaults: Static assets allow HTTPS compression and caching of eligible variants. Dynamic HTTP responses can also be compressed. Dynamic HTTPS compression is disabled (EnableForHttps = false) to mitigate BREACH. MIME types extend ASP.NET Core defaults with raw TTF/OTF fonts; WOFF and WOFF2 are already compressed.

Provider Default Level Override Hook
Brotli SmallestSize ConfigureBrotliCompressionLevel()
Gzip SmallestSize ConfigureGzipCompressionLevel()

After a successful warm-up, subsequent requests can use cached compressed variants. Warm-up runs after startup and is best effort, so early requests may perform compression themselves.

Local Development Infrastructure

Use DRN.Framework.Testing to provision Postgres during local Development without manual Docker management. The following setup keeps Testcontainers dependencies in Debug builds and explicitly enables local dependency launch.

1. Add the Debug-Only Package

NuGet consumers should reference the DRN.Framework.Testing version matching DRN.Framework.Hosting:

<ItemGroup Condition="'$(Configuration)' == 'Debug'">
    
    <PackageReference Include="DRN.Framework.Testing" Version="VERSION" />
</ItemGroup>

Repository contributors may use the sibling project reference instead.

2. Enable External Dependency Launch

Add the explicit opt-in to appsettings.Development.json:

{
  "DrnDevelopmentSettings": {
    "LaunchExternalDependencies": true
  }
}

External dependencies launch only for a Development host with this setting enabled. Test and temporary hosts are excluded.

3. Configure Startup Actions

Implement DrnProgramActions to launch the configured local dependencies.

#if DEBUG
using DRN.Framework.Testing.Extensions;

public class SampleProgramActions : DrnProgramActions
{
    public override async Task ApplicationBuilderCreatedAsync<TProgram>(
        TProgram program, WebApplicationBuilder builder,
        IAppSettings appSettings, IScopedLog scopedLog)
    {
        var options = new ExternalDependencyLaunchOptions
        {
            PostgresContainerSettings = new() 
            { 
                Reuse = true, // Faster restarts
                HostPort = 6432 // Avoid conflicts with local Postgres
            }
        };

        // When enabled, starts missing containers and updates AppSettings.
        await builder.LaunchExternalDependenciesAsync(scopedLog, appSettings, options);
    }
}
#endif

Hosting Utilities

IAppStartupStatus

Singleton gate for background services that need to wait until the host has fully started before executing.

using DRN.Framework.Hosting.Utils;
using DRN.Framework.Utils.DependencyInjection.Attributes;
using Microsoft.Extensions.Hosting;

[HostedService]
public sealed class MyWorker(IAppStartupStatus startupStatus) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        if (!await startupStatus.WaitForStartAsync(stoppingToken))
            return;

        // The application has started.
    }
}

[HostedService] is discovered when the worker's assembly is registered with AddServicesWithAttributes().

IServerSettings

Resolves bound server addresses from Kestrel. Normalizes wildcard hosts (0.0.0.0, [::], +, *) to localhost for internal self-requests. Prefers HTTP over HTTPS to avoid TLS overhead.

using DRN.Framework.Hosting.Utils;

public class MyService(IServerSettings server)
{
    public void LogAddresses()
    {
        var loopback = server.GetLoopbackAddress();   // e.g. "http://localhost:5988"
        var all = server.GetAllAddresses();            // All normalized bound addresses
    }
}

Global Usings

Suggested global usings for Hosted applications to reduce boilerplate:

global using DRN.Framework.Hosting.DrnProgram;
global using DRN.Framework.Hosting.Endpoints;
global using DRN.Framework.Utils.DependencyInjection;
global using DRN.Framework.Utils.Logging;
global using DRN.Framework.Utils.Settings;
global using Microsoft.AspNetCore.Mvc;

For complete examples, see Sample.Hosted.


Documented with the assistance of DiSC OS


Semper Progressivus: Always Progressive

Commit Info

Author: Duran Serkan KILIÇ
Date: 2026-09-09 11:24:34 +0300
Hash: 042fe6657917c7b6edc21dd2cc7879143639d9ca

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on DRN.Framework.Hosting:

Package Downloads
DRN.Framework.Testing

DRN.Framework.Testing package encapsulates testing dependencies and provides practical, effective helpers such as resourceful data attributes and test context. This package enables a new encouraging testing technique called as DTT(Duran's Testing Technique). With DTT, any developer can write clean and hassle-free unit and integration tests without complexity. ## Commit Info Author: Duran Serkan KILIÇ Date: 2026-09-09 11:24:34 +0300 Hash: 042fe6657917c7b6edc21dd2cc7879143639d9ca

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.10.0-preview002 39 9/9/2026
0.10.0-preview001 84 9/7/2026
0.9.9-preview004 96 8/27/2026
0.9.9-preview003 94 8/26/2026
0.9.9-preview002 93 8/20/2026
0.9.9-preview001 105 8/16/2026
0.9.8 144 8/12/2026
0.9.8-preview004 93 8/12/2026
0.9.8-preview003 92 8/9/2026
0.9.8-preview002 98 8/8/2026
0.9.8-preview001 107 8/8/2026
0.9.7 122 7/29/2026
0.9.6 128 7/15/2026
0.9.6-preview004 137 7/7/2026
0.9.6-preview003 126 7/1/2026
0.9.6-preview002 158 6/29/2026
0.9.6-preview001 122 6/28/2026
0.9.5 142 6/14/2026
0.9.5-preview011 119 6/14/2026
0.9.5-preview010 125 6/14/2026
Loading failed

## Version 0.10.0

### Bug Fixes

*   **NuGet Release Notes**: Package metadata includes only the latest version section, excluding historical releases and the documentation footer. Packing rejects missing version sections and release notes over 35,000 characters; the bundled Markdown retains the full history.

### New Features

*   **Hosting Log Events**: Added `HostingLogEvents` in `DRN.Framework.Hosting.Logging` for consumer filters and event references. Applications can define companion catalogs for their own events.
*   **Policy-Based Razor Visibility**: Added `policy-only="PolicyName"` with optional `policy-resource` for policy-based HTML visibility. Endpoint authorization remains required.
*   **Programmatic Application Builder Hook**: Added a four-argument `CreateApplicationAsync` overload for host integrations while retaining the existing three-argument overload for binary compatibility.
*   **Unified Authentication Claims**: Use `ConfigureAuthenticationClaims()` for subject, name, email, role, and MFA mappings. Ordinary Identity applications use the defaults; external providers need their own authentication integration.
*   **Identity Lifecycle Integration**: Added `ConfigureIdentityRenewal` and `DrnSignInManager<TUser>` for Identity sign-in and refresh integration. Register the manager explicitly, including when customizing sign-in behavior.

### Breaking Changes

*   **Swagger UI Prefix Validation**: When enabled, Swagger UI requires a `RoutePrefix` that is nonempty after trimming slashes. Root mounts now fail startup; migrate to a non-root prefix such as `swagger`, `docs/swagger`, or `api-docs`. The configured subtree is reserved for Swagger CSP.
*   **Authenticated UI Visibility Convention**: `authorized-only` now checks `ScopeContext.Authenticated` instead of completed MFA. Authenticated users with pending or incomplete MFA can see these elements on anonymous or MFA-exempt pages. Endpoint policies remain responsible for MFA enforcement; use `policy-only` for policy-specific visibility.
*   **Presence-Only Visibility Markers**: Removed the `AuthorizedOnly` and `AnonymousOnly` boolean properties from their TagHelpers. Use bare `authorized-only` / `anonymous-only` attributes; presence activates filtering regardless of the value, including `"false"`. Omit the attribute to omit its filter. Both markers are removed from rendered HTML.
*   **Policy-Scoped MFA Exemptions**: Exempt schemes must also be selected for authentication. Programmatic authorization without an HTTP policy context no longer uses scheme exemptions.
*   **Identity MFA Policy Registration**: Call `AddDrnIdentityMfaPolicies()` after registering Identity when using `IdentityManagementControllerBase`. Factor management requires completed MFA after enrollment and returns HTTP 403 when denied.

*   **Claim Configuration Consolidation**: Removed `ConfigureMFAClaim` and separate MFA DI configuration. Move the marker to `Mfa = new(type, value)` in `ConfigureAuthenticationClaims`. MFA handlers and management helpers now receive `AuthenticationClaimConfig`; recompile and migrate affected calls. `ConfigureSecurityStampValidatorOptions` receives `(SecurityStampValidatorOptions, IAppSettings, AuthenticationClaimConfig)`; forward those arguments to base. Identity claim-option overrides are superseded by the unified config. Canonical subject changes may require reauthentication; aliases do not automatically migrate existing Identity tickets.

*   **Identity MFA Setup Response**: When MFA is globally enforced, password-valid accounts without two-factor authentication now receive an HTTP 200 five-minute setup credential instead of the ordinary authenticated credential returned previously. Cookie requests receive an empty response with a non-persistent, non-refreshable setup cookie; bearer requests receive an `AccessTokenResponse` with `ExpiresIn = 300` and an empty `RefreshToken`. Clients must use the setup credential with `TwoFactorAuth`, enable two-factor authentication, discard the setup credential, and log in again with an authenticator or recovery code.

### Security

*   **Swagger Prefix Normalization**: Leading and trailing slashes are removed from the UI prefix for both Swagger routing and CSP selection, preserving redirects from `/docs` and `/docs/` to the document when configured with `/docs/`.
*   **Swagger CSP**: Added the independently replaceable `CspFor.CspPolicySwagger` policy with same-origin scripts and inline styles for Swagger SVG rendering, preserving document caching. Selection matches the validated UI prefix with path segment boundaries. Disabled Swagger retains the default CSP. Existing self/inline policies outside the Swagger subtree retain nonce-based styles.
*   **Revocation Guidance**: Documented cookie, refresh-token, and access-token revocation limits. Existing token lifetimes and default MFA requirements are unchanged.
*   **Authorization Audit Events**: Added challenge, forbid, and exemption events (7401–7403) for log filtering and alerts. Factor, recovery, and revocation events are not included.
*   **Reverse Proxy Trust & Forwarded Headers**: Binds `ForwardedHeaders` settings with CIDR and proxy parsing. Invalid formats throw `ConfigurationException`. Defaults trust RFC 1918 private networks and loopback with `ForwardLimit = 2`. `TrustPrivateNetworks = false` removes the private-network defaults. A nonempty `KnownIPNetworks` list replaces network defaults; `KnownProxies` adds entries without clearing existing trust.

---

Documented with the assistance of [DiSC OS](https://github.com/duranserkan/DRN-Project/blob/develop/.agent/rules/DiSCOS.md)

---
**Semper Progressivus: Always Progressive**