ForgeTrust.RazorWire 0.1.0-preview.2

This is a prerelease version of ForgeTrust.RazorWire.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package ForgeTrust.RazorWire --version 0.1.0-preview.2
                    
NuGet\Install-Package ForgeTrust.RazorWire -Version 0.1.0-preview.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ForgeTrust.RazorWire" Version="0.1.0-preview.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ForgeTrust.RazorWire" Version="0.1.0-preview.2" />
                    
Directory.Packages.props
<PackageReference Include="ForgeTrust.RazorWire" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ForgeTrust.RazorWire --version 0.1.0-preview.2
                    
#r "nuget: ForgeTrust.RazorWire, 0.1.0-preview.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ForgeTrust.RazorWire@0.1.0-preview.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ForgeTrust.RazorWire&version=0.1.0-preview.2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=ForgeTrust.RazorWire&version=0.1.0-preview.2&prerelease
                    
Install as a Cake Tool

RazorWire

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.

60-Second Quickstart

AppSurface has not published the public v0.1 package set yet, so the copy-paste path today is repo-local:

  1. Clone this repository and use the .NET 10 SDK.
  2. Run the MVC sample:
dotnet run --project examples/razorwire-mvc/RazorWireWebExample.csproj
  1. Open the URL printed in the console and navigate to /Reactivity.

Wait for the Permanent Island card to load, then click the + button. The Instance Score and Session Score update in place without a full-page reload.

When consuming package builds from a configured feed, reference ForgeTrust.RazorWire first and then continue at Add the Module. Public NuGet install commands will replace this note when the v0.1 publishing path is live.

Hero Proof

examples/razorwire-mvc/Views/Shared/Components/Counter/Default.cshtml

<div id="counter-widget" class="p-4 bg-white border border-slate-100 rounded-xl shadow-sm flex items-center justify-between group">
    <div class="flex gap-6">
        <div class="space-y-0.5">
            <span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Instance Score</span>
            <div id="instance-score-value" class="text-2xl font-black text-indigo-600 tabular-nums">@Model</div>
        </div>
        <div class="space-y-0.5">
            <span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Session Score</span>
            <div id="session-score-value" class="text-2xl font-black text-indigo-400 tabular-nums">0</div>
        </div>
    </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" class="h-10 w-10 bg-indigo-600 text-white rounded-lg flex items-center justify-center hover:bg-indigo-700 active:scale-90 transition-all shadow-sm shadow-indigo-100">
            <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
        </button>
    </form>
</div>

examples/razorwire-mvc/Controllers/ReactivityController.cs

[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();
    }

    // Safe redirect
    var referer = Request.Headers["Referer"].ToString();

    return Url.IsLocalUrl(referer) ? Redirect(referer) : RedirectToAction(nameof(Index));
}

examples/razorwire-mvc/Views/Reactivity/_CounterInput.cshtml

<input type='hidden' name='clientCount' id='client-count-input' value='@Model' />

Read the focused proof path for the file-by-file walkthrough. If copying this pattern gives you a bare 400 Bad Request, anti-forgery is the first thing to check. See Security & Anti-Forgery.

The source-backed snippets in this README are generated from docs:snippet markers in the sample app. After changing marked sample code, run:

# From the repository root:
dotnet run --project tools/ForgeTrust.AppSurface.MarkdownSnippets/ForgeTrust.AppSurface.MarkdownSnippets.csproj -- generate

For failed submissions, RazorWire also ships a convention-based form UX stack: default form-local fallbacks for unhandled failures, server helpers for validation errors, anti-forgery diagnostics in development, and styling/event hooks for consumers. See Failed Form UX or run the sample and visit /Reactivity/FormFailures.

Generated UI Design Contract

RazorWire should feel like a quiet enhancement inside the host application, not like a separate visual product placed on top of it. Package-owned generated UI follows the RazorWire generated UI design contract.

Use that contract when adding or styling RazorWire-generated nodes such as form feedback, stream status affordances, or package-owned fallback UI. It defines the scope boundary, data-attribute and CSS custom-property styling surface, accessibility baseline, override model, and anti-patterns. It does not apply to app-authored forms, partials, layouts, or RazorDocs chrome.

Add the Module

Once you already reference the RazorWire package in your app, add RazorWireWebModule to your root module:

public class MyRootModule : IAppSurfaceWebModule
{
    public void RegisterDependentModules(ModuleDependencyBuilder builder)
    {
        builder.AddModule<RazorWireWebModule>();
    }
}

Enable TagHelpers and Scripts

RazorWire markup only lights up when your views import the package TagHelpers and your shared layout renders the client scripts once. Without this step, rw:island, rw:stream-source, and rw-active forms fall back to plain HTML behavior.

examples/razorwire-mvc/Views/_ViewImports.cshtml

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, ForgeTrust.RazorWire

examples/razorwire-mvc/Views/Shared/_Layout.cshtml

<rw:scripts/>

Configure Services (Optional)

You can customize RazorWire behavior via RazorWireOptions:

services.AddRazorWire(options =>
{
    options.Streams.BasePath = "/custom-stream-path";
    options.Forms.FailureMode = RazorWireFormFailureMode.Auto;
    options.Forms.DefaultFailureMessage = "We could not submit this form. Check your input and try again.";
});

Also Possible

  • Keep sidebars and other regions independent with rw:island, including lazy loading and permanent="true" persistence across page transitions.
  • Push live updates to connected clients with IRazorWireStreamHub and rw:stream-source.
  • Return form updates from normal MVC controllers with this.RazorWireStream(), not a separate JSON API.
  • See the broader RazorWire MVC Example for registration, message publishing, islands, and SSE.
  • See Failed Form UX for server failure conventions, customization, and diagnostics.
  • See Security & Anti-Forgery for the form-update patterns that matter in production.

Core Concepts

Islands

Islands are isolated regions of a page that can load, reload, or update independently. RazorWire renders them as Turbo Frames, so you can decompose a page into smaller Razor-backed units without introducing a separate frontend app.

Streams and SSE

RazorWire can push Turbo Stream updates to one or more clients over Server-Sent Events. That makes it a good fit for counters, feeds, presence lists, and other UI that should update live while staying server-rendered.

Form Enhancement

Standard HTML forms can return targeted stream updates instead of full reloads or redirect-first flows. The counter example above is the smallest version of that story: submit a normal MVC form, return RazorWire updates, and change only the DOM you care about.

When EnableFailureUx is enabled, form[rw-active] also marks enhanced form posts with X-RazorWire-Form: true and __RazorWireForm=1. That gives the runtime and server adapters enough context to render useful failed-submission UX without every controller hand-rolling client glue.

Security & Anti-Forgery

Handling anti-forgery tokens correctly is critical when updating forms via Turbo Streams. See Security & Anti-Forgery for the detailed patterns and recommendations.

Development anti-forgery failures from RazorWire forms are rewritten into helpful form-local diagnostics when possible. Production responses stay safe and generic. See Failed Form UX.

Development Experience

RazorWire is designed for a fast feedback loop during development:

  • Razor Runtime Compilation is automatically enabled in Development, so you can edit .cshtml files and refresh without rebuilding.
  • Local scripts and styles automatically receive version hashes for cache busting, even without asp-append-version="true".

API Reference

RazorWireBridge

  • Frame(controller, id, viewName, model) returns a partial view wrapped in a <turbo-frame> with the specified ID.
  • FrameComponent(controller, id, componentName) renders a view component inside a <turbo-frame>.

IRazorWireStreamHub

  • PublishAsync(channel, content) broadcasts a Turbo Stream fragment to every subscriber on a channel.

this.RazorWireStream() (controller extension)

  • Append(target, content) adds content to the end of the target element.
  • Prepend(target, content) adds content to the beginning.
  • Replace(target, content) replaces the target element entirely.
  • Update(target, content) replaces the inner content of the target.
  • Remove(target) removes the target element.
  • FormError(target, title, message) updates the target with an encoded generated error block and marks the response handled.
  • FormValidationErrors(target, ModelState, title, maxErrors, message) updates the target with a stable MVC validation summary and marks the response handled.
  • BuildResult(statusCode) returns the stream and optionally sets the HTTP status code.

TagHelpers

rw:island

Wraps content in a <turbo-frame>.

  • id: unique identifier for the island.
  • src: URL to load content from.
  • loading: load strategy such as lazy.
  • permanent: persists the element across Turbo page transitions.
  • swr: enables stale-while-revalidate behavior.
  • client-module: client module path or name to mount for hybrid islands.
  • client-strategy: mount timing such as load, visible, or idle.
  • client-props: JSON payload passed to the client module's mount function.
