A Hotwire implementation library for ASP.NET Core.
Hotwire is an approach for building fast, modern web applications while minimizing the use of JavaScript. This library makes it easy to use Hotwire in ASP.NET Core applications.
- Fast page transitions: AJAXifies links and form submissions to prevent full page reloads
- Progressive enhancement: Works even when JavaScript is disabled
- Persistent elements: Elements that maintain state across page transitions (e.g., music players)
- Tag Helper support:
<turbo-drive-meta>,<turbo-permanent>, and more
- Partial page updates: Update only specific portions of the page
- Lazy loading: Load content on demand
- Tag Helper support: Easily define frames with
<turbo-frame>
- Real-time updates: Dynamically update pages using WebSocket or SSE
- 16 standard actions: append, prepend, replace, update, remove, before, after, append_all, prepend_all, replace_all, update_all, remove_all, before_all, after_all, morph, refresh
- Custom actions: Define your own DOM manipulation logic (Rails parity achieved)
- Tag Helper support: Easily generate Turbo Streams with
<turbo-stream>and<turbo-stream-custom> - SignalR integration: Real-time broadcast functionality (see below)
Full Stimulus support (separate package Stimulus.AspNetCore):
- 5 Tag Helpers: Controller, Action, Target, Value, Class
- 9 HTML extension methods: Programmatically generate Stimulus attributes
- Lightweight JavaScript framework: Minimal JavaScript for HTML manipulation
- 20 tests: All passing
- Sample app: WireStimulus provides 5 controller examples
Real-time Turbo Streams using SignalR:
- TurboStreamsHub: SignalR Hub for managing WebSocket connections
- ITurboStreamBroadcaster: Service for broadcasting views in real-time
- Channel-based subscriptions: Broadcast only to specific channels
- Automatic reconnection: Automatic retry on connection loss
- turbo-signalr.js: Client-side JavaScript library
- Sample app: WireSignal provides notification and chat demos
dotnet add package Hotwire.AspNetCoreOr, individual packages:
dotnet add package Turbo.AspNetCore
dotnet add package Stimulus.AspNetCore- Add Tag Helpers to _ViewImports.cshtml:
@addTagHelper *, Turbo.AspNetCore- Set meta tags in _Layout.cshtml:
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"]</title>
@* Load Turbo.js *@
<script type="module" src="https://cdn.jsdelivr.net/npm/@@hotwired/turbo@@latest/dist/turbo.es2017-esm.min.js"></script>
@* Enable Turbo Drive *@
<turbo-drive-meta enabled="true" transition="fade" />
</head>
<body>
@RenderBody()
</body>- Create persistent elements (optional):
<turbo-permanent id="music-player">
<audio controls>
<source src="/audio/music.mp3" type="audio/mpeg">
</audio>
</turbo-permanent><turbo-frame id="messages">
<h2>Messages</h2>
<p>Latest messages will appear here</p>
<a href="/messages/1">Read message</a>
</turbo-frame>Controller:
using Turbo.AspNetCore;
public class MessagesController : Controller
{
public IActionResult Create(MessageViewModel model)
{
// Validation...
if (Request.IsTurboStreamRequest())
{
return TurboStream(model);
}
return RedirectToAction("Index");
}
}View (Create.cshtml):
<turbo-stream action="append" target="messages">
<template>
<div class="message">
<p>@Model.Content</p>
</div>
</template>
</turbo-stream>JavaScript (Define custom action):
// wwwroot/js/custom-actions.js
Turbo.StreamActions.notify = function() {
const message = this.getAttribute("message");
const type = this.getAttribute("type") || "info";
alert(`[${type}] ${message}`);
}View (Tag Helper):
<turbo-stream-custom action="notify" message="Saved successfully!" type="success"></turbo-stream-custom>Or HTML extension method:
@Html.TurboStreamCustom("notify", new { message = "Saved successfully!", type = "success" })For details, see the Turbo Custom Actions Guide.
Add Tag Helpers to _ViewImports.cshtml:
@addTagHelper *, Stimulus.AspNetCoreView (Dropdown example):
<div stimulus-controller="dropdown"
stimulus-value-dropdown-open="false"
stimulus-class-dropdown-active="show">
<button stimulus-action="click->dropdown#toggle"
class="btn btn-primary">
Open Dropdown
</button>
<div stimulus-target="dropdown.menu"
class="dropdown-menu">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
</div>
</div>JavaScript (Stimulus controller):
// wwwroot/js/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["menu"]
static classes = ["active"]
static values = { open: Boolean }
toggle(event) {
event.preventDefault()
this.openValue = !this.openValue
}
openValueChanged() {
if (this.openValue) {
this.menuTarget.classList.add(this.activeClass)
} else {
this.menuTarget.classList.remove(this.activeClass)
}
}
}For details, see the Stimulus.AspNetCore README.
Setup in Program.cs:
using Turbo.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// Add SignalR
builder.Services.AddSignalR();
// Add Turbo Stream Broadcaster
builder.Services.AddTurboStreamBroadcaster();
var app = builder.Build();
// Map SignalR Hub
app.MapHub<TurboStreamsHub>("/hubs/turbo-streams");
app.Run();Broadcasting in controller:
using Turbo.AspNetCore;
public class NotificationsController : Controller
{
private readonly ITurboStreamBroadcaster _broadcaster;
public NotificationsController(ITurboStreamBroadcaster broadcaster)
{
_broadcaster = broadcaster;
}
[HttpPost]
public async Task<IActionResult> Create(Notification notification)
{
// Broadcast to all subscribers
await _broadcaster.BroadcastViewAsync(
"notifications", // Channel name
"_Notification", // Partial view
notification // Model
);
return this.TurboStream("_Notification", notification);
}
}Client-side connection (JavaScript):
// After loading turbo-signalr.js
const turboSignalR = new TurboSignalR();
await turboSignalR.start();
await turboSignalR.subscribe('notifications');
// Event listener
document.addEventListener('turbo-signalr:streamReceived', (event) => {
console.log('Real-time update received!');
});For details, see the SignalR Integration Guide and WireSignal Sample.
This repository contains 5 sample applications demonstrating each Hotwire feature.
Demo of fast page transitions and persistent elements.
cd examples/WireDrive
dotnet runKey features:
- Fast page transitions (no reload required)
- Persistent elements (e.g., music player)
- Progressive enhancement
For details, see the WireDrive README.
Demo of partial page updates.
cd examples/WireFrame
dotnet runKey features:
- Partial page updates
- Lazy loading
- Nested frames
Demo of real-time updates and custom actions.
cd examples/WireStream
dotnet run
# Navigate to http://localhost:5000/CustomActionsKey features:
- 16 standard Turbo Stream actions
- 5 custom actions (set_title, notify, slide_in, highlight, console_log)
- DOM manipulation demo
Comprehensive Stimulus controller demo.
cd examples/WireStimulus
dotnet runKey features:
- 5 practical Stimulus controllers
- Dropdown: Toggle + auto-close
- Clipboard: Copy to clipboard + feedback
- Counter: Increment/decrement
- Form: Real-time validation
- Slideshow: Image carousel + autoplay
For details, see the WireStimulus README.
Demo of real-time Turbo Streams using SignalR.
cd examples/WireSignal
dotnet runKey features:
- Real-time notification system
- Live chat
- WebSocket connection via SignalR
- Channel-based subscriptions
For details, see the WireSignal README.
- Turbo Drive Guide - How to use Turbo Drive
- Turbo Custom Actions Guide - How to implement custom actions
- SignalR Integration Guide - Real-time Turbo Streams with SignalR
- Hotwire Investigation Report - Detailed implementation status and Rails parity evaluation
- Turbo Custom Actions Implementation Plan - Design and implementation plan for custom actions
- Stimulus.AspNetCore README - Complete documentation for Stimulus Tag Helpers
- WireDrive README - Turbo Drive examples
- WireStimulus README - Comprehensive Stimulus examples (5 controllers)
- WireSignal README - SignalR integration examples
- .NET 8.0 / 9.0 / 10.0 (library)
- .NET 10.0+ (sample apps)
This repository contains 3 packages:
Integrated package with all features (Turbo + Stimulus)
dotnet add package Hotwire.AspNetCoreImplementation of Turbo Drive/Frames/Streams
dotnet add package Turbo.AspNetCoreStimulus Tag Helpers and HTML extensions
dotnet add package Stimulus.AspNetCoredotnet builddotnet testTest results: All 56 tests (36 Turbo + 20 Stimulus) passing ✅
# Collect coverage (Coverlet)
dotnet test --collect:"XPlat Code Coverage"
# Restore local tools and generate HTML report (ReportGenerator)
dotnet tool restore
dotnet tool run reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"coveragereport"Hotwire.AspNetCore/
├── src/
│ ├── Hotwire.AspNetCore/ # Integrated package
│ ├── Turbo.AspNetCore/ # Turbo implementation
│ │ ├── TagHelpers/ # Turbo Tag Helpers (6)
│ │ ├── Hubs/ # SignalR Hub
│ │ └── wwwroot/js/ # turbo-signalr.js
│ └── Stimulus.AspNetCore/ # Stimulus implementation
│ └── TagHelpers/ # Stimulus Tag Helpers (5)
├── test/
│ ├── Turbo.AspNetCore.Test/ # Turbo tests (36)
│ └── Stimulus.AspNetCore.Test/ # Stimulus tests (20)
├── examples/
│ ├── WireDrive/ # Turbo Drive demo
│ ├── WireFrame/ # Turbo Frames demo
│ ├── WireStream/ # Turbo Streams demo
│ ├── WireStimulus/ # Stimulus demo
│ └── WireSignal/ # SignalR demo
└── docs/ # Documentation
| Feature | Rails (turbo-rails) | Hotwire.AspNetCore | Status |
|---|---|---|---|
| Turbo Drive | ✅ | ✅ | Fully implemented |
| Turbo Frames | ✅ | ✅ | Fully implemented |
| Turbo Streams (standard actions) | ✅ 16 actions | ✅ 16 actions | Rails parity |
| Turbo Streams (custom actions) | ✅ turbo_stream.action() | ✅ TurboStreamCustom | Rails parity |
| Turbo 8 (morph/refresh) | ✅ | ✅ | Fully implemented |
| Real-time streams | ✅ ActionCable | ✅ SignalR | Rails parity |
| Stimulus | ✅ stimulus-rails | ✅ Stimulus.AspNetCore | Rails parity |
| Tag Helpers | ✅ Rails Helpers | ✅ ASP.NET Tag Helpers | ASP.NET optimized |
MIT License