Skip to content

Upgrade Guide for v3.0

InertiaCore v3.0 brings the C# / ASP.NET Core adapter up to parity with the canonical Inertia.js v3.0 wire format. This release focuses on a unified prop resolution pipeline, richer SSR observability, and a handful of small APIs that close the gap with the Laravel adapter. You can find the legacy docs for v2.0 at inertiacore.dev/docs/v2.

What’s New

PropsResolver Refactor

A single recursive walker handles shared, partial, merge, defer, once, and scroll resolution in one pass. Nested prop types now work at any depth.

Exception Responses

Render a custom Inertia error page directly from your exception middleware via Inertia.HandleExceptionsUsing(…).

Disable SSR Per-Request

Opt individual requests or URL patterns out of SSR with Inertia.DisableSsr() and Inertia.WithoutSsr().

SSR Failure Events

Subscribe to IGateway.RenderFailed for structured SSR failure reporting with error type, hint, and source location.

Nested Prop Types

Inertia.Merge(), Inertia.Defer(), and Inertia.Once() now work inside closures and nested dictionaries.

Component Transformers

Rewrite component names at render time via Inertia.TransformComponentUsing(…) for backend-driven versioning.

This release also includes several additional improvements:

Upgrade Dependencies

To upgrade to InertiaCore v3.0, update the NuGet package in your .csproj:

Terminal window
dotnet add package AspNetCore.InertiaCore --version 3.0.0

InertiaCore v3.0 targets .NET 6.0, 7.0, 8.0, and 9.0. If your app targets an older TFM, stay on v2.

After upgrading, review your InertiaOptions configuration. A handful of options were renamed or moved in this release — see the configuration changes section below.

Breaking Changes


Response Constructor Signature

The internal Response constructor now takes the resolved prop dictionary, shared props, and metadata as a single PropsResolver output rather than a stream of individual flags. Application code does not call this constructor directly, but if your test suite uses reflection to instantiate Response — a pattern some downstream test harnesses follow to work around the internal visibility — you will need to update the argument list.

The previous 9-parameter signature:

// Before (v2)
new Response(component, props, rootView, version,
encryptHistory, clearHistory, serializer, urlResolver, withAllErrors)

is now:

// After (v3)
new Response(component, props, rootView, version,
encryptHistory, clearHistory, serializer, urlResolver, withAllErrors,
preserveFragment)

The new preserveFragment trailing parameter defaults to false. Existing reflection call sites should append a trailing false to restore their previous behavior.

Scattered Resolve* Methods Removed

The ad-hoc Response.ResolveSharedProps(), ResolvePartialProperties(), ResolveOnceProperties(), ResolveAlways(), ResolveMergeProps(), ResolveDeepMergeProps(), ResolveMatchPropsOn(), ResolveDeferredProps(), ResolveScrollProps(), ResolveOnceProps(), ResolveInertiaPropertyProviders(), and GetMergeablePropsForRequest() methods have been removed from Response. All prop resolution now happens inside the new internal PropsResolver class in a single recursive pass.

Response.ResolveValidationErrors(), ResolveCacheDirections(), and ResolveFlashData() remain on Response because they build page-level metadata rather than resolving individual props.

No action is required unless your code reflected into these methods directly.

Gateway Constructor Gains IHttpContextAccessor

InertiaCore.Ssr.Gateway now takes IHttpContextAccessor via its constructor so it can read per-request SSR disable state from HttpContext.Items. The service is already registered by services.AddInertia() via services.AddHttpContextAccessor(), so production DI wiring requires no changes.

Test code that instantiates Gateway by hand must pass an accessor:

// Before (v2)
var gateway = new Gateway(
httpClientFactory, serializer, options, environment);
// After (v3)
var gateway = new Gateway(
httpClientFactory, serializer, options, environment,
Mock.Of<IHttpContextAccessor>());

Page.SharedProps Field Added

The internal Page model gained a new SharedProps field that serializes as sharedProps in the JSON response body. This is a wire-format addition — the Inertia.js v3 frontend reads it to preserve top-level shared props across instant visits.

The field is nullable and omitted from the JSON payload when empty, so existing v2 frontends are unaffected.

Configuration Changes

InertiaOptions gained several new properties for v3 features:

  • SsrBundlePath — explicit override for SSR bundle discovery
  • SsrThrowOnError — opt-in to typed SsrException on SSR failure
  • WithAllErrors — emit every ModelState error message for a field instead of only the first
  • UseScriptTagForInitialPage — emit the initial page state as a <script type="application/json"> element instead of an HTML-encoded data-page attribute

None are breaking. All default to the v2 behavior. Review the updated InertiaOptions.cs if you want to opt into the new defaults.

Deprecated Session-Based History Flags Removed

The deprecated session-backed paths for ClearHistory and EncryptHistory have been removed. Both now store their state exclusively on HttpContext.Items, which fixes a concurrency bug where the flags would race between concurrent requests on the singleton factory.

This is only breaking for code that read or wrote session["inertia.clear_history"] / session["inertia.encrypt_history"] directly. The public Inertia.ClearHistory() and Inertia.EncryptHistory() facade methods are unchanged.

Other Changes


Nested Prop Types

Prop types like Inertia.Optional(), Inertia.Defer(), Inertia.Merge(), Inertia.DeepMerge(), and Inertia.Once() now work inside closures and nested dictionaries. InertiaCore resolves them at any depth and uses dot-notation paths in partial reload metadata.

