Indiko.Hosting.Web 2.5.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Indiko.Hosting.Web --version 2.5.0
                    
NuGet\Install-Package Indiko.Hosting.Web -Version 2.5.0
                    
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="Indiko.Hosting.Web" Version="2.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Indiko.Hosting.Web" Version="2.5.0" />
                    
Directory.Packages.props
<PackageReference Include="Indiko.Hosting.Web" />
                    
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 Indiko.Hosting.Web --version 2.5.0
                    
#r "nuget: Indiko.Hosting.Web, 2.5.0"
                    
#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 Indiko.Hosting.Web@2.5.0
                    
#: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=Indiko.Hosting.Web&version=2.5.0
                    
Install as a Cake Addin
#tool nuget:?package=Indiko.Hosting.Web&version=2.5.0
                    
Install as a Cake Tool

Indiko.Hosting.Web

A convention-over-configuration ASP.NET Core Web API hosting bootstrap library. Drop in WebHostBootstrapper and a single WebStartup subclass, and the full production middleware stack — CORS, response caching, health checks, API versioning, HTTPS enforcement, forwarded headers, and more — is wired for you automatically.

NuGet License: MIT .NET 10


Table of Contents


Features

  • Zero-boilerplate startup — subclass WebStartup, point the bootstrapper at it, and the entire middleware pipeline is configured.
  • Options-driven flagsWebStartupOptions is bound from appsettings.json; toggle behaviour per environment without changing code.
  • Virtual property overrides — each flag is also a protected virtual property; subclasses can override to hard-code architecturally fixed values.
  • Health check endpoint/healthz registered automatically; extend with custom checks.
  • Response cachingAddResponseCaching() and UseResponseCaching() wired; declare cache profiles in configuration.
  • CORS — Development: any origin; Production: restricted to AllowOrigins from configuration, with named policies CorsPolicy and AllowHeaders (wildcard subdomain support).
  • API versioning — URL-segment versioning enabled in Development; disabled in non-Development environments.
  • Controllers-only or ControllersWithViews — toggled via AddControllersWithViews; static files and MapDefaultControllerRoute follow automatically.
  • JSON cycle handlingReferenceHandler.IgnoreCycles applied globally.
  • Forwarded headers — opt-in via EnableForwardedHeaderOptions.
  • Force HTTPS — opt-in via ForceHttps; applied on top of the environment-level redirection.
  • HSTS — enabled automatically in Production.
  • IRequestMetadataService — extracts tenant and user from the JWT Authorization header; registered as Transient.
  • Flexible host builder — fluent .ConfigureHostBuilder(Action<IHostBuilder>) for host extensions such as UseWindowsService().
  • Block system integrationBlockManager propagates ConfigureServices, Configure, ConfigureBuilder, and PreRunAsync to all registered Indiko blocks.

Installation

dotnet add package Indiko.Hosting.Web

Quick Start

1. Create your Startup class

using Indiko.Hosting.Web;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace MyWebApi;

public class MyStartup : WebStartup
{
    public MyStartup(IConfiguration configuration, IWebHostEnvironment environment)
        : base(configuration, environment)
    {
    }

    public override void ConfigureServices(IServiceCollection services)
    {
        base.ConfigureServices(services); // always call base first

        // Register your own services
        services.AddScoped<IOrderService, OrderService>();
    }
}

2. Write your entry point

// Program.cs
await new WebHostBootstrapper().RunAsync<MyStartup>(args);

3. Add appsettings.json

{
  "ServiceName": "MyWebApi",
  "AllowOrigins": "https://app.example.com,https://admin.example.com",
  "WebStartupOptions": {
    "AddControllersWithViews": false,
    "EnableForwardedHeaderOptions": true,
    "ForceHttps": false
  },
  "CacheProfiles": {
    "Default": { "Duration": 60 },
    "NoCache": { "Duration": 0, "NoStore": true }
  }
}

4. Add a controller

using Microsoft.AspNetCore.Mvc;

namespace MyWebApi.Controllers;

