Indiko.Hosting.Mvc 2.8.0

dotnet add package Indiko.Hosting.Mvc --version 2.8.0
                    
NuGet\Install-Package Indiko.Hosting.Mvc -Version 2.8.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.Mvc" Version="2.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Indiko.Hosting.Mvc" Version="2.8.0" />
                    
Directory.Packages.props
<PackageReference Include="Indiko.Hosting.Mvc" />
                    
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.Mvc --version 2.8.0
                    
#r "nuget: Indiko.Hosting.Mvc, 2.8.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.Mvc@2.8.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.Mvc&version=2.8.0
                    
Install as a Cake Addin
#tool nuget:?package=Indiko.Hosting.Mvc&version=2.8.0
                    
Install as a Cake Tool

Indiko.Hosting.Mvc

A convention-over-configuration ASP.NET Core MVC hosting bootstrap library with full Razor Pages support. Subclass MvcStartup, run with MvcHostBootstrapper, and the complete middleware stack — MVC, Razor Pages, response caching, health checks, static files, HTTPS enforcement, and forwarded headers — is assembled for you.

NuGet License: MIT .NET 10


Table of Contents


Features

  • Zero-boilerplate MVC startup — subclass MvcStartup, run MvcHostBootstrapper, and a complete MVC + Razor Pages application is ready.
  • Full Razor Pages supportAddRazorPages(), MapRazorPages(), and Razor runtime compilation registered automatically.
  • ControllersWithViews always active — MVC is always in ControllersWithViews mode (appropriate for view-rendering applications).
  • Options-driven flagsMvcStartupOptions is bound from appsettings.json; toggle behaviour per environment without changing code.
  • Virtual property overridesEnableForwardedHeaderOptions and ForceHttps are protected virtual; subclasses can hard-code fixed values.
  • Response cachingAddResponseCaching() and UseResponseCaching() wired; declare cache profiles in configuration.
  • Health check endpoint/healthz registered automatically; extend with custom checks.
  • Static filesUseStaticFiles() always active; serve wwwroot assets out of the box.
  • CORS — Development: any origin; Production: any origin (production restriction via AllowOrigins is planned — see the CORS section for details).
  • JSON serialization — Newton­soft.Json is available via Microsoft.AspNetCore.Mvc.NewtonsoftJson.
  • Forwarded headers — opt-in via EnableForwardedHeaderOptions.
  • Force HTTPS — opt-in via ForceHttps; applied on top of 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.Mvc

Quick Start

1. Create your Startup class

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

namespace MyMvcApp;

public class MyStartup : MvcStartup
{
    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<IProductRepository, ProductRepository>();
    }
}

2. Write your entry point

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

3. Add appsettings.json

{
  "ServiceName": "MyMvcApp",
  "MvcStartupOptions": {
    "EnableForwardedHeaderOptions": false,
    "ForceHttps": false
  },
  "CacheProfiles": {
    "Default": { "Duration": 60 },
    "NoCache": { "Duration": 0, "NoStore": true }
  }
}

4. Add a controller with a view

// Controllers/HomeController.cs
using Microsoft.AspNetCore.Mvc;

namespace MyMvcApp.Controllers;

public class HomeController : Controller
{
    private readonly IProductRepository _products;

    public HomeController(IProductRepository products) => _products = products;

    [ResponseCache(CacheProfileName = "Default")]
    public async Task<IActionResult> Index()
    {
        var products = await _products.GetFeaturedAsync();
        return View(products);
    }
}

@model IEnumerable<Product>
@{
    ViewData["Title"] = "Products";
}
<h1>Featured Products</h1>
@foreach (var product in Model)
{
    <div class="product-card">
        <h2>@product.Name</h2>
        <p>@product.Description</p>
    </div>
}
MyMvcApp/
├── Controllers/
│   └── HomeController.cs
├── Views/
│   ├── Home/
│   │   └── Index.cshtml
│   └── Shared/
│       └── _Layout.cshtml
├── Pages/
│   ├── _ViewStart.cshtml
│   └── Index.cshtml
├── wwwroot/
│   ├── css/
│   │   └── site.css
│   ├── js/
│   │   └── site.js
│   └── lib/
├── MyStartup.cs
├── Program.cs
├── appsettings.json
└── appsettings.Production.json

MvcStartupOptions: Options-Driven Configuration

MvcStartupOptions extends HostStartupOptions and inherits two flags:

Property Type Default Description
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.

