Skip to content

salesHgabriel/InertiaSharp

Repository files navigation

⚡ InertiaSharp

The ASP.NET Core adapter for Inertia.js — build modern SPAs with Vue, React, or Svelte without building a separate API. Inspired by inertia-rails.


What is InertiaSharp?

InertiaSharp lets you build single-page applications using your ASP.NET Core controllers or Minimal api as the backend, while rendering your frontend in Vue 3, React, or Svelte. No REST API, no token auth, no separate frontend repo needed.

Browser → ASP.NET Core Controller → return this.Inertia("Dashboard", props)
or
Browser → ASP.NET Core Minimal api → return Results.Extensions.Inertia("Dashboard", props); 
                                              ↓
                               First visit: full HTML page
                               Subsequent: JSON page object (XHR)
                                              ↓
                                    Vue/React/Svelte renders

How the Inertia Protocol Works

Step What happens
First visit Browser makes a normal GET. Server returns full HTML with <div id="app" data-page="{...}">. Vite/Vue boots from data-page.
Subsequent clicks Inertia intercepts <Link> clicks, sends XHR with X-Inertia: true. Server returns JSON page object instead of HTML.
Asset version mismatch Server returns 409 Conflict with X-Inertia-Location, client does a full reload to pick up new assets.
Partial reloads Client sends X-Inertia-Partial-Data header. Server only includes requested props.
POST redirects Middleware converts 302303 so browsers re-GET instead of re-POST.

Package Structure

InertiaSharp/
├── src/
│   └── InertiaSharp/                   # 📦 NuGet Package
│       ├── InertiaOptions.cs           #   Configuration
│       ├── InertiaPage.cs              #   Page object model
│       ├── InertiaService.cs           #   Shared props (scoped)
│       ├── InertiaResult.cs            #   IActionResult implementation
│       ├── Middleware/
│       │   └── InertiaMiddleware.cs    #   Version check + 302→303
│       ├── Extensions/
│       │   ├── ControllerExtensions.cs #   this.Inertia(...)
│       │   └── ServiceCollectionExtensions.cs
│       └── TagHelpers/
│           └── InertiaTagHelper.cs     #   <inertia /> → <div id="app">
│
└── sample/
    └── InertiaSharp.Sample/            # 🎯 Demo app (ASP.NET Core 10)
        ├── Controllers/
        │   ├── AuthController.cs       #   Login, Register, Logout
        │   ├── DashboardController.cs  #   Protected dashboard
        │   └── ProfileController.cs   #   Profile + password change
        ├── Models/AppUser.cs           #   Extended Identity user
        ├── Data/AppDbContext.cs        #   EF Core + Identity
        ├── Permissions/               #   Roles + policy constants
        ├── Views/Shared/App.cshtml    #   Inertia shell view
        └── ClientApp/                 #   Vue 3 + Vite frontend
            └── src/
                ├── app.ts             #   Inertia + Vue bootstrap
                ├── Pages/
                │   ├── Auth/Login.vue
                │   ├── Auth/Register.vue
                │   ├── Dashboard.vue
                │   └── Profile/Edit.vue
                └── Layouts/
                    ├── AppLayout.vue  #   Sidebar for auth pages
                    └── GuestLayout.vue #  Centered card for auth

Getting Started

Prerequisites

1. Install the NuGet package

dotnet add package InertiaSharp

2. Configure Program.cs

using InertiaSharp.Extensions;

builder.Services.AddInertia(opt =>
{
    opt.RootView = "App";     // → Views/Shared/App.cshtml
    opt.Version  = "1.0.0";  // bump on asset changes
});

// ...
app.UseRouting();
app.UseInertia();             // ← after UseRouting, before UseAuth
app.UseAuthentication();
app.UseAuthorization();

3. Create the shell view Views/Shared/App.cshtml

@addTagHelper *, InertiaSharp
<!DOCTYPE html>
<html>
<head>
    <meta name="csrf-token" content="@Antiforgery.GetAndStoreTokens(Context).RequestToken" />
    <script type="module" src="http://localhost:5173/src/app.ts"></script>
</head>
<body>
    <inertia />   <!-- renders: <div id="app" data-page="..."> -->
</body>
</html>

4. Return Inertia responses from controllers

public class EventsController : Controller
{
    // GET /events
    public IActionResult Index()
        => this.Inertia("Events/Index", new { events = _db.Events.ToList() });

    // POST /events — validation errors sent back as props
    [HttpPost]
    public IActionResult Store([FromForm] CreateEventDto dto)
    {
        if (!ModelState.IsValid)
        {
            var errors = ModelState.ToDictionary(
                k => k.Key,
                v => v.Value?.Errors.First().ErrorMessage ?? "");
            return this.Inertia("Events/Create", new { errors });
        }
        // ...
        return Redirect("/events");
    }
}

