Multiple apps and libraries

Mount a second Inertia UI under its own path, with its own shared props, asset version, state cookie, root view, error pages and SSR.

On this page

One ASP.NET Core app can serve more than one Inertia UI. A typical case is a library, such as an admin dashboard, that ships its own prebuilt client and is mounted under /dashboard in a host app that already uses Inertia.Net.

Each UI is a mount: a named InertiaOptions that app.UseInertia(name) selects inside an app.Map(...) branch. The default mount is the one AddInertia(...) and UseInertia() configure, so existing apps do not change.

What a mount keeps to itself

Per mountShared by all mounts
Shared props (o.Share) and every other InertiaOptions settingIInertiaCache and the cache store (choose distinct InertiaProp.Cache(key) keys)
Asset version: the 409 check only compares against the mount's own versionIInertiaLive and the SignalR hub
State cookie: .Inertia.State.{name} and .Inertia.Previous.{name} by defaultMetrics (named mounts add an inertia.mount tag) and DevTools
Root view: the mount's RootView file, or a keyed IInertiaRootViewAntiforgery and data protection
Vite manifest, SSR servers, managed Node processes, TypeScript outputThe SSR HttpClient (its ConnectTimeout comes from the default mount)
Error pages registered with AddInertiaErrorPages(name, ...)

Register and map a mount

Text
builder.Services.AddInertia(o => o.Share("user", ctx => ctx.User.Identity?.Name)); // the host app

builder.Services.AddInertia("dashboard", o =>
{
    o.Version = "dashboard-2.1.0";          // the dashboard's own asset version
    o.RootView = "dashboard.html";           // or register a keyed IInertiaRootView, below
    o.Share("product", "Dashboard");
});

var app = builder.Build();

app.Map("/dashboard", dashboard =>
{
    dashboard.UseInertia("dashboard");       // before routing in the branch
    dashboard.UseRouting();
    dashboard.UseEndpoints(e => e.MapGet("/", () => Render("Dashboard/Index")));
});

app.UseInertia();                            // the host app's mount
app.MapGet("/", () => Render("Home"));

Map sets the request's path base, so page URLs, Back() fallbacks, Vite asset URLs and the previous-URL cookie stay inside /dashboard. Calling AddInertia("dashboard", ...) again adds to that mount's configuration.

Startup fails when UseInertia(name) names a mount that was never registered, or when two mounts share a state or previous-URL cookie name.

A root view shipped by a library

A library cannot rely on a file in the host's content root. Register a keyed IInertiaRootView that writes an embedded template instead:

Text
builder.Services.AddKeyedSingleton<IInertiaRootView, DashboardRootView>("dashboard");

sealed class DashboardRootView : IInertiaRootView
{
    public async ValueTask RenderAsync(InertiaRootViewContext context)
    {
        var html = context.HttpContext.Response;
        await html.WriteAsync("<!doctype html><html><head><script type=\"module\" src=\"/dashboard/app.js\"></script></head><body>");
        await html.WriteAsync(context.GetBodyHtml()); // the page script and root element
        await html.WriteAsync("</body></html>");
    }
}

Error pages per mount

Text
builder.Services.AddInertiaErrorPages();                                         // the host's Error component
builder.Services.AddInertiaErrorPages("dashboard", o => o.Component = "Dashboard/Error");

app.UseInertiaErrorPages();                                                      // once, before the Map branches

A mount renders only the error components registered for it, because another mount's components are not in its bundle. A mount without AddInertiaErrorPages(name, ...) leaves its errors to the rest of the pipeline.

SSR, health checks and TypeScript

Each mount has its own SSR gateway, and its own managed Node processes when o.Ssr.UseNodeProcess() is set; give each mount its own BasePort. Check a mount's SSR servers with:

Text
builder.Services.AddHealthChecks().AddInertiaSsrForMount("dashboard"); // named "inertia-ssr-dashboard"

o.TypeScript.OutputPath is per mount too, so each client gets its own generated declarations.

For library authors

Expose one registration call and one mapping call, and keep the mount name private to the library:

Text
public static IServiceCollection AddDashboard(this IServiceCollection services) =>
    services.AddInertia("dashboard", o => o.Version = DashboardAssets.Version)
            .AddKeyedSingleton<IInertiaRootView, DashboardRootView>("dashboard");

public static IApplicationBuilder MapDashboard(this IApplicationBuilder app, string path) =>
    app.Map(path, branch =>
    {
        branch.UseInertia("dashboard");
        branch.UseRouting();
        branch.UseEndpoints(e => { /* the dashboard's endpoints */ });
    });

Mounts are adapter configuration: the Inertia protocol on the wire does not change.