AddControllersWithViews is always true for MVC applications and is not configurable. If you want a pure JSON API without views, use Indiko.Hosting.Web instead.

Configure via appsettings.json

All properties are automatically bound from the "MvcStartupOptions" section. If the section is absent, both flags default to false:

{
  "MvcStartupOptions": {
    "EnableForwardedHeaderOptions": false,
    "ForceHttps": false
  }
}

Use per-environment files for targeted overrides:

// appsettings.Production.json
{
  "MvcStartupOptions": {
    "EnableForwardedHeaderOptions": true,
    "ForceHttps": true
  }
}

Environment variables also work using the double-underscore separator:

MvcStartupOptions__EnableForwardedHeaderOptions=true
MvcStartupOptions__ForceHttps=true

Configure via property override

Each flag is a protected virtual property. Override in your subclass to hard-code a fixed value:

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

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

When a property is overridden, the corresponding appsettings.json value for that property is ignored.


Flexible Host Builder

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

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

Multiple calls chain cleanly:

await new MvcHostBootstrapper()
    .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 ConfigureWebHostDefaults, so they execute last and can override anything set by blocks.


Razor Pages and Views

Razor Pages and MVC views are configured unconditionally. No flags or switches are required.

Razor Pages

AddRazorPages() and MapRazorPages() are registered automatically. Create .cshtml files under Pages/ using the standard Razor Pages conventions:

// Pages/Products/Index.cshtml.cs
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MyMvcApp.Pages.Products;

public class IndexModel : PageModel
{
    private readonly IProductRepository _products;

    public IndexModel(IProductRepository products)
        => _products = products;

    public IReadOnlyList<Product> Products { get; private set; } = [];

    public async Task OnGetAsync()
    {
        Products = await _products.GetAllAsync();
    }
}

@page
@model MyMvcApp.Pages.Products.IndexModel
@{
    ViewData["Title"] = "All Products";
}
<h1>Products</h1>
@foreach (var p in Model.Products)
{
    <p>@p.Name</p>
}

MVC Views

Create controllers under Controllers/ and views under Views/{ControllerName}/. The default route pattern is {controller=Home}/{action=Index}/{id?}, mapped by MapDefaultControllerRoute().

Runtime compilation

Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation is included as a dependency, enabling live Razor file changes without a full rebuild in Development.


Response Caching and CacheProfiles

AddResponseCaching() and UseResponseCaching() are registered unconditionally. Cache profiles are loaded from the "CacheProfiles" configuration section:

{
  "CacheProfiles": {
    "Default":      { "Duration": 60 },
    "Long":         { "Duration": 3600, "Location": "Any" },
    "NoCache":      { "Duration": 0, "NoStore": true },
    "ByAccept":     { "Duration": 120, "VaryByHeader": "Accept" }
  }
}

Apply profiles with [ResponseCache]:

[ResponseCache(CacheProfileName = "Default")]
public IActionResult Index() => View();

[ResponseCache(CacheProfileName = "NoCache")]
public IActionResult Dashboard() => View();

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"))
            .AddCheck<MyDatabaseHealthCheck>("database");
}

Custom health check implementation

using Microsoft.Extensions.Diagnostics.HealthChecks;

public class MyDatabaseHealthCheck : IHealthCheck
{
    private readonly IDbConnectionFactory _factory;

    public MyDatabaseHealthCheck(IDbConnectionFactory factory)
        => _factory = factory;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            await using var conn = _factory.Create();
            await conn.OpenAsync(cancellationToken);
            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(ex.Message);
        }
    }
}

Invoke at:

GET /healthz

Static Files

UseStaticFiles() is always active. Place assets in the wwwroot/ directory:

wwwroot/
├── css/
│   └── site.css       → /css/site.css
├── js/
│   └── site.js        → /js/site.js
└── images/
    └── logo.png       → /images/logo.png

Reference assets in Razor views using tag helpers:

<link rel="stylesheet" href="https://nuget.org/~/css/site.css" />
<script src="https://nuget.org/~/js/site.js"></script>
<img src="https://nuget.org/~/images/logo.png" alt="Logo" />

CORS Configuration

Note: In the current implementation, CORS is configured to allow any origin in both Development and Production. Restricting Production origins via AllowOrigins configuration is a planned improvement marked in the code. If your application requires restricted production CORS, override ConfigureServices after calling base.ConfigureServices and add a policy explicitly.

Current behaviour (both environments)

