Skip to content
fullstackhero

Reference

Web building block

The composition root for the host — AddHeroPlatform / UseHeroPlatform, module loader, global exception handler, validation pipeline, CurrentUser middleware, and observability.

views 0 Last updated

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 in FshPlatformOptions.
  • UseHeroPlatform(WebApplication, Action<FshPipelineOptions>?) — wires the middleware pipeline in the right order; mounts modules’ endpoints and middleware via the ModuleLoader.

Module discovery + loading

  • IModule interface — every module implements it:
    • ConfigureServices(IHostApplicationBuilder) — DI registration phase
    • MapEndpoints(IEndpointRouteBuilder) — endpoint registration phase
    • MapMiddlewares(WebApplication) — middleware insertion phase
  • [FshModule(Order = n)] attribute — registration order; lower runs first.
  • ModuleLoader — discovers all IModule implementations via reflection at startup, runs ConfigureServices in order, then later MapEndpoints + MapMiddlewares.

Cross-cutting

  • GlobalExceptionHandler : IExceptionHandler — converts CustomException, NotFoundException, ForbiddenException, UnauthorizedException, plus uncaught exceptions, into ProblemDetails (RFC 9457). Honors Production env 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 populate ICurrentUser from 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.
  • AddHeroVersioningAsp.Versioning.Http with reader from header X-Version + URL segment.
  • AddHeroIdempotencyIdempotency-Key header support; replay protection via distributed cache.
  • AddHeroFeatureFlagsMicrosoft.FeatureManagement with 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:

  1. UseExceptionHandler() — converts exceptions to ProblemDetails
  2. UseResponseCompression()
  3. UseCors()before HTTPS redirect (preflight requests can’t follow redirects)
  4. UseHttpsRedirection()
  5. Security headers
  6. UseStaticFiles() (optional)
  7. Job dashboard (/hangfire)
  8. UseRouting()
  9. OpenAPI + Scalar
  10. UseAuthentication()
  11. Module-supplied middleware (via ConfigureMiddleware)
  12. UseRateLimiter()
  13. UseAuthorization()
  14. Module endpoint mapping (via MapEndpoints)
  15. Health endpoints, SSE endpoints, SignalR
  16. 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 OPTIONS requests cannot follow an HTTP→HTTPS redirect per the Fetch spec. The kit puts UseCors before UseHttpsRedirection. Don’t reorder.
  • CurrentUserMiddleware runs last. It’s after authorization, so anything between UseAuthentication and CurrentUserMiddleware runs with a populated ClaimsPrincipal but a null ICurrentUser. If you write middleware that needs ICurrentUser, place it after this one — or read claims directly.
  • AddHeroPlatform defaults are conservative. Caching, jobs, mailing, feature flags, idempotency, SSE, realtime, quotas are all off unless explicitly enabled. Only OpenAPI and OpenTelemetry default to on. This keeps the smallest possible host trivially deployable.
  • The ModuleLoader is reflection-based and discovers types at startup once. Adding a module means adding the project, adding the marker assembly to the host’s moduleAssemblies array, and adding it to AddMediator(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.cs
  • src/BuildingBlocks/Web/Modules/ModuleLoader.cs
  • src/BuildingBlocks/Web/Exceptions/GlobalExceptionHandler.cs
  • src/BuildingBlocks/Web/Mediator/Behaviors/ValidationBehavior.cs
  • src/BuildingBlocks/Web/Auth/CurrentUserMiddleware.cs