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 phaseMapMiddlewares(WebApplication)— middleware insertion phase
[FshModule(Order = n)]attribute — registration order; lower runs first.ModuleLoader— discovers allIModuleimplementations via reflection at startup, runs ConfigureServices in order, then later MapEndpoints + MapMiddlewares.
Cross-cutting
GlobalExceptionHandler : IExceptionHandler— convertsCustomException,NotFoundException,ForbiddenException,UnauthorizedException, plus uncaught exceptions, intoProblemDetails(RFC 9457). HonorsProductionenv hiding by default.ValidationBehavior<TRequest, TResponse>— Mediator pipeline behavior that runs FluentValidation against the request before the handler.CurrentUserMiddleware— runs late in the pipeline (after authorization) to populateICurrentUserfrom the JWT claims and request headers.MediatorTracingBehavior— emits OpenTelemetry activities for every Mediator command/query.
Feature toggles (FshPlatformOptions)
builder.AddHeroPlatform(o =>{ o.EnableCaching = true; // adds HybridCache + Valkey o.EnableJobs = true; // adds Hangfire o.EnableMailing = true; // adds IMailService o.EnableQuotas = true; // adds quota middleware o.EnableIdempotency = true; // adds the Idempotency-Key filter o.EnableSse = true; // mounts SSE endpoints o.EnableRealtime = true; // mounts SignalR hub o.EnableFeatureFlags = true; // EnableOpenApi, EnableOpenTelemetry default to true});Sub-blocks the Web block wires
AddHeroLogging— Serilog with HTTP context enrichers.AddHeroOpenTelemetry— full stack: traces, metrics, logs via OTLP exporter; Mediator + EF Core instrumentation.AddHeroOpenApi— OpenAPI 3.1 + Scalar UI at/scalar/v1.AddHeroVersioning—Asp.Versioning.Httpwith reader from headerX-Version+ URL segment.AddHeroIdempotency—Idempotency-Keyheader support; replay protection via distributed cache.AddHeroFeatureFlags—Microsoft.FeatureManagementwith tenant overrides.AddHeroSse— Server-Sent Events endpoint plumbing.AddHeroRealtime— SignalR hub registration over a Valkey backplane.AddHeroSecurityHeaders— CSP, HSTS, X-Frame-Options, X-Content-Type-Options.
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) { builder.Services.AddHeroDbContext<CatalogDbContext>(); PermissionConstants.Register(CatalogPermissions.All); // ... more registrations }
public void MapEndpoints(IEndpointRouteBuilder endpoints) { var v1 = endpoints.MapGroup("api/v{version:apiVersion}/catalog") .WithTags("Catalog") .HasApiVersion(1); v1.MapCreateBrandEndpoint(); v1.MapGetBrandsEndpoint(); // ... }}The ModuleLoader runs ConfigureServices for every module in [FshModule(Order)] order, 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()UseCors()— before HTTPS redirect (preflight requests can’t follow redirects)UseHttpsRedirection()- Security headers
UseStaticFiles()(optional)- Job dashboard (
/hangfire) UseRouting()- OpenAPI + Scalar
UseAuthentication()- Module-supplied middleware (via
ConfigureMiddleware) UseRateLimiter()UseAuthorization()- Module endpoint mapping (via
MapEndpoints) - Health endpoints, SSE endpoints, SignalR
CurrentUserMiddleware(last so authorization is already resolved)
Configuration
The Web block reads many config sections — Logging, Cors, OpenApi, OriginOptions, SecurityHeadersOptions, plus the optional sub-block sections (CachingOptions, HangfireOptions, MailOptions, FeatureManagement, IdempotencyOptions, etc.). 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, idempotency, SSE, realtime, quotas are all off unless explicitly enabled. OnlyOpenAPIandOpenTelemetrydefault to on. This keeps the smallest possible host trivially deployable.- The
ModuleLoaderis reflection-based and discovers types at startup once. Adding a module means adding the project, adding the marker assembly to the host’smoduleAssembliesarray, and adding it toAddMediator(o => o.Assemblies = [...]). 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.