return Inertia.Render("Dashboard", new Dictionary<string, object?>
{
["auth"] = () => new Dictionary<string, object?>
{
["user"] = currentUser,
["notifications"] = Inertia.Defer(() => currentUser.UnreadNotifications),
["invoices"] = Inertia.Optional(() => currentUser.Invoices),
},
});

On the client side, the only and except options, as well as the Deferred and WhenVisible components, all support dot-notation for targeting nested props.

router.reload({ only: ['auth.notifications'] })

Classes implementing the ProvidesInertiaProperties interface also work at any nesting level.

Dot-Notation Top-Level Keys

When you pass a Dictionary<string, object?> with dot-notation keys directly to Inertia.Render, InertiaCore unpacks them into nested structures before resolution:

return Inertia.Render("Profile", new Dictionary<string, object?>
{
["user.name"] = "Alice",
["user.email"] = "alice@example.com",
});
// becomes:
// {
// "user": { "name": "Alice", "email": "alice@example.com" }
// }

Anonymous object property names cannot contain dots, so this syntax only applies to dictionary-style props.

Enum Component Names

Inertia.Render now accepts any Enum value as the component name:

public enum Pages { Home, UserList, UserEdit }
return Inertia.Render(Pages.UserEdit, new { id });

The enum member name (UserEdit) is used as the component name. This mirrors the Laravel adapter’s BackedEnum | UnitEnum | string overload while staying idiomatic in C#.

Exception Responses

Register an exception handler closure and render a custom Inertia error page when an unhandled exception reaches the pipeline:

Program.cs
app.UseInertiaExceptionHandler();
// Startup configuration
Inertia.HandleExceptionsUsing(response =>
{
response
.StatusCode(500)
.Render("ErrorPage", new
{
message = response.Exception.Message,
status = response.StatusCode(),
})
.WithSharedData();
});

The middleware catches downstream exceptions on Inertia requests, invokes your callback, and renders the configured component through the normal pipeline (shared props, merge props, etc.). Non-Inertia requests and unconfigured handlers fall through to the default ASP.NET Core exception handler.

Disable SSR Per Request

Opt individual requests out of SSR via the static facade:

// Disable for the current request
Inertia.DisableSsr();
// Disable based on a predicate evaluated at dispatch time
Inertia.DisableSsr(() => User.IsInRole("Admin"));
// Disable for URL patterns (supports * wildcards)
Inertia.WithoutSsr("admin/*", "auth/*");

State is stored per-request in HttpContext.Items, so concurrent requests don’t race. When SSR is disabled for a request, Gateway.ShouldDispatch() returns false and the response falls back to client-side rendering.

SSR Failure Events

Subscribe to structured SSR failure reports for logging and error tracking:

var gateway = app.ApplicationServices.GetRequiredService<IGateway>();
gateway.RenderFailed += (sender, evt) =>
{
logger.LogError(
"Inertia SSR failed for {Component} at {Url}: {Error} ({Type})",
evt.Component, evt.Url, evt.Error, evt.Type);
};

The SsrRenderFailed record carries the failed page data, error message, a classified SsrErrorType (Connection, Render, ComponentResolution, BrowserApi, Unknown), and optional diagnostic fields (Hint, BrowserApi, Stack, SourceLocation) when the SSR node process provides them.

The event fires on every SSR failure regardless of the SsrThrowOnError option — it is purely observational and does not alter the existing silent-fallback-by-default semantics.

Component Transformers

Register a transformer that runs on every Inertia.Render call and rewrites the component name before it is written to the page response:

Inertia.TransformComponentUsing(name => $"v2/{name}");
// Later:
Inertia.Render("Home"); // component is "v2/Home"

Useful for backend-driven component versioning or namespace aliasing without touching every controller action. The transformer runs before EnsurePagesExist validation, so the rewritten name is used for on-disk component lookup.

Preserve URL Fragment

Inertia.PreserveFragment() flags the current request to have the URL fragment (#section-3) preserved across the next redirect:

public IActionResult Update(int id, UpdateRequest request)
{
_service.Update(id, request);
Inertia.PreserveFragment();
return Redirect("/items#saved");
}

The Inertia.js frontend sees the preserveFragment flag on the response and reapplies the hash after navigation.

PullFlashed

Inertia.PullFlashed() reads the current request’s merged flash data (from both HttpContext.Items and TempData) and atomically clears both stores, ensuring subsequent calls return empty:

var toast = Inertia.PullFlashed();

Useful when you want a toast or one-shot notification to appear exactly once without relying on the normal TempData reflash lifecycle.

Initial Page as <script> Tag

Set InertiaOptions.UseScriptTagForInitialPage = true to emit the initial page state as a <script type="application/json"> element instead of an HTML-encoded data-page attribute on the root div:

<!-- Default (v2 behavior) -->
<div id="app" data-page="{&quot;component&quot;:&quot;Home&quot;,...}"></div>
<!-- With UseScriptTagForInitialPage = true -->
<script data-page="app" type="application/json">{"component":"Home",...}</script>
<div id="app"></div>

The script-tag variant produces cleaner escaping for large props and payloads containing quote characters or HTML fragments. Matches the Laravel adapter’s use_script_element_for_initial_page config.

Page.ClearHistory / Page.EncryptHistory Wire Format

The clearHistory and encryptHistory fields in the page object are now omitted from the JSON response when false, matching the Laravel adapter’s v3 behavior. Previously every response included "clearHistory": false and "encryptHistory": false even when history was not being cleared or encrypted. This is a wire-format compression, not a behavioral change — the Inertia.js frontend treats absent fields as false.