[ApiController]
[Route("[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orders;

    public OrdersController(IOrderService orders) => _orders = orders;

    [HttpGet]
    public async Task<IActionResult> GetAll() =>
        Ok(await _orders.GetAllAsync());
}

Run with dotnet run. The API is available immediately; the health endpoint is at /healthz.


WebStartupOptions: Options-Driven Configuration

WebStartupOptions extends HostStartupOptions and exposes three flags that control how the middleware pipeline is assembled:

Property Type Default Description
AddControllersWithViews bool false Registers AddControllersWithViews instead of AddControllers. Enables static files and MapDefaultControllerRoute.
EnableForwardedHeaderOptions bool false Calls UseForwardedHeaders() at the start of the pipeline. Required when running behind a reverse proxy.
ForceHttps bool false Calls UseHttpsRedirection() regardless of environment. Production always includes HTTPS redirection anyway.

Configure via appsettings.json

All three properties are automatically bound from the "WebStartupOptions" configuration section. No code change is required:

{
  "WebStartupOptions": {
    "AddControllersWithViews": false,
    "EnableForwardedHeaderOptions": true,
    "ForceHttps": false
  }
}

Use per-environment overrides by maintaining separate files:

// appsettings.Production.json
{
  "WebStartupOptions": {
    "EnableForwardedHeaderOptions": true,
    "ForceHttps": true
  }
}
// appsettings.Development.json
{
  "WebStartupOptions": {
    "ForceHttps": false
  }
}

Configure via property override

Each option is exposed as a protected virtual property. Override it in your subclass to hard-code a value that takes precedence over configuration:

public class MyStartup : WebStartup
{
    public MyStartup(IConfiguration configuration, IWebHostEnvironment environment)
        : base(configuration, environment) { }

    // Always behind a reverse proxy in every deployment
    protected override bool EnableForwardedHeaderOptions => true;

    // This API never renders Razor views
    protected override bool AddControllersWithViews => false;
}

When to use each approach

Scenario Recommended approach
Different values per environment appsettings.{Environment}.json
Feature is architecturally fixed (e.g., always behind a proxy) Property override in subclass
Same binary deployed with different configuration appsettings.json
Quick local prototyping Either

Flexible Host Builder

WebHostBootstrapper is sealed, but IHostBuilder can be extended fluently before RunAsync is called:

await new WebHostBootstrapper()
    .ConfigureHostBuilder(b => b.UseWindowsService())
    .RunAsync<MyStartup>(args);

Multiple calls chain without conflict:

await new WebHostBootstrapper()
    .ConfigureHostBuilder(b => b.UseWindowsService())
    .ConfigureHostBuilder(b =>
    {
        b.ConfigureAppConfiguration((ctx, cfg) =>
            cfg.AddEnvironmentVariables("MYAPP_"));
    })
    .RunAsync<MyStartup>(args);

Actions registered via .ConfigureHostBuilder() are applied after all BlockManager.ConfigureBlock calls and after the internal ConfigureWebHostDefaults, so they execute last and can override anything set by blocks.


CORS Configuration

CORS behaviour differs between Development and all other environments.

Development

Any origin, any method, any header — no configuration needed. The default CORS policy is registered and applied by app.UseCors() (no policy name argument).

Production and Staging

Origins are read from the top-level "AllowOrigins" key as a comma-separated string. Two named policies are registered:

Policy name Behaviour
CorsPolicy Exact origin match, any method, any header, credentials allowed
AllowHeaders Wildcard subdomain match, any method, any header, credentials allowed

app.UseCors("CorsPolicy") is applied in the pipeline. AllowHeaders is available for explicit [EnableCors("AllowHeaders")] decoration on individual controllers or actions.

{
  "AllowOrigins": "https://app.example.com,https://admin.example.com"
}

Warning: If AllowOrigins is absent or empty in a non-Development environment, a warning is logged and no CORS policy is registered. Browsers will block all cross-origin requests. Always set this key in production configuration.

Custom CORS policies

Override ConfigureServices to add supplementary policies alongside the base ones:

public override void ConfigureServices(IServiceCollection services)
{
    base.ConfigureServices(services); // registers base policies first

    services.AddCors(options =>
    {
        options.AddPolicy("MobileClients", builder =>
        {
            builder.WithOrigins("https://mobile.example.com")
                   .AllowAnyMethod()
                   .AllowAnyHeader();
        });
    });
}

Then decorate the relevant controllers:

[EnableCors("MobileClients")]
[ApiController]
[Route("mobile/[controller]")]
public class MobileOrdersController : ControllerBase { ... }

Response Caching and CacheProfiles

AddResponseCaching() and UseResponseCaching() are registered unconditionally. Cache profiles are loaded from the "CacheProfiles" configuration section and made available to all MVC controller options:

{
  "CacheProfiles": {
    "Default": { "Duration": 60 },
    "Long":    { "Duration": 3600 },
    "NoCache": { "Duration": 0, "NoStore": true }
  }
}

Use profiles on individual actions with the [ResponseCache] attribute:

[HttpGet("{id}")]
[ResponseCache(CacheProfileName = "Default")]
public async Task<IActionResult> GetById(int id) { ... }

[HttpGet("live-feed")]
[ResponseCache(CacheProfileName = "NoCache")]
public IActionResult GetLiveFeed() { ... }

All CacheProfile properties supported by ASP.NET Core are available:

Property Type Description
Duration int Cache duration in seconds
Location ResponseCacheLocation Any, Client, or None
NoStore bool Sets Cache-Control: no-store
VaryByHeader string Adds a Vary response header
VaryByQueryKeys string[] Varies the cache by query string keys (requires response caching middleware)

API Versioning

URL-segment API versioning (via Asp.Versioning) is registered only in Development. In all other environments it is disabled and an Information-level log message is emitted.

Development URL pattern:

GET /v1/orders
GET /v2/orders

When versioning is active, unversioned requests assume the default version (ApiVersion.Default), and responses include api-supported-versions and api-deprecated-versions headers.

using Asp.Versioning;

[ApiController]
[ApiVersion("1.0")]
[Route("v{version:apiVersion}/[controller]")]
public class OrdersController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll() => Ok("v1 orders");
}

