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
<PackageReference Include="Indiko.Hosting.Mvc" Version="2.8.0" />
<PackageVersion Include="Indiko.Hosting.Mvc" Version="2.8.0" />
<PackageReference Include="Indiko.Hosting.Mvc" />
paket add Indiko.Hosting.Mvc --version 2.8.0
#r "nuget: Indiko.Hosting.Mvc, 2.8.0"
#:package Indiko.Hosting.Mvc@2.8.0
#addin nuget:?package=Indiko.Hosting.Mvc&version=2.8.0
#tool nuget:?package=Indiko.Hosting.Mvc&version=2.8.0
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.
Table of Contents
- Features
- Installation
- Quick Start
- MvcStartupOptions: Options-Driven Configuration
- Flexible Host Builder
- Razor Pages and Views
- Response Caching and CacheProfiles
- Health Checks
- Static Files
- CORS Configuration
- HTTPS and HSTS
- Forwarded Headers
- IRequestMetadataService
- Environment-Specific Behavior
- Best Practices
- Related Packages
Features
- Zero-boilerplate MVC startup — subclass
MvcStartup, runMvcHostBootstrapper, and a complete MVC + Razor Pages application is ready. - Full Razor Pages support —
AddRazorPages(),MapRazorPages(), and Razor runtime compilation registered automatically. - ControllersWithViews always active — MVC is always in ControllersWithViews mode (appropriate for view-rendering applications).
- Options-driven flags —
MvcStartupOptionsis bound fromappsettings.json; toggle behaviour per environment without changing code. - Virtual property overrides —
EnableForwardedHeaderOptionsandForceHttpsareprotected virtual; subclasses can hard-code fixed values. - Response caching —
AddResponseCaching()andUseResponseCaching()wired; declare cache profiles in configuration. - Health check endpoint —
/healthzregistered automatically; extend with custom checks. - Static files —
UseStaticFiles()always active; servewwwrootassets out of the box. - CORS — Development: any origin; Production: any origin (production restriction via
AllowOriginsis planned — see the CORS section for details). - JSON serialization — Newtonsoft.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
Authorizationheader; registered asTransient. - Flexible host builder — fluent
.ConfigureHostBuilder(Action<IHostBuilder>)for host extensions such asUseWindowsService(). - Block system integration —
BlockManagerpropagatesConfigureServices,Configure,ConfigureBuilder, andPreRunAsyncto 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>
}
5. Recommended project structure
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. |
AddControllersWithViewsis alwaystruefor MVC applications and is not configurable. If you want a pure JSON API without views, useIndiko.Hosting.Webinstead.
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
AllowOriginsconfiguration is a planned improvement marked in the code. If your application requires restricted production CORS, overrideConfigureServicesafter callingbase.ConfigureServicesand 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}.jsonto toggleMvcStartupOptionsrather than conditional code in startup. - Do not add
UseHttpsRedirection()manually whenForceHttps = true— the baseConfigurecall already adds it. - Behind a reverse proxy, always enable
EnableForwardedHeaderOptionsso 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
wwwrootcontent minimal. Bundle and minify CSS/JS using the .NET bundling tools or a frontend build step, then copy output towwwroot.
Related Packages
| 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 | Versions 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. |
-
net10.0
- Indiko.Hosting.Abstractions (>= 2.8.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.8)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 10.0.8)
- Microsoft.AspNetCore.Components (>= 10.0.8)
- Microsoft.AspNetCore.Components.Analyzers (>= 10.0.8)
- Microsoft.AspNetCore.Components.Web (>= 10.0.8)
- Microsoft.AspNetCore.Connections.Abstractions (>= 10.0.8)
- Microsoft.AspNetCore.Http.Connections.Common (>= 10.0.8)
- Microsoft.AspNetCore.Metadata (>= 10.0.8)
- Microsoft.AspNetCore.Mvc.NewtonsoftJson (>= 10.0.8)
- Microsoft.AspNetCore.Mvc.Razor.Extensions (>= 6.0.36)
- Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation (>= 10.0.8)
- Microsoft.AspNetCore.Razor.Language (>= 6.0.36)
- Microsoft.AspNetCore.WebUtilities (>= 10.0.8)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.8)
- Microsoft.Extensions.DependencyModel (>= 10.0.8)
- Microsoft.Extensions.Localization (>= 10.0.8)
- Microsoft.Extensions.Localization.Abstractions (>= 10.0.8)
- Microsoft.IdentityModel.Protocols.OpenIdConnect (>= 8.18.0)
- System.IdentityModel.Tokens.Jwt (>= 8.18.0)
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 |