Migrating from Blazor Server
This guide helps you migrate from Blazor Server to Tombatron.Turbo while preserving real-time interactivity.
Overview
| Blazor Server | Tombatron.Turbo |
|---|---|
| C# components with SignalR | HTML + Turbo.js with SignalR |
| Full component re-render | Targeted DOM updates |
| Stateful connections | Stateless HTTP + optional streams |
| ~200KB+ initial payload | ~30KB (Turbo + SignalR) |
Key Differences
Component Model
Blazor Server:
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
Tombatron.Turbo (Form-based):
@page
@model CounterModel
<h1>Counter</h1>
<turbo-frame id="counter">
<p>Current count: @Model.Count</p>
<form method="post" asp-page-handler="Increment">
<button type="submit">Click me</button>
</form>
</turbo-frame>
public class CounterModel : PageModel
{
private static int _count = 0; // Use session/database in real apps
public int Count => _count;
public IActionResult OnPostIncrement()
{
_count++;
if (HttpContext.IsTurboFrameRequest())
{
return Partial("_Counter", this);
}
return RedirectToPage();
}
}
Real-Time Updates
Blazor Server:
// Updates automatically sync via SignalR circuit
@inject NotificationService Notifications
@foreach (var notification in Notifications.Items)
{
<div>@notification.Message</div>
}
@code {
protected override void OnInitialized()
{
Notifications.OnChange += StateHasChanged;
}
}
Tombatron.Turbo:
<!-- Subscribe to updates -->
<turbo stream="notifications"></turbo>
<!-- Target for updates -->
<div id="notification-list">
@foreach (var notification in Model.Notifications)
{
<div>@notification.Message</div>
}
</div>
// Server-side broadcast
public class NotificationService
{
private readonly ITurbo _turbo;
public async Task AddNotification(string message)
{
// Save notification...
await _turbo.Stream("notifications", builder =>
{
builder.Append("notification-list", $"<div>{message}</div>");
});
}
}
Migration Strategies
Strategy 1: Gradual Migration
Keep Blazor for complex interactive components, use Turbo for simpler pages.
// Program.cs
builder.Services.AddServerSideBlazor();
builder.Services.AddTurbo();
app.MapBlazorHub();
app.MapTurboHub();
app.MapRazorPages();
Strategy 2: Full Migration
Replace Blazor components with Razor Pages + Turbo.
- Convert Blazor components to Razor Pages
- Use Turbo Frames for partial updates
- Use Turbo Streams for real-time features
Common Patterns
Two-Way Binding
Blazor:
<input @bind="searchText" @bind:event="oninput" />
Turbo (debounced form submission):
<turbo-frame id="search-results">
<form method="get" data-turbo-frame="search-results">
<input type="search" name="q" value="@Model.Query"
data-turbo-submit-delay="300" />
</form>
@foreach (var result in Model.Results)
{
<div>@result.Name</div>
}
</turbo-frame>
Loading States
Blazor:
@if (isLoading)
{
<div>Loading...</div>
}
else
{
<div>@content</div>
}
Turbo:
<turbo-frame id="content" src="/content" loading="lazy">
<div>Loading...</div>
</turbo-frame>
Error Handling
Blazor:
<ErrorBoundary>
<ChildContent>@content</ChildContent>
<ErrorContent>Something went wrong</ErrorContent>
</ErrorBoundary>
Turbo:
<!-- Turbo shows response content on errors -->
<!-- Return error message in the frame -->
<turbo-frame id="form-frame">
<div class="error">Something went wrong</div>
</turbo-frame>
Cascading Values
Blazor:
<CascadingValue Value="@theme">
<ChildComponent />
</CascadingValue>
Turbo: Use ViewData, shared layouts, or CSS variables:
<div data-theme="@Model.Theme">
<!-- Child content inherits theme via CSS -->
</div>
What You Gain
- Simpler Mental Model - Standard HTTP request/response
- Better Performance - Smaller payloads, no persistent connections required
- Easier Debugging - Standard browser DevTools work fully
- Progressive Enhancement - Works without JavaScript (degrades to full page loads)
- Horizontal Scaling - Stateless servers are easier to scale
What You Lose
- Fine-grained reactivity - No automatic DOM diffing
- C# in browser - Logic moves to server or JavaScript
- Component lifecycle - No
OnInitialized,OnParametersSet, etc. - Built-in validation - Use standard ASP.NET validation
Step-by-Step Migration
1. Add Turbo to Your Project
dotnet add package Tombatron.Turbo
2. Configure Services
builder.Services.AddTurbo();
app.UseTurbo();
app.MapTurboHub();
3. Add Turbo.js
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8/dist/turbo.es2017-esm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@8/dist/browser/signalr.min.js"></script>
4. Convert Components to Pages
For each Blazor component:
- Create a Razor Page
- Move logic to PageModel
- Wrap interactive sections in
<turbo-frame> - Create handler methods for interactions
- Add stream subscriptions for real-time features
5. Update Navigation
Replace NavLink with standard links:
<!-- Blazor -->
<NavLink href="/products">Products</NavLink>
<!-- Turbo - just works -->
<a href="/products">Products</a>
6. Handle State
Move from component state to:
- Session state
- Database
- Distributed cache
- TempData for flash messages