[ApiController]
[ApiVersion("2.0")]
[Route("v{version:apiVersion}/[controller]")]
public class OrdersV2Controller : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll() => Ok("v2 orders with extra fields");
}

API versioning is disabled in Production by design. If production versioning is required, override ConfigureServices and register AddApiVersioning explicitly after calling base.ConfigureServices.


Health Checks

AddHealthChecks() is registered unconditionally and the endpoint is mapped at /healthz.

Custom health checks

public override void ConfigureServices(IServiceCollection services)
{
    base.ConfigureServices(services);

    services.AddHealthChecks()
            .AddSqlServer(Configuration.GetConnectionString("Default"))
            .AddRedis(Configuration.GetConnectionString("Redis"))
            .AddCheck<MyCustomHealthCheck>("my-service");
}

Custom health check implementation

using Microsoft.Extensions.Diagnostics.HealthChecks;

public class MyCustomHealthCheck : IHealthCheck
{
    private readonly IMyDependency _dependency;

    public MyCustomHealthCheck(IMyDependency dependency)
        => _dependency = dependency;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var isHealthy = await _dependency.PingAsync(cancellationToken);
        return isHealthy
            ? HealthCheckResult.Healthy("Service responding normally.")
            : HealthCheckResult.Unhealthy("Service did not respond.");
    }
}

Invoke the endpoint:

GET /healthz
200 OK                    → Healthy
503 Service Unavailable   → Unhealthy

HTTPS and HSTS

Environment Behaviour
Production UseHttpsRedirection() + UseHsts() always active
Non-production Neither, unless ForceHttps = true
Any + ForceHttps = true UseHttpsRedirection() added unconditionally

Configure HSTS settings in ConfigureServices if you need to adjust max-age or enable preload:

public override void ConfigureServices(IServiceCollection services)
{
    base.ConfigureServices(services);

    services.AddHsts(options =>
    {
        options.MaxAge = TimeSpan.FromDays(365);
        options.IncludeSubDomains = true;
        options.Preload = true;
    });
}

Forwarded Headers

When running behind a reverse proxy (nginx, Traefik, Azure Application Gateway, etc.), UseForwardedHeaders() rewrites HttpContext.Connection.RemoteIpAddress, HttpContext.Request.Scheme, and the Host header from the X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host headers.

Enable via configuration:

{
  "WebStartupOptions": {
    "EnableForwardedHeaderOptions": true
  }
}

Or via property override:

protected override bool EnableForwardedHeaderOptions => true;

To restrict which proxy IPs are trusted, configure ForwardedHeadersOptions in ConfigureServices:

using System.Net;

public override void ConfigureServices(IServiceCollection services)
{
    base.ConfigureServices(services);

    services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.KnownProxies.Add(IPAddress.Parse("10.0.0.1"));
        options.KnownNetworks.Clear(); // limit to explicit list
        options.KnownProxies.Clear();
        options.KnownProxies.Add(IPAddress.Parse("10.0.0.1"));
    });
}