// Effective registration — both Development and Production
services.AddCors(setup =>
{
    setup.AddDefaultPolicy(policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyMethod()
              .AllowAnyHeader();
    });
});

app.UseCors() is called in non-Production environments only. In Production, UseCors is currently not called in the pipeline — CORS headers are not added unless you override Configure.

Overriding CORS for production

To restrict origins in Production until the built-in support is added, override ConfigureServices and Configure:

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

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

        if (!Environment.IsDevelopment())
        {
            var origins = Configuration.GetValue<string>("AllowOrigins")
                ?.Split(',', StringSplitOptions.RemoveEmptyEntries)
                ?? [];

            services.AddCors(options =>
            {
                options.AddPolicy("ProductionCors", builder =>
                {
                    builder.WithOrigins(origins)
                           .AllowAnyMethod()
                           .AllowAnyHeader()
                           .AllowCredentials();
                });
            });
        }
    }

    public override void Configure(
        IApplicationBuilder app,
        IWebHostEnvironment environment,
        ILoggerFactory logger)
    {
        base.Configure(app, environment, logger);

        if (!environment.IsDevelopment())
        {
            app.UseCors("ProductionCors");
        }
    }
}

HTTPS and HSTS

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

Configure HSTS settings in ConfigureServices:

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

Enable when running behind a reverse proxy (nginx, Traefik, Azure Application Gateway, etc.):

{
  "MvcStartupOptions": {
    "EnableForwardedHeaderOptions": true
  }
}

Or via property override:

protected override bool EnableForwardedHeaderOptions => true;

UseForwardedHeaders() is called unconditionally when enabled (regardless of environment) and rewrites RemoteIpAddress, Request.Scheme, and Host from the standard forwarded headers.


IRequestMetadataService

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

Interface

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

Usage in a controller

public class HomeController : Controller
{
    private readonly IRequestMetadataService _metadata;

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

    public async Task<IActionResult> Index()
    {
        var (tenant, user) = await _metadata.GetRequestMetdataAsync();
        ViewData["User"] = user;
        return View();
    }
}

Usage in a Razor Page

public class DashboardModel : PageModel
{
    private readonly IRequestMetadataService _metadata;

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

    public string CurrentUser { get; private set; } = string.Empty;

    public async Task OnGetAsync()
    {
        var (_, user) = await _metadata.GetRequestMetdataAsync();
        CurrentUser = user;
    }
}

Environment-Specific Behavior

Feature Development Production
CORS Any origin (default policy), UseCors() called Any origin registered (production restriction planned); UseCors() not called by default
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 Always Always
Razor Pages routing Always (MapRazorPages) Always
Controller routing Always (MapDefaultControllerRoute) Always

Best Practices

  • Always call base.ConfigureServices(services) first in your overrides. The base method registers MVC, MvcCore, Razor Pages, health checks, response caching, ControllersWithViews, CacheProfiles, CORS, IHttpContextAccessor, IRequestMetadataService, and all block services.
  • Use appsettings.{Environment}.json to toggle MvcStartupOptions rather than conditional code in startup.
  • Do not add UseHttpsRedirection() manually when ForceHttps = true — the base Configure call already adds it.
  • Behind a reverse proxy, always enable EnableForwardedHeaderOptions so logs, rate limiting, and IP-based authorization see the originating IP, not the proxy's.
  • Override CORS for Production if your application has browser clients with specific origins — see the CORS Configuration section.
  • Keep wwwroot content minimal. Bundle and minify CSS/JS using the .NET bundling tools or a frontend build step, then copy output to wwwroot.

Package Purpose
Indiko.Hosting.Web ASP.NET Core Web API bootstrap (JSON APIs, API versioning, no views)
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.8.0 96 5/22/2026
2.7.8 98 5/7/2026
2.7.7 96 5/7/2026
2.7.6 113 4/23/2026
2.7.5 116 4/23/2026
2.7.4 101 4/23/2026
2.7.3 121 4/23/2026
2.7.2 105 4/23/2026
2.7.1 103 4/23/2026
2.7.0 119 4/23/2026
2.6.4 108 4/21/2026
2.6.3 102 4/21/2026
2.6.2 100 4/21/2026
2.6.1 98 4/18/2026
2.6.0 105 4/17/2026
2.5.1 113 4/14/2026
2.5.0 114 3/30/2026
2.2.18 120 3/8/2026
2.2.17 107 3/8/2026
2.2.16 106 3/8/2026
Loading failed