The Web block is the host-side composition layer that turns a IHostApplicationBuilder into a fullstackhero application. Two extension methods — AddHeroPlatform and UseHeroPlatform — wire every cross-cutting concern: logging, OpenTelemetry, exception handling, CORS, OpenAPI + Scalar, API versioning, authentication, authorization, validation pipeline, idempotency, feature flags, SSE, SignalR realtime, rate limiting, module loading, and current-user resolution. Roughly 2,000+ lines of code across 40+ files.
What it ships
Composition methods
AddHeroPlatform(IHostApplicationBuilder, Action<FshPlatformOptions>?)— registers all platform services. Hooks every optional block (caching, jobs, mailing, feature flags, idempotency, SSE, realtime, quotas) behind a boolean toggle inFshPlatformOptions.UseHeroPlatform(WebApplication, Action<FshPipelineOptions>?)— wires the middleware pipeline in the right order; mounts modules’ endpoints and middleware via theModuleLoader.
Module discovery + loading
IModuleinterface — every module implements it:ConfigureServices(IHostApplicationBuilder)— DI registration phaseMapEndpoints(IEndpointRouteBuilder)— endpoint registration phaseConfigureMiddleware(IApplicationBuilder)— middleware insertion phase (default no-op)
[assembly: FshModule(typeof(XModule), order)]— assembly-level attribute; lower order runs first, ties break alphabetically.ModuleLoader—AddModules(builder, assemblies)reads theFshModuleattributes off the passed assemblies (and registers their FluentValidation validators), instantiates each module, and runsConfigureServicesin order.UseModuleMiddlewares()andMapModules()later replayConfigureMiddleware+MapEndpoints.
Cross-cutting
GlobalExceptionHandler : IExceptionHandler— converts exceptions intoProblemDetails(RFC 9457):CustomExceptionand its subclasses map to theirStatusCodewithErrorMessagesin anerrorsextension; FluentValidation’sValidationExceptionbecomes a 400 with a per-property errors dictionary;UnauthorizedAccessException→ 401,KeyNotFoundException→ 404,BadHttpRequestExceptionkeeps its own status. Everything else is a generic 500 with the real message withheld (in every environment). Every response carriestraceId+correlationIdextensions.ValidationBehavior<TRequest, TResponse>— Mediator pipeline behavior that runs FluentValidation against the request before the handler.CurrentUserMiddleware— registered last in the pipeline (after authorization) to populateICurrentUserfrom the authenticatedClaimsPrincipaland tag the current OTel activity with user/tenant/correlation ids.MediatorTracingBehavior— emits OpenTelemetry activities for every Mediator command/query (registered byAddHeroOpenTelemetry).
Feature toggles (FshPlatformOptions)
builder.AddHeroPlatform(o =>{ o.EnableCaching = true; // adds HybridCache + Valkey (default off) o.EnableJobs = true; // adds Hangfire (default off) o.EnableMailing = true; // adds IMailService (default off) o.EnableQuotas = true; // adds quota services (default off) o.EnableSse = true; // adds SSE plumbing (default off) o.EnableRealtime = true; // adds SignalR + Redis backplane (default off) o.EnableFeatureFlags = true; // adds Microsoft.FeatureManagement (default off) // EnableCors, EnableOpenApi, EnableOpenTelemetry, EnableIdempotency default to true});UseHeroPlatform has its own FshPipelineOptions mirror: UseCors, UseOpenApi, ServeStaticFiles, MapModules (default true) and MapSseEndpoints, MapRealtime, UseQuotas (default false) — enabling a service in AddHeroPlatform is only half the job, you also flip the matching pipeline toggle.
Sub-blocks the Web block wires
AddHeroLogging— Serilog with HTTP context enrichers.AddHeroOpenTelemetry— traces + metrics via OTLP; registersMediatorTracingBehaviorand wires the caching/Hangfire/module sources and meters.AddHeroOpenApi— OpenAPI documents + Scalar UI at/scalar;OpenApiOptions(Title, Description, Versions, Contact, License).AddHeroVersioning—Asp.Versioningwith URL-segment versioning only (api/v{version}/…), default v1, assumed when unspecified.AddHeroIdempotency—IdempotencyEndpointFilter+IdempotencyOptions(HeaderNamedefaultIdempotency-Key,DefaultTtl24h,MaxKeyLength128); replay protection via distributed cache.AddHeroFeatureFlags—Microsoft.FeatureManagementwith theTenantFeatureFilterfor per-tenant overrides and aFeatureGateEndpointFilterfor endpoints.AddHeroSse— Server-Sent Events plumbing (SseConnectionManager, token service, endpoints viaMapHeroSseEndpoints).AddHeroRealtime— SignalR (AppHubat/api/v1/realtime/hub+ presence endpoint) with a Redis backplane whenCachingOptions:Redisis set.UseHeroSecurityHeaders— security-headers middleware driven bySecurityHeadersOptions(CSP, HSTS, X-Frame-Options, X-Content-Type-Options…).AddHeroResilience(onIHttpClientBuilder) — standard Polly resilience pipeline for outbound HTTP, tuned byHttpResilienceOptions.AddHeroRateLimiting/UseHeroRateLimiting— fixed-window policies fromRateLimitingOptions.MapHeroHealthEndpoints— anonymous/health/live(process only) and/health/ready(all checks, 503 on failure), always mapped.
How modules consume Web
A module’s IModule.ConfigureServices adds its DI registrations; MapEndpoints mounts its routes:
[assembly: FshModule(typeof(CatalogModule), 600)]
public sealed class CatalogModule : IModule{ public void ConfigureServices(IHostApplicationBuilder builder) { PermissionConstants.Register(CatalogPermissions.All); builder.Services.AddHeroDbContext<CatalogDbContext>(); // ... more registrations }
public void MapEndpoints(IEndpointRouteBuilder endpoints) { var versionSet = endpoints.NewApiVersionSet() .HasApiVersion(new ApiVersion(1)) .ReportApiVersions() .Build();
var group = endpoints .MapGroup("api/v{version:apiVersion}/catalog") .WithTags("Catalog") .WithApiVersionSet(versionSet) .RequireAuthorization();
group.MapCreateBrandEndpoint(); group.MapSearchBrandsEndpoint(); // ... }}The ModuleLoader runs ConfigureServices for every module in the order declared by each module’s assembly-level [assembly: FshModule(typeof(XModule), order)] attribute, then later runs MapEndpoints for each.
The pipeline order
UseHeroPlatform wires the middleware pipeline in a specific order; this is the canonical sequence:
UseExceptionHandler()— converts exceptions to ProblemDetailsUseResponseCompression()UseHeroCors()— before HTTPS redirect (preflight requests can’t follow redirects)UseHttpsRedirection()UseHeroSecurityHeaders()UseStaticFiles()(optional,ServeStaticFiles)- Hangfire dashboard (
/jobsby default,HangfireOptions:Route) UseRouting()- OpenAPI + Scalar
UseAuthentication()- Module-supplied middleware (via
ConfigureMiddleware— e.g. Auditing’s HTTP capture) UseHeroRateLimiting()UseHeroQuotas()(optional,UseQuotas)UseAuthorization()- Module endpoint mapping (via
MapEndpoints) - Health endpoints (always), SSE endpoints + SignalR (optional)
CurrentUserMiddleware(last so authorization is already resolved)
Configuration
The Web block reads many config sections — CorsOptions, OpenApiOptions, OriginOptions, SecurityHeadersOptions, RateLimitingOptions, plus the optional sub-block sections (CachingOptions, HangfireOptions, MailOptions, FeatureManagement, IdempotencyOptions, QuotaOptions, HttpResilienceOptions). The kit’s appsettings.json template ships a complete starter set.
How to extend
Add custom request logic without owning a module
builder.Services.Configure<MvcOptions>(opts => opts.Filters.Add<MyFilter>()) won’t work for minimal APIs. Instead, add a Mediator pipeline behavior:
public sealed class TimingBehavior<TRequest, TResponse>(ILogger<TimingBehavior<TRequest, TResponse>> log) : IPipelineBehavior<TRequest, TResponse>{ public async ValueTask<TResponse> Handle(TRequest msg, MessageHandlerDelegate<TRequest, TResponse> next, CancellationToken ct) { var sw = Stopwatch.StartNew(); try { return await next(msg, ct).ConfigureAwait(false); } finally { log.LogInformation("{Req} took {Ms}ms", typeof(TRequest).Name, sw.ElapsedMilliseconds); } }}
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(TimingBehavior<,>));Replace the global exception handler
Implement your own IExceptionHandler, register it as services.AddExceptionHandler<MyHandler>(). Keep the RFC 9457 ProblemDetails shape — the kit’s React clients parse it.
Gotchas
- CORS before HTTPS redirect. Preflight
OPTIONSrequests cannot follow an HTTP→HTTPS redirect per the Fetch spec. The kit putsUseCorsbeforeUseHttpsRedirection. Don’t reorder. CurrentUserMiddlewareruns last. It’s after authorization, so anything betweenUseAuthenticationandCurrentUserMiddlewareruns with a populatedClaimsPrincipalbut a nullICurrentUser. If you write middleware that needsICurrentUser, place it after this one — or read claims directly.AddHeroPlatformdefaults are conservative. Caching, jobs, mailing, feature flags, SSE, realtime, quotas are all off unless explicitly enabled.EnableCors,EnableOpenApi,EnableOpenTelemetry, andEnableIdempotencydefault to on (CORS and OpenAPI are additionally gated by configuration — noCorsOptionsorigins means no CORS middleware;OpenApiOptions:Enabledcan switch docs off per environment).- The
ModuleLoaderreads assembly attributes once at startup. Adding a module means adding the project, adding the marker assembly to the host’smoduleAssembliesarray, and adding both marker types toAddMediator(o => o.Assemblies = [...])— inProgram.csandDbMigrator/Program.cs(four places total). Forgetting the Mediator step is the silent failure mode: services build, endpoints map, but handlers aren’t found at request time.
Critical files
src/BuildingBlocks/Web/Extensions.cssrc/BuildingBlocks/Web/Modules/ModuleLoader.cssrc/BuildingBlocks/Web/Exceptions/GlobalExceptionHandler.cssrc/BuildingBlocks/Web/Mediator/Behaviors/ValidationBehavior.cssrc/BuildingBlocks/Web/Auth/CurrentUserMiddleware.cs
Related
- Architecture: dependency injection — module load order + Mediator wiring.
- Modules overview — every module composes against this block.
- Cross-cutting concerns — feature flags, idempotency, OpenTelemetry.