IRequestMetadataService

IRequestMetadataService is registered as Transient and provides the tenant and user identity extracted from the current HTTP request's Authorization header.

Interface

public interface IRequestMetadataService
{
    Task<(string tenant, string user)> GetRequestMetdataAsync();
}

User resolution order

  1. HttpContext.User.Identity.Name — populated when ASP.NET Core authentication middleware is active.
  2. JWT sub claim from the raw Authorization: Bearer <token> header — used when auth middleware is not wired but a JWT is still present.

Usage

[ApiController]
[Route("[controller]")]
public class AuditController : ControllerBase
{
    private readonly IRequestMetadataService _metadata;

    public AuditController(IRequestMetadataService metadata)
        => _metadata = metadata;

    [HttpGet("whoami")]
    public async Task<IActionResult> WhoAmI()
    {
        var (tenant, user) = await _metadata.GetRequestMetdataAsync();
        return Ok(new { tenant, user });
    }
}

Note: The tenant field is returned as an empty string in the current implementation. To populate it, subclass RequestMetadataService, override the tenant extraction logic, and register your implementation in ConfigureServices after the base.ConfigureServices call.

public override void ConfigureServices(IServiceCollection services)
{
    base.ConfigureServices(services);

    // Replace the default implementation
    services.AddTransient<IRequestMetadataService, MyTenantAwareMetadataService>();
}

Environment-Specific Behavior

Feature Development Production / Staging
CORS policy Any origin (default policy) AllowOrigins from config (CorsPolicy + AllowHeaders)
CORS middleware call UseCors() (no policy name) UseCors("CorsPolicy")
API versioning Enabled (URL segment) Disabled
Developer exception page Enabled Disabled
HTTPS redirection Only if ForceHttps = true Always
HSTS Disabled Enabled
Forwarded headers Conditional on EnableForwardedHeaderOptions Conditional on EnableForwardedHeaderOptions
Static files Only if AddControllersWithViews = true Only if AddControllersWithViews = true
Controller routing MapControllers() or MapDefaultControllerRoute() depending on AddControllersWithViews Same

Best Practices

  • Always call base.ConfigureServices(services) and base.Configure(app, env, logger) first in your overrides. The base methods register health checks, caching, controllers, CORS, API versioning, IHttpContextAccessor, IRequestMetadataService, and all block services. Calling base last may silently overwrite your registrations.
  • Set AllowOrigins in every non-Development environment. If it is absent, CORS is not registered, and a warning log is the only indication. Browsers will block all cross-origin requests.
  • Use appsettings.{Environment}.json to toggle WebStartupOptions rather than conditional code in startup. This keeps the binary environment-agnostic.
  • Register custom health checks by chaining on AddHealthChecks() rather than calling services.AddHealthChecks() again — the builder returned from base.ConfigureServices is sufficient.
  • Do not add UseHttpsRedirection() manually when ForceHttps = true; the base Configure call already adds it and a double registration would redirect twice.
  • Behind a load balancer or proxy, always enable EnableForwardedHeaderOptions to preserve the originating IP and scheme for logging, rate limiting, and IP-based authorization.
  • Avoid mixing the two configuration approaches for the same property. Pick one: either drive it from appsettings.json or from a property override, so the source of truth is unambiguous.

Package Purpose
Indiko.Hosting.Mvc MVC + Razor Pages bootstrap with views, Razor Pages, and static files
Indiko.Hosting.Abstractions Base bootstrapper, HostStartupOptions, IRequestMetadataService interface

License

MIT — Copyright © INDIKO and contributors.

Repository: https://github.com/0xc3u/BuildingBlocks

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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.7.8 99 5/7/2026
2.7.7 88 5/7/2026
2.7.6 148 4/23/2026
2.7.5 157 4/23/2026
2.7.4 121 4/23/2026
2.7.3 101 4/23/2026
2.7.2 105 4/23/2026
2.7.1 102 4/23/2026
2.7.0 114 4/23/2026
2.6.4 122 4/21/2026
2.6.3 104 4/21/2026
2.6.2 97 4/21/2026
2.6.1 95 4/18/2026
2.6.0 97 4/17/2026
2.5.1 103 4/14/2026
2.5.0 120 3/30/2026
2.2.18 125 3/8/2026
2.2.17 93 3/8/2026
2.2.16 99 3/8/2026
2.2.15 100 3/7/2026
Loading failed