RazorWire MVC Example
Source of truth
This sample is the concrete proof behind the RazorWire package README. It shows how returned Razor fragments, islands, and SSE fit into a normal ASP.NET Core MVC app without a separate client rendering stack.
Start Here: Return Razor Fragments
Run the application from the repository root:
Bash
dotnet run --project examples/razorwire-mvc/RazorWireWebExample.csprojThis is the repo-local path while the public
v0.1package install flow is being finalized. It assumes you are in a clone of this repository with the .NET 10 SDK installed.If you
cd examples/razorwire-mvcfirst,dotnet runalso works from there.Open the URL printed in the console and navigate to
/Reactivity.Wait for the
Permanent Islandsidebar to load.Click the
+button in the counter widget.Watch
Instance ScoreandSession Scoreupdate in place without a full page reload.
That is the core RazorWire workflow in one interaction: a normal MVC form posts, the controller returns targeted Razor fragments, and the UI updates only where it needs to.
To inspect failed-submission conventions, navigate to /Reactivity/FormFailures. That page intentionally triggers validation, anti-forgery, authorization, malformed request, and server failures so you can compare server-handled errors with the default runtime fallback.
What Just Happened
Plain text/Reactivity
-> loads the Permanent Island from /Reactivity/Sidebar
-> renders the Counter view component inside that island
-> posts the counter form to ReactivityController.IncrementCounter
-> returns a RazorWire stream with targeted updates
-> updates the two counters and replaces the hidden input for the next clickFiles Behind the Hero Flow
examples/razorwire-mvc/Views/Reactivity/Index.cshtmlloads the permanent island withsrc="/Reactivity/Sidebar".examples/razorwire-mvc/Views/Shared/_Sidebar.cshtmlhosts the island content and invokes theCounterview component.examples/razorwire-mvc/Views/Shared/Components/Counter/Default.cshtmlrenders the counter values plus theIncrementCounterform.examples/razorwire-mvc/Controllers/ReactivityController.csreturns the targeted stream updates.examples/razorwire-mvc/Views/Reactivity/_CounterInput.cshtmlreplaces the hiddenclientCountinput after each click.
Proof Slice
examples/razorwire-mvc/Views/Shared/Components/Counter/Default.cshtml
Razor<div id="instance-score-value" class="text-2xl font-black text-indigo-600 tabular-nums">@Model</div>
<div id="session-score-value" class="text-2xl font-black text-indigo-400 tabular-nums">0</div>
<form asp-controller="Reactivity" asp-action="IncrementCounter" method="post" rw-active="true" data-counter-form>
<input type="hidden" name="clientCount" id="client-count-input" value="0" />
<button type="submit" aria-label="Increment counter">+</button>
</form>
examples/razorwire-mvc/Controllers/ReactivityController.cs
C#[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult IncrementCounter([FromForm] int clientCount)
{
CounterViewComponent.Increment();
clientCount++;
if (Request.IsTurboRequest())
{
return this.RazorWireStream()
.Update("instance-score-value", CounterViewComponent.Count.ToString())
.Update("session-score-value", clientCount.ToString())
.ReplacePartial("client-count-input", "_CounterInput", clientCount)
.BuildResult();
}
var referer = Request.Headers["Referer"].ToString();
return Url.IsLocalUrl(referer) ? Redirect(referer) : RedirectToAction(nameof(Index));
}
examples/razorwire-mvc/Views/Reactivity/_CounterInput.cshtml
Razor<input type='hidden' name='clientCount' id='client-count-input' value='@Model' />If Your Result Differs
- If the page loads on a different port, use the URL printed by
dotnet run. - If clicking
+gives you a bare400 Bad Request, check the package docs for Security & Anti-Forgery. That is the first thing to verify when you copy this pattern into another page or app. - If the form does not update in place, check the same anti-forgery guidance first, then confirm you are still posting with
rw-active="true"and returning a RazorWire stream fromIncrementCounter. - If you want the broader sample context instead of the focused proof, continue below.
Broader Sample Features
Islands
The sample uses rw:island to load and persist independent UI regions.
ReactivityController.Sidebar()returns the permanent sidebar island.ReactivityController.UserList()returns theUserListview component inside its own island.Views/Home/Index.cshtml,Views/Reactivity/Index.cshtml, andViews/Navigation/Index.cshtmlall reuse the samepermanent-islandso it can persist across page transitions.
Live Updates over SSE
The sample also demonstrates live multi-client updates.
Views/Reactivity/Index.cshtmlincludes<rw:stream-source id="rw-stream-reactivity" channel="reactivity" permanent="true" />.ReactivityController.PublishMessage()pushes new messages to every connected client.ReactivityController.BroadcastUserPresenceAsync()updates the user list and online count across sessions.
Registration and Message Publishing
The reactivity page includes two additional form flows:
Views/Reactivity/_UserRegistration.cshtmlposts toRegisterUserand swaps the register and message forms.Views/Reactivity/_MessageForm.cshtmlposts toPublishMessageand prepends messages into the live feed.
RegisterUser stores the display name in a razorwire-username cookie with Secure, HttpOnly, and SameSite=Lax set. Keep that shape when copying the sample into an application: the cookie is only a convenience for the demo identity, but browsers can otherwise send it over cleartext HTTP. Outside localhost-style development, serve the flow over HTTPS before depending on the cookie. During local development, use the printed localhost URL rather than swapping in 127.0.0.1; browsers treat those as different cookie hosts, and some will reject Secure cookies from an HTTP loopback IP.
Those flows are richer than the counter demo, but the counter is the cleanest first proof because it does not depend on stream-hub context to feel convincing.
Failed Form UX
The sample includes a dedicated /Reactivity/FormFailures page that demonstrates:
FormValidationErrorsreturning a422validation stream withX-RazorWire-Form-Handled: true.- Development anti-forgery diagnostics for a raw form that intentionally omits
__RequestVerificationToken. - Default runtime fallbacks for unhandled
400,403, and500responses. - Consumer customization with CSS variables and
razorwire:form:failurein manual mode.
See the package guide for the API contract and troubleshooting notes: Failed Form UX.
Project Structure
Controllers/ReactivityController.cs: main demo controller for islands, form posts, and stream responses.Views/Reactivity/: reactivity page plus registration, message, and counter partials.Views/Shared/: shared island and view component rendering.ViewComponents/: view component entry points such asCounterandUserList.Services/: in-memory sample services such asUserPresenceServiceandMessageStore.
Development Notes
To enable Razor Runtime Compilation and live static asset updates in the sample, run in the Development environment, for example with ASPNETCORE_ENVIRONMENT=Development.
Local assets such as site.js and site.css automatically receive version hashes for cache busting. You can still use asp-append-version="true" explicitly if you want to make that behavior obvious in markup.
RazorWire lets ASP.NET Core MVC apps update UI by returning Razor fragments from the server instead of building a separate JSON endpoint and client-state rendering loop.
RazorWire-enhanced forms now have a convention for server failures. A form with `rw-active="true"` gets request markers, a default form-local fallback UI for unhandled failures, server-side helpers for high-quality validation errors, and development diagnostics for anti-forgery failures.