5. Shared props (middleware pattern)

// In a base controller or middleware:
public class InertiaBaseController : Controller
{
    protected readonly InertiaService _inertia;

    public InertiaBaseController(InertiaService inertia)
    {
        _inertia = inertia;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        _inertia.Share("auth", new
        {
            user = HttpContext.User.Identity?.IsAuthenticated == true
                ? new { name = HttpContext.User.Identity.Name }
                : null
        });
    }
}

6. Setup to publish in .csproj

  <!--
    This target runs `npm run build` before publishing.
    During development, Vite dev server handles assets (hot reload).
  -->
  <Target Name="PublishFrontend" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
    <Exec WorkingDirectory="ClientApp" Command="npm ci" />
    <Exec WorkingDirectory="ClientApp" Command="npm run build" />
  </Target>

Development — Hot Reload

Run both servers simultaneously with app.UseViteDevelopmentServer():

app.UseViteDevelopmentServer(); // always before app.Run()
app.Run();

Or script .sh:

# From repo root:
./run-dev.sh

Or manually in two terminals:

# Terminal 1 — Vite dev server (HMR for .vue files)
cd sample/InertiaSharp.Sample/ClientApp
npm install
npm run dev

# Terminal 2 — ASP.NET Core (auto-restarts on C# changes)
cd sample/InertiaSharp.Sample
dotnet watch run
  • Vue changes → instant hot module replacement (no page reload)
  • C# changesdotnet watch restarts the backend, Inertia triggers a full page reload

Visit: https://localhost:5001

Demo credentials: admin@demo.com / Password123!


EF Core Migrations

cd sample/InertiaSharp.Sample

# Create initial migration
dotnet ef migrations add InitialCreate --output-dir Data/Migrations

# Apply (also runs automatically on startup via MigrateAsync)
dotnet ef database update

Permissions / Authorization Flow

User → Login → Cookie issued → Role assigned (Admin/Editor/Viewer)
                                     ↓
                         DashboardController.Index()
                         reads roles → passes `permissions` prop
                                     ↓
                         Dashboard.vue conditionally renders
                         sections based on permissions

Policy-based authorization on controllers:

[Authorize(Policy = Policies.CanManageUsers)]
public IActionResult UserManagement() => this.Inertia("Admin/Users");

Production Deployment

Option A — Docker (recommended)

# Build image
docker build -t inertia-sharp-app .

# Run with persistent database volume
docker run -d \
  -p 8080:8080 \
  -v inertia_data:/data \
  --name inertia-app \
  inertia-sharp-app

# Visit http://localhost:8080

Option B — dotnet publish

# Build frontend first
cd sample/InertiaSharp.Sample/ClientApp
npm ci && npm run build

# Publish .NET app
cd ..
dotnet publish -c Release -o ./publish

# Run
cd publish
./InertiaSharp.Sample

Option C — Azure App Service / Railway / Fly.io

The Dockerfile works on any container platform. Set these environment variables:

Variable Value
ConnectionStrings__Default Your connection string
ASPNETCORE_ENVIRONMENT Production
ASPNETCORE_URLS http://+:8080

Asset versioning in production

Program.cs automatically computes the asset version from the Vite manifest:

var viteManifestPath = Path.Combine(env.WebRootPath, ".vite", "manifest.json");
var assetVersion = File.Exists(viteManifestPath)
    ? Convert.ToHexString(MD5.HashData(File.ReadAllText(viteManifestPath)))
    : "1";

builder.Services.AddInertia(opt => { opt.Version = assetVersion; });

When you deploy a new frontend build, the version changes and all connected clients automatically reload to pick up new assets (409 Conflict flow).


Minimal APIs

InertiaSharp supports Minimal APIs first-class via InertiaHttpResult (IResult) alongside the traditional MVC InertiaResult (IActionResult). Both share the exact same rendering engine — props merging, partial reloads, version checking — so you can mix and match freely or use Minimal APIs exclusively.

Three Minimal API patterns

Pattern 1 — Results.Extensions.Inertia() — the standard idiomatic way:

app.MapGet("/dashboard", async (UserManager<AppUser> users, ClaimsPrincipal user) =>
{
    var appUser = await users.GetUserAsync(user)!;
    return Results.Extensions.Inertia("Dashboard", new
    {
        name  = appUser.FullName,
        email = appUser.Email,
    });
})
.RequireAuthorization();

Pattern 2 — app.MapInertia() — zero-boilerplate static pages:

// Component receives no dynamic props — perfect for About, Terms, 404, etc.
app.MapInertia("/about", "Marketing/About");
app.MapInertia("/403",   "Errors/Forbidden").AllowAnonymous();

// With static props
app.MapInertia("/features", "Marketing/Features", new
{
    plans = new[] { "Free", "Pro", "Enterprise" }
});