<rw:island id="sidebar" src="/Reactivity/Sidebar" loading="lazy" permanent="true">
    <p>Loading sidebar...</p>
</rw:island>

form[rw-active]

Enhances a normal form so Turbo handles the submission and optional frame targeting.

  • rw-active="true" enables RazorWire form handling.
  • rw-target sets the target frame when you want to constrain the response.
  • data-rw-form-failure-target points failed-submission UI at a local error container by simple element ID, optionally prefixed with #; selector-like values are ignored.
  • data-rw-form-failure="auto" uses the default fallback UI, manual only dispatches events, and off disables the failure convention for that form.
  • Generated hidden fields __RazorWireForm and, when possible, __RazorWireFormFailureTarget help server-side adapters identify and localize form failures.
<form asp-controller="Reactivity" asp-action="IncrementCounter" method="post" rw-active="true">
    <input type="hidden" name="clientCount" value="0" />
    <button type="submit" aria-label="Increment counter">+</button>
</form>

rw:stream-source

Subscribes the page to a RazorWire stream channel.

  • channel: required channel name.
  • permanent: keeps the stream source alive across Turbo visits.
<rw:stream-source id="rw-stream-reactivity" channel="reactivity" permanent="true"></rw:stream-source>

requires-stream

Marks an element as inactive until a named stream is connected.

<button type="submit" requires-stream="reactivity">Send</button>

<time rw-type="local">

Localizes UTC timestamps on the client with the browser's Intl APIs.

  • rw-display: time, date, datetime, or relative.
  • rw-format: short, medium, long, or full.
<time datetime="@Model.Timestamp" rw-type="local" rw-display="relative"></time>

rw:scripts

Injects the client scripts RazorWire needs, including Turbo and the RazorWire assets.

<rw:scripts />

The script tag also carries failed-form runtime configuration derived from RazorWireOptions.Forms; no inline configuration script is required.

Utilities

StringUtils

  • ToSafeId(input, appendHash) sanitizes values for DOM IDs or anchors and can append a deterministic hash for uniqueness.

Client-Side Interop

RazorWire also supports hybrid islands where a server-rendered region mounts a client module:

<rw:island id="interactive-chart"
           client-module="ChartComponent"
           client-strategy="visible"
           client-props='{ "data": [1, 2, 3] }'>
</rw:island>

Static Export

RazorWire can generate CDN-ready static output with the installable razorwire .NET tool, or with the short-lived dnx tool execution path. CDN mode is the default: extensionless internal routes such as /about are emitted as files such as about.html, and exporter-managed links, frames, scripts, stylesheets, images, <img> and <source> srcset candidates, and CSS url(...) references are rewritten to the generated artifact URLs. When the conventional /_appsurface/errors/404 route is available, it emits 404.html through the same validation and rewrite path. Use --mode hybrid when the exported directory will still be served behind infrastructure that resolves application-style extensionless URLs.

CDN export validates the dependencies it can discover while crawling. Missing frame routes, unsafe query-bearing frame sources, missing internal assets, and managed URLs that cannot be rewritten fail the export with RWEXPORT### diagnostics instead of producing a broken folder. The validation boundary is deliberate: app-authored JavaScript fetches, form posts, Server-Sent Events, import maps, and other runtime behavior outside markup/CSS references are not proven static by the exporter.

Those package-based commands require a published package or an explicit local package source; public package publishing is still manual until the coordinated release automation tracked in #161 lands.

For installation, dnx, local-package, and source-run examples, see the RazorWire CLI.

Examples

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on ForgeTrust.RazorWire:

Package Downloads
ForgeTrust.AppSurface.Docs

ForgeTrust.AppSurface.Docs package for AppSurface application composition.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0-preview.1 74 6/28/2026
0.1.0-rc.4 267 6/16/2026
0.1.0-rc.3 178 6/8/2026
0.1.0-rc.2 69 6/3/2026
0.1.0-rc.1 57 5/31/2026
0.1.0-preview.4 61 5/25/2026
0.1.0-preview.3 60 5/20/2026
0.1.0-preview.2 62 5/14/2026