| name | drn-hosting |
| description | DRN.Framework.Hosting - DrnProgramBase for web application bootstrapping, endpoint configuration, security middleware (CSP, nonce), authentication/authorization, TagHelpers for asset management, and Razor Pages integration. Essential for web application setup and hosting. Keywords: hosting, web-application, drnprogrambase, endpoints, middleware, security, csp, nonce, authentication, authorization, taghelpers, razor-pages, mfa, background-service |
| last-updated | "2026-06-12T00:00:00.000Z" |
| difficulty | advanced |
| tokens | ~3K |
DRN.Framework.Hosting
Web application hosting with security-first design, endpoints, and middlewares.
When to Apply
- Creating new hosted applications
- Configuring security (CSP, Auth, MFA)
- Working with endpoints and Razor Pages
- Adding or customizing middlewares
- Using TagHelpers for frontend rendering
DrnProgramBase Pattern
All DRN web apps inherit from DrnProgramBase<TProgram>:
public class SampleProgram : DrnProgramBase<SampleProgram>, IDrnProgram
{
public static async Task Main(string[] args) => await RunAsync(args);
protected override Task AddServicesAsync(
WebApplicationBuilder builder,
IAppSettings appSettings,
IScopedLog scopedLog)
{
builder.Services.AddSampleHostedServices(appSettings);
return Task.CompletedTask;
}
protected override MfaRedirectionConfig ConfigureMFARedirection()
=> new(Get.Page.User.Management.EnableAuthenticator,
Get.Page.User.LoginWith2Fa,
Get.Page.User.Login,
Get.Page.User.Logout,
Get.Page.All);
}
Builder Phase Hooks
| Method | Purpose |
|---|
AddServicesAsync() | [Required] Add services to DI |
ConfigureSwaggerOptions() | Customize Swagger/OpenAPI |
ConfigureApplicationBuilder() | Root builder customization |
ConfigureMvcBuilder() | IMvcBuilder customization; Razor edit loops use IDE/dotnet watch Hot Reload, not runtime compilation |
ConfigureDefaultSecurityHeaders() | CSP and security header policies |
ConfigureDefaultCsp() | Customize CSP directives |
ConfigureSecurityHeaderPolicyBuilder() | Route-specific security policies |
ConfigureAuthorizationOptions() | Authorization policy config |
ConfigureCookiePolicy() | GDPR and consent cookie settings |
ConfigureStaticFileOptions() | Static file serving and caching |
ConfigureResponseCachingOptions() | Response caching (16MB limit, case insensitive) |
ConfigureResponseCompressionOptions() | Compression MIME types, HTTPS=false (BREACH prevention) |
ConfigureCompressionProviders() | Brotli + Gzip provider setup |
ConfigureBrotliCompressionLevel() | Brotli level (default: SmallestSize) |
ConfigureGzipCompressionLevel() | Gzip level (default: SmallestSize) |
ConfigureMvcOptions() | MvcOptions configuration |
ConfigureForwardedHeadersOptions() | Forwarded headers (default: All) |
ConfigureRequestLocalizationOptions() | Localization cultures, cookie provider |
ConfigureHostFilteringOptions() | Allowed hosts from config |
ConfigureSecurityStampValidatorOptions() | Security stamp refresh with AMR claim preservation |
ConfigureDefaultCspBase() | Base CSP directives (base-uri, form-action, frame-ancestors) |
ConfigureCookieTempDataProvider() | TempData cookie settings |
CreatePreAuthRateLimiter() | Pre-auth rate limiter orchestration |
ConfigurePostAuthRateLimiterOptions() | Post-auth rate limiter orchestration and policies |
Application Phase Hooks
| Method | Purpose |
|---|
ConfigureApplicationPipelineStart() | Earliest middleware (HSTS, Security Headers) |
ConfigureApplicationPreScopeStart() | Pre-logger (Static files) |
ConfigureApplicationPostScopeStart() | After HttpScopeMiddleware |
ConfigureApplicationPreAuthentication() | Before Auth (Localization) |
ConfigureApplicationPostAuthentication() | Post-Auth (MFA Redirection) |
ConfigureApplicationPostAuthorization() | Post-AuthZ (Swagger UI) |
MapApplicationEndpoints() | Route mapping (Controllers, Razor Pages) |
ValidateEndpoints() | Post-mapping endpoint validation |
ValidateServicesAsync() | DI validation |
ConfigureMFARedirection() | MFA page configuration |
ConfigureMFAExemption() | Route-specific MFA exemption config |
Execution order: Builder Phase → builder.Build() → Pipeline Phase → ValidateEndpoints → ValidateServices → application.RunAsync()
Advanced Startup (DrnProgramActions)
Intercept startup without modifying main program class:
public class SampleProgramActions : DrnProgramActions
{
public override async Task ApplicationBuilderCreatedAsync<TProgram>(
TProgram program, WebApplicationBuilder builder,
IAppSettings appSettings, IScopedLog scopedLog)
{
}
public override async Task ApplicationValidatedAsync<TProgram>(
TProgram program, WebApplication application,
IAppSettings appSettings, IScopedLog scopedLog)
{
}
}
Configuration Properties
| Property | Description |
|---|
AppBuilderType | Controls builder creation (Empty, Slim, Default, DrnDefaults) |
DrnProgramSwaggerOptions | OpenAPI and Swagger UI config |
Security Features
MFA by Default
MFA enforced globally via FallbackPolicy. Any route not opted-out requires MFA.
[AllowAnonymous]
[Authorize(Policy = AuthPolicy.MfaExempt)]
protected override void ConfigureAuthorizationOptions(AuthorizationOptions options) { }
GDPR & Consent
ConsentCookie: Manages user consent state via secure HttpOnly cookie
ScopedUserMiddleware: Populates IScopedLog with ConsentGranted status
Per-Route Security Headers
protected override void ConfigureSecurityHeaderPolicyBuilder(HeaderPolicyCollection policies, IAppSettings appSettings)
{
policies.AddPolicy("AllowExternalScripts", builder =>
builder.AddContentSecurityPolicy(csp =>
csp.AddScriptSrc().Self().From("https://cdn.example.com")));
}
Page & Endpoint Management
PageCollectionBase
public class SamplePageFor : PageCollectionBase<SamplePageFor>
{
public RootPageFor Root { get; } = new();
public UserPageFor User { get; } = new();
}
public class UserPageFor : PageForBase
{
protected override string[] PathSegments { get; } = ["User"];
public string Login { get; init; } = string.Empty;
public string Register { get; init; } = string.Empty;
public UserProfilePageFor Profile { get; } = new();
}
<a asp-page="@Get.Page.User.Login">Log In</a>
EndpointCollectionBase
public class SampleEndpointFor : EndpointCollectionBase<SampleProgram>
{
public QaApiFor Qa { get; } = new();
}
public class TagFor() : ControllerForBase<TagController>(QaApiFor.ControllerRouteTemplate)
{
public ApiEndpoint GetAsync { get; private set; } = null!;
public ApiEndpoint PostAsync { get; private set; } = null!;
}
Middlewares
| Middleware | Purpose |
|---|
HttpScopeMiddleware | Request/response logging with IScopedLog, TraceId, duration |
PreAuthRateLimitingMiddleware | Early abuse throttling before authentication |
ScopedUserMiddleware | Populates IScopedLog with user identity and consent |
MfaRedirectionMiddleware | Redirect users without MFA to setup page |
MfaExemptionMiddleware | Exempt specific routes/schemes from MFA |
Rate Limiting Rules
- Derive from
SingletonRateLimitRule / ScopedRateLimitRule for automatic attribute-based DI registration.
- Return
null when the rule does not apply; return RateLimitRuleResult.TokenBucket(...), FixedWindow(...), SlidingWindow(...), ConcurrencyLimiter(...), CustomPartition(...), AllowRequest(...), or DenyRequest(...) when it applies.
RateLimitRuleResult.Action is Limit, Allow, or Deny; StopRemainingRules only controls whether later rules compose after this result.
- Lower
Order runs first. Matching rules compose through .NET's native chained limiter, so tenant + user + IP can all apply; framework defaults use int.MaxValue.
- Override
ShortCircuitOnMatch for same-order allow/deny rules that must run before normal same-order quota rules; use lower Order when they must bypass earlier singleton or scoped quotas. If they return null, later rules still evaluate. If they return a result, that result decides the action and remaining rules are skipped.
- The pre-auth middleware honors ASP.NET Core
[DisableRateLimiting] endpoint metadata; use it for trusted health checks or operational endpoints that must never consume quota. [EnableRateLimiting] does not bypass the global pre-auth limiter.
- Default post-auth partitioning uses stable user id claims (
NameIdentifier/sub) with auth scheme, not mutable display names.
- Use scoped rules plus
IScopedUser for post-auth claim-aware partitions. Prefer RateLimitFor (or app-owned wrappers around RateLimitFor) over repeated HttpContext.User parsing.
- Set
PolicyName on a rule only when it should run for endpoints marked with matching ASP.NET Core [EnableRateLimiting("policy-name")] metadata. null means global DRN rule; blank names are invalid.
- Post-auth defaults to 100/minute; pre-auth defaults to a coarser 1,000/minute IP bucket for B2B NAT/VPN/CDN egress addresses. Configure settings under
DrnAppFeatures:DrnRateLimit; phase override values of 0 inherit the shared settings. Settings are a startup snapshot exposed through IAppSettings.Features.RateLimit.
- Treat
DrnRateLimitOptions as global defaults. Tenant plan, feature-flag, account, or endpoint-specific quotas belong in app-owned rules; because rule evaluation is synchronous, load plan data into the request scope or a refreshed in-memory snapshot before evaluating the rule.
- Singleton rules are sorted once. Pre-auth uses singleton rules only. Scoped rule existence/order is detected at startup, then scoped rules are resolved from the request provider only for post-auth. Global
Order is preserved across singleton and scoped rules; same-order ShortCircuitOnMatch rules run first, and every matching rule composes. Limiter partition factories must not capture HttpContext or scoped services because limiter instances are cached per partition.
- Post-auth uses DI-configured
RateLimiterOptions, so named policies and rejection callbacks registered through AddRateLimiter(options => ...) remain available to [EnableRateLimiting("policy-name")].
- DRN emits metrics through the
DRN.Framework.Hosting.RateLimiting meter; add this meter to OpenTelemetry exports when pre-auth metrics or DRN rule-level rejection metrics are needed. The action tag distinguishes limit, allow, deny, and unknown.
- Pre-auth and post-auth rejection logs default to deterministic keyed hashes with a
blake3-keyed: prefix. This preserves correlation without exposing raw API keys, tenant hints, service identifiers, user identifiers, or IPs. Use DrnRateLimit.PartitionLogMode = PlainText only for controlled development or dedicated encrypted audit sinks.
- Built-in limiter state is process-local.
HybridCache can cache policy data, but hard multi-replica quotas need edge enforcement or a Redis-backed custom RateLimiter returned through RateLimitRuleResult.CustomPartition(...).
Background Services
Use [HostedService] attribute to auto-register BackgroundService implementations:
[HostedService]
public class MyBackgroundWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
TagHelpers
| TagHelper | Target | Purpose |
|---|
ViteScriptTagHelper | <script> | Resolve Vite manifest + SRI |
ViteLinkTagHelper | <link> | Resolve Vite manifest + SRI |
NonceTagHelper | <script>, <style>, <link>, <iframe> | Add CSP nonce |
CsrfTokenTagHelper | hx-post/put/delete/patch | Add CSRF token |
AuthorizedOnlyTagHelper | *[authorized-only] | Render if MFA complete |
AnonymousOnlyTagHelper | *[anonymous-only] | Render if anonymous |
PageAnchorAspPageTagHelper | <a asp-page> | Mark active page |
PageAnchorHrefTagHelper | <a href> | Mark active page |
ScriptDefaultsTagHelper | <script> | Modern defaults: defer (external), type="module" (inline) |
Vite: <script src="buildwww/app/js/appPostload.js"> -> <script src="/appPostload/appPostload.abc123.js" integrity="sha256-xyz">
Nonce: Auto-added to <script>, <style>, <link>, <iframe>. Opt-out: <script disable-nonce="true">
CSRF: Auto-added to hx-post/put/delete/patch. Opt-out: <button disable-csrf-token="true">
Auth visibility:
<nav authorized-only>Profile links here</nav>
<a asp-page="/User/Login" anonymous-only>Sign In</a>
Active page marking:
<a asp-page="/Dashboard">Dashboard</a>
<!-- If on /Dashboard → class="active fw-bold" aria-current="page" -->
<a asp-page="/Settings" ActiveClass="current">Settings</a>
<a asp-page="/Help" MarkWhenActive="false">Help</a>
Configuration (appsettings.json)
Kestrel
{
"Kestrel": {
"EndpointDefaults": { "Protocols": "Http1" },
"Endpoints": { "All": { "Url": "http://*:5988" } }
}
}
NLog
{
"NLog": {
"targets": {
"console": { "type": "Console", "layout": "${longdate}|${level}|${logger}|${message}${onexception:|${exception:format=tostring}}" }
},
"rules": [
{ "logger": "*", "minLevel": "Info", "writeTo": "console" },
{ "logger": "Microsoft.*", "maxLevel": "Info", "final": true },
{ "logger": "Microsoft.Hosting.Lifetime", "minLevel": "Info", "writeTo": "console", "final": true }
]
}
}
wwwroot Structure
Application (*.Hosted/wwwroot/) — Vite build output:
wwwroot/
├── app/ # app.[hash].css, appPreload.[hash].js
├── appPostload/ # appPostload.[hash].js
├── images/ # Static images
└── lib/ # htmx, bootstrap, react bundles
Vite source (*.Hosted/buildwww/) — unbundled source files:
buildwww/
├── app/
│ ├── css/ # App stylesheets
│ └── js/ # App scripts (appPreload.js, appPostload.js)
├── lib/ # Library sources (htmx, bootstrap, react)
├── plugins/ # Vite plugins
└── types/ # TypeScript type definitions
vite.config.js defines named builds (app, appPostload, htmx, bootstrap, react) selected via BUILD_TYPE env var. Output goes to wwwroot/ with content-hashed filenames and manifest files for TagHelper resolution.
ViteManifest discovers Vite's default .vite/manifest.json files under the active web root, or ContentRootPath/wwwroot when the web root is empty. The package's buildTransitive target uses the exact convention path buildTransitive/$(PackageId).targets and includes wwwroot/**/.vite/manifest.json for Web SDK publish output. When changing Staging/static-web-asset behavior, verify manifest discovery in the running app as well as server startup.
See drn-entityframework for LaunchExternalDependenciesAsync setup with Testcontainers.
Related Skills
Global Usings
global using DRN.Framework.SharedKernel;
global using DRN.Framework.SharedKernel.Domain;
global using DRN.Framework.Utils.DependencyInjection;
global using DRN.Framework.Hosting.DrnProgram;
global using DRN.Framework.Hosting.Endpoints;
global using Microsoft.AspNetCore.Mvc.RazorPages;