Pattern 3 — app.MapInertiaGroup() — grouped + authorized, with shared middleware:

// All routes in the group inherit .RequireAuthorization()
// The endpoint filter runs on every request to share auth props
var authenticated = app
    .MapInertiaGroup("/app")
    .RequireAuthorization()
    .AddEndpointFilter(async (ctx, next) =>
    {
        var inertia = ctx.HttpContext.RequestServices.GetRequiredService<InertiaService>();
        inertia.Share("auth", new { user = ctx.HttpContext.User.Identity!.Name });
        return await next(ctx);
    });

authenticated.MapGet("dashboard", () =>
    Results.Extensions.Inertia("Dashboard"));

authenticated.MapGet("profile", () =>
    Results.Extensions.Inertia("Profile/Edit"));

// Fine-grained policy per route
authenticated
    .MapGet("admin/users", async (UserManager<AppUser> users) =>
        Results.Extensions.Inertia("Admin/Users", new { users = users.Users.ToList() }))
    .RequireAuthorization(Policies.CanManageUsers);

Comparison: MVC vs Minimal API

MVC Controllers Minimal APIs
Return type InertiaResult : IActionResult InertiaHttpResult : IResult
Syntax this.Inertia("Page", props) Results.Extensions.Inertia("Page", props)
Static pages Custom action app.MapInertia(route, component)
Grouped routes Base controller + [Route] app.MapInertiaGroup(prefix)
Shared props Inject InertiaService Same — inject InertiaService
Rendering engine InertiaPageRenderer InertiaPageRenderer ✓ (shared)

Using React or Svelte instead of Vue

The server-side package is framework-agnostic. Only the client bootstrap changes.

React:

npm install @inertiajs/react react react-dom
// src/app.tsx
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'

createInertiaApp({
  resolve: name => {
    const pages = import.meta.glob('./Pages/**/*.tsx', { eager: true })
    return pages[`./Pages/${name}.tsx`]
  },
  setup({ el, App, props }) {
    createRoot(el).render(<App {...props} />)
  },
})

Svelte:

npm install @inertiajs/svelte svelte
// src/app.ts
import { createInertiaApp } from '@inertiajs/svelte'
createInertiaApp({
  resolve: name => import(`./Pages/${name}.svelte`),
  setup({ el, App, props }) {
    new App({ target: el, props })
  },
})

CLI — Project Scaffolding (inertiasharp)

InertiaSharp ships an interactive CLI tool (InertiaSharp.Cli) that scaffolds full starter projects in seconds. It is distributed as a .NET global tool under the command name inertiasharp.

Install

dotnet tool install -g InertiaSharp.Cli

Usage

inertiasharp [project-name]
inertiasharp --help
inertiasharp --version

If you omit the project name, the wizard will ask for it interactively.

Interactive wizard

Running inertiasharp (or inertiasharp MyApp) opens a step-by-step prompt:

Step Options
API style MVC Controllers · Minimal API
Database SQLite · PostgreSQL · SQL Server
Frontend Vue 3 + Reka UI (shadcn-vue) · React + shadcn/ui · Svelte + shadcn-svelte
Auth ASP.NET Core Identity (login, register, profile, roles) · Simple home page with a DB query

After confirming, the CLI:

  1. Generates the full ASP.NET Core project (.csproj, Program.cs, controllers / endpoints, EF Core context, shell Razor view, appsettings.json, run-dev.sh, .gitignore).
  2. Generates the frontend (ClientApp/) configured for the chosen framework and Vite.
  3. Runs npm install inside ClientApp/.
  4. Runs dotnet restore in the project root.

What gets generated

MyApp/
├── MyApp.csproj
├── Program.cs
├── appsettings.json
├── run-dev.sh                  # starts both Vite dev server and dotnet watch
├── Controllers/                # (MVC style only)
│   ├── HomeController.cs
│   ├── AuthController.cs       # (with auth)
│   ├── DashboardController.cs  # (with auth)
│   ├── ProfileController.cs    # (with auth)
│   └── AdminController.cs      # (with auth)
├── Models/
├── Data/AppDbContext.cs
├── Permissions/                # Roles + Policies (with auth)
├── Contracts/                  # Request DTOs  (with auth)
├── Views/Shared/App.cshtml     # Inertia shell
├── wwwroot/
├── Migrations/
└── ClientApp/                  # Vite + Vue / React / Svelte
    └── src/
        ├── app.ts (or .tsx)
        ├── Pages/
        └── Layouts/

Next steps after scaffolding

cd MyApp
bash run-dev.sh

If you included auth, run migrations on first start (applied automatically):

dotnet ef migrations add InitialCreate
# migrations are auto-applied on startup

Demo credentials (auth template): admin@demo.com / Password123!


License

MIT

About

The .NET adapter for Inertia.js.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Languages