18. Mär 2024
Lesedauer 9 Min.
Blazor SSR statt MVC und Razor Pages
ASP.NET Core Blazor 8.0, Teil 1
In .NET 8.0 hat Microsoft das Einsatzspektrum von Blazor auf Server-Side-Rendering erweitert.

Blazor gab es schon vor dem Erscheinen von .NET 8.0 in verschiedenen Varianten:
- Blazor Server und Blazor WebAssembly für Single-Page-Web-Apps,
- Blazor Desktop für Hybrid-Apps auf Windows,
- Blazor MAUI für Hybrid-Apps auf Windows, macOS, iOS und Android.

Blazor-Varianten und Blazor-Projektvorlagen vor und nach .NET 8.0 (Bild 1)
Autor
- Blazor Server-Side-Rendering (auch Static Server-Side-Rendering genannt, abgekürzt in beiden Fällen mit SSR) für Multi-Page-Web-Apps,
- Blazor Auto-Modus mit Wechsel zwischen Blazor Server und Blazor WebAssembly für Single-Page-Web-Apps.
Blazor SSR ist nicht Blazor Server
Der Begriff Static Server-Side-Rendering (Blazor SSR) kann verwirren. Erste Erfahrungen in der Praxis zeigen, dass zahlreiche Entwickler denken: „Das gibt es doch schon mit Blazor Server.“ Aber nein, Blazor Server und Blazor SSR sind nicht das Gleiche.Der schon seit .NET Core 3.1 verfügbare Blazor Server rendert zwar auf dem Server, es entsteht aber dennoch eine Single Page Application (SPA). Über eine WebSocket-Verbindung werden Unterschiede des Renderings zum vorherigen Rendering (im Programmiererlatein „Diff“ oder „Change Set“ genannt) zum Client gesendet und dort per JavaScript ausgetauscht. Der Benutzer bemerkt daher nicht, dass es ein Server-Rendering gab. Die Seite ist genauso interaktiv wie beim Einsatz von Blazor WebAssembly oder eines JavaScript-basierten Webfrontend-Frameworks wie Angular, React oder Vue.js.Mit dem neuen Blazor Static Server-Side-Rendering (Blazor SSR) bietet Microsoft im Rahmen von Blazor nun auch eine rein serverseitige, aus Client-Sicht statische HTML-Erzeugung zu einer Multi Page Application (MPA). Das Rendering-Ergebnis wird bei Blazor SSR in einem Rutsch über eine normale HTTP-Verbindung zum Client gesendet, und es werden immer ganze Seiten ausgetauscht, sodass der Benutzer ein typisches „Flackern“ der Darstellung beim Seitenwechsel wahrnimmt. Neue HTTP-Anfragen beim Server werden nur durch Hyperlinks oder Formulareinsendungen ausgelöst. .NET- oder JavaScript-Code im Browser ist bei Blazor SSR nicht notwendig.Die Architektur von Blazor SSR entspricht grundsätzlich dem, was bisher mit ASP.NET Core Model-View-Controller (MVC) und ASP.NET Core Razor Pages angeboten wurde. Allerdings kann Blazor SSR mit zusätzlichen Features punkten:- Blazor SSR bietet im Gegensatz zu MVC / Razor Pages ein echtes Komponentenmodell.
- Die Razor-Template-Syntax in Blazor SSR hat mehr Funktionen.
- Asynchron gerenderte Inhalte können in der HTTP-Antwort nachgesendet werden (Streaming).
- Seitenteile können einzeln gesendet werden (Enhanced Navigation).
- In einer statisch gerenderten Site können Entwicklerinnen und Entwickler SPA-Inseln (einzelne Seiten oder auch Seitenteile) einbetten, die mit Blazor Server, Blazor WebAssembly oder im Auto-Modus laufen.
Blazor-Arten im Vergleich
Bild 2 zeigt die Architekturen ASP.NET Core MVC, ASP.NET Core Razor Pages, Blazor SSR ohne Streaming und Blazor SSR mit Streaming sowie Blazor Server und Blazor WebAssembly im Vergleich. JavaScript muss der Browser nur für Blazor Server, Blazor WebAssembly und Blazor SSR mit Streaming verarbeiten können, denn nur in diesen Fällen wird eine von Microsoft gelieferte JavaScript-Datei in den Webbrowser geladen, nämlich:
Alle Blazor-Webanwendungsarten im Vergleich zu MVC und Razor Pages (Bild 2)
Autor
- blazor.web.js (mit 180 KB) bei Projekten auf Basis der Vorlage Blazor Web App, beziehungsweise
- blazor.webassembly.js, dotnet.js, dotnet.runtime.js und dotnet.native.js (mit in Summe rund 128 KB) bei Blazor WebAssembly Standalone App.
Tabelle 1: Funktionsvergleich für MPAs mit Blazor SSR und Blazor-SPAs (mit Blazor Server, Blazor WebAssembly)
Blazor SSR kann MVC und Razor Pages ersetzen
Nun stellt sich die Frage, wofür Microsoft nun Blazor SSR einführt, wenn doch die Architektur grundsätzlich dieselbe ist wie bei ASP.NET Core MVC und ASP.NET Core Razor Pages.Der Aufbau einer Komponente und auch die Razor-Syntax sind in Razor Components, die Blazor verwendet, nicht dieselben wie bei MVC und Razor Pages. Indem Microsoft nun das modernere Komponentenmodell aus Blazor auch komplett auf den Server bringt, hat der Entwickler zukünftig die Wahl, eine Razor-Komponente nicht nur in Blazor Server und Blazor WebAssembly, sondern auch komplett auf dem Server laufen zu lassen.Man kann sicherlich sagen: Das Grab für MVC und Razor Pages ist damit schon ausgehoben, auch wenn Microsoft wohl vorerst nicht verkünden wird, dass die beiden älteren Modelle beerdigt werden sollen.Blazor SSR lässt die Vorgänger MVC und Razor Pages zu Auslaufmodellen werden, denn die Razor Components von Blazor bieten eine einfachere Syntax, insbesondere für die Formulardatenbehandlung, Datenbindung und das Einbetten von Komponenten. Zudem bieten die Razor Components bei Blazor SSR auch Enhanced Navigation und Streaming, was es beides bei MVC und Razor Pages nicht gibt.Projektvorlage für Blazor SSR
Ein Blazor-SSR-Projekt erstellt man mit der seit .NET 8.0 neuen Projektvorlage Blazor Web App (siehe Bild 3). Für Blazor SSR wählt man bei Interaktivitätsmodus die Option None (siehe auch [1]). Die Auswahl bei Interactivity Location hat in diesem Fall keine Auswirkung. Die anderen Optionen zum Interaktivitätsmodus werden im dritten Teil dieser Artikelserie in der übernächsten dotnetpro-Ausgabe besprochen.
Einstellungen der Projektvorlage „Blazor Web App“ (Bild 3)
Autor
Das erstellte Projekt hat ähnliche Elemente wie die bisherigen Blazor-Projektvorlagen, aber im Detail sind einige Dinge anders:
- App.razor ist die neue Startkomponente für die Anwendung. Seit Blazor 8.0 findet man hier die HTML-Grundstruktur, die vor Blazor 8.0 in der _Host.cshtml (bei Blazor Server) beziehungsweise index.html (bei Blazor WebAssembly) steckte. Die Datei enthält die Tags <HeadOutlet> als Platzhalter für die <Title>-Tags für den Seitentitel (siehe <Title> in Home.razor) und <Routes> als Platzhalter für den Seiteninhalt, den die Routingkomponente Routes.razor liefert.
- Die Routingkomponente Routes.razor steuert eine Razor-Komponente anhand des URL an. Dafür sorgt das Element <Found> in der Datei mit Verweis auf die Rahmenlayoutseite, diese verweist auf die MainLayout.razor im /Components/Layout-Ordner. Wie bisher kann man weitere Elemente wie <Navigating> (für Ladeanzeigen) und <NotFound> (für nicht gefundene URLs – aber nur bei Blazor WebAssembly) festlegen beziehungsweise per Attribut AdditionalAssemblies beim <Router> auch zusätzliche bei der URL-Auswertung zu berücksichtigende Assemblies angeben, wenn man zusätzliche Seiten in Razor Class Libraries definiert hat.
- Der Ordner /Components/Layout enthält die Rahmenlayoutseite MainLayout.razor mit dahinterliegender CSS-Datei (MainLayout.razor.css). Wenn Include Sample Pages nicht gewählt ist, findet man in MainLayout.razor keine Tags, sondern nur die Einbindung der jeweiligen Unterseite mit @Body. Mit Beispielseiten findet man hier eine Rahmenseite mit Navigationsleiste. Die Navigationsleiste ist (wie bisher) ausgelagert in NavMenu.razor.
- Ohne aktivierte Beispielseiten gibt es im Ordner /Components/Pages eine Startseite Home.razor mit statischem Begrüßungstext und eine Fehlerdarstellungsseite Error.razor. Mit der Option Include Sample Pages erhält man als zusätzliche Seite Weather.razor (vor Blazor 8.0: FeatchData.razor) mit der Anzeige zufälliger Wettervorhersage-Daten via Streaming Rendering (siehe unten).
- In Program.cs findet man AddRazorComponents() und app.MapRazorComponents<App>().
Ein einfacher Zähler mit Blazor SSR
Einen Zähler zu implementieren ist ein Standardbeispiel für eine Webanwendung. Heutzutage würde man einen solchen Zähler in den meisten Fällen als SPA realisieren. Hier soll der Zähler mit Blazor SSR als MPA realisiert werden, um einige daraus resultierende Herausforderungen zu erläutern.Da sich bei SSR keine clientseitigen Ereignisse behandeln lassen, kann man einen Zähler mit Blazor SSR nicht wie in anderen Blazor-Arten mit dem @onclick-Handler realisieren:
<p>Current count: <span class="hljs-keyword">@currentCount</span></p>
<button <span class="hljs-keyword">class</span>=<span class="hljs-string">"btn btn-primary"</span>
<span class="hljs-keyword">@onclick</span>=<span class="hljs-string">"IncrementCount"</span>>+<span class="hljs-keyword">@increment</span></button>
...
<span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> IncrementCount()
{
currentCount += increment;
}
Hier würde bei Blazor SSR nichts gezählt werden, da @onclick einfach ignoriert wird.Das Listing 1 zeigt eine Lösung mit Blazor SSR:
Listing 1: SSR-basierter Zähler mit Bindung an einen einfachen Datentyp
<span class="hljs-meta">@page</span> <span class="hljs-string">"/counterSSR/{initCount:int=0}/</span><br/><span class="hljs-string"> {increment:int=2}"</span><br/>&lt;PageTitle&gt;Counter&lt;/PageTitle&gt;<br/><span class="hljs-meta">@code</span> {<br/> [Parameter]<br/> <span class="hljs-keyword">public</span> int? initCount { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-number">0</span>;<br/> [Parameter]<br/> <span class="hljs-keyword">public</span> int? increment { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-number">1</span>;<br/>}<br/>&lt;h2&gt; Counter auf SSR-Weise (mit einfacher Property)<br/> &lt;/h2&gt;<br/>&lt;p&gt;Current count: <span class="hljs-meta">@CurrentCount</span>&lt;/p&gt;<br/>&lt;EditForm Model=<span class="hljs-string">"this"</span> OnValidSubmit=<span class="hljs-string">"IncrementCount"</span> <br/> FormName=<span class="hljs-string">"CounterForm1"</span>&gt;<br/> &lt;input type=<span class="hljs-string">"hidden"</span> name=<span class="hljs-string">"CurrentCount"</span> <br/> <span class="hljs-meta">@bind</span>=<span class="hljs-string">"CurrentCount"</span> /&gt;<br/> @* oder: &lt;InputNumber <span class="hljs-meta">@bind</span>-Value=<span class="hljs-string">"CurrentCount"</span> <br/> style=<span class="hljs-string">"display:none"</span> /&gt; *@<br/> &lt;button <span class="hljs-class"><span class="hljs-keyword">class</span>="<span class="hljs-title">btn</span> <span class="hljs-title">btn</span>-<span class="hljs-title">primary</span>"&gt;+<span class="hljs-meta">@increment</span>&lt;<span class="hljs-type">/button</span>&gt;</span><br/><span class="hljs-class">&lt;<span class="hljs-type">/EditForm</span>&gt;</span><br/><span class="hljs-class"><span class="hljs-meta">@code</span> </span>{<br/> <span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> void OnInitialized()<br/> {<br/> <span class="hljs-keyword">if</span> (CurrentCount <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span>) <br/> <span class="hljs-comment">// nur beim ersten Aufruf der Seite</span><br/><span class="hljs-comment"> {</span><br/><span class="hljs-comment"> CurrentCount = initCount;</span><br/><span class="hljs-comment"> }</span><br/><span class="hljs-comment"> }</span><br/><span class="hljs-comment"> [SupplyParameterFromForm]</span><br/><span class="hljs-comment"> int? CurrentCount { get; set; } = null;</span><br/><span class="hljs-comment"> private void IncrementCount()</span><br/><span class="hljs-comment"> {</span><br/><span class="hljs-comment"> CurrentCount += increment;</span><br/><span class="hljs-comment"> }</span><br/><span class="hljs-comment">}</span>
- Man braucht ein Formular, wahlweise ein Standardformular (<form>) oder die in Blazor in allen Varianten verfügbare Wrapper-Komponente <EditForm>. Das Formular ist zwingend notwendig, um einen Postback zum Webserver auszulösen, mit dem der aktuelle Zählerwert übergeben wird.
- Das Formular-Tag muss einen Namen besitzen, den man bei <form> mit @formname <form method=”POST” @formname= ”Registration” Model= ”reg” @onsubmit=”HandleSubmit”> und bei <EditForm> im Attribut FormName festlegt: <EditForm FormName=”Registration” Model=”reg” OnValidSubmit=”HandleSubmit”>
- Ohne diese Namensangabe kommt es zum Laufzeitfehler. Der Formularname muss zudem eindeutig sein.
- Der Zähler muss in einem Eingabefeld dargestellt werden. Das ist zwingend notwendig, da Blazor SSR im Gegensatz zu den SPA- und Hybrid-Varianten von Blazor zustandslos ist. Die Komponenten behalten ihren Zustand zwischen zwei Rundgängen nicht. Der Wert muss also immer wieder vom Browser zum Webserver zurück übertragen werden. Das Eingabefeld kann aber gegebenenfalls per CSS unsichtbar gemacht werden (style=”display:none”), oder man verwendet ein verstecktes Feld: <inputtype=”hidden”>
- Zum Absenden des Formulars braucht man eine Schaltfläche. Jeder Klick auf die Schaltfläche führt zu einem POST-Request mit Rundgang vom Browser zum Webserver.
- Da OnInitialized() bei jedem Rundgang immer wieder aufgerufen wird, ist darauf zu achten, dass die Initialisierung der Property CurrentCount nur beim ersten Aufruf erfolgt.
<form method=<span class="hljs-string">"<span class="hljs-keyword">POST</span>"</span> @onsubmit=<span class="hljs-string">"IncrementCount"</span>
@formname=<span class="hljs-string">"CounterForm1"</span>>
<AntiforgeryToken></AntiforgeryToken>
<input type=<span class="hljs-string">"hidden"</span> name=<span class="hljs-string">"CurrentCount"</span>
@bind=<span class="hljs-string">"CurrentCount"</span> />
<button type=<span class="hljs-string">"submit"</span> class=<span class="hljs-string">"btn btn-primary"</span>>
+@increment</button>
</form>
Anti-Forgery-Token zum Schutz gegen Cross-Site Request Forgery
Für den Schutz gegen Angriffe nach dem Prinzip der Cross-Site Request Forgery (CSRF/XSRF) liefert Microsoft in ASP.NET Core 8.0 eine neue Middleware, die Entwicklerinnen und Entwickler im Startcode einer ASP.NET-Core-Anwendung folgendermaßen aktivieren können:
<span class="hljs-selector-tag">builder</span><span class="hljs-selector-class">.Services</span><span class="hljs-selector-class">.AddAntiforgery</span>();
Dieser Aufruf aktiviert zunächst nur in der Verarbeitungspipeline das Feature IAntiforgeryValidationFeature. Ein auf ASP.NET Core aufbauendes Webframework (zum Beispiel Blazor, Web API, MVC, Razor Pages) muss sodann ein AntiForgery-Token bei Formulareinsendungen mitsenden.In der Projektvorlage Blazor Web App findet man den Aufruf von app.UseAntiforgery() nach app.UseStaticFiles() und gegebenenfalls nach app.UseAuthentication() und app.UseAuthorization().Konsequenz ist, dass bei Blazor SSR alle Formulare ein Anti-Forgery-Token erzeugen müssen. <EditForm> erledigt das automatisch. Bei <form> muss man <AntiforgeryToken></AntiforgeryToken> explizit verwenden. Damit wird jeweils sichergestellt, dass ein eindeutiges Token pro Formularabsendung erzeugt und geprüft wird.Man kann die Anti-Forgery-Token auch pro Komponente deaktivieren:
<span class="hljs-meta">@using</span> Microsoft.AspNetCore.Antiforgery;
<span class="hljs-meta">@attribute</span> [RequireAntiforgeryToken(<span class="hljs-string">required:</span> <span class="hljs-literal">false</span>)]
Zustandsverwaltung per Cookies
Das erweiterte Zähler-Beispiel in Listing 2 zeigt die Realisierung des Zählers mit Zustandsverwaltung per Browser-Cookies und einem komplexen Objekt zur Datenbindung (CounterModel). Zugriff auf die Liste der Cookies erhält man über die Klasse HttpContext im Namensraum Microsoft.AspNetCore.Http (Assembly Microsoft.AspNetCore.Http.Abstractions). Eine Instanz davon wird bei Blazor SSR per kaskadierendem Parameter an alle Komponenten übergeben (nicht wie bei anderen Blazor-Arten per Dependency Injection!).Listing 2: SSR-basierter Zähler mit Bindung an ein komplexes Objekt und Zustandsverwaltung per Cookie
@page <span class="hljs-string">"/counterSSRCookies/{initCount:int=0}/</span><br/><span class="hljs-string"> {increment:int=2}"</span><br/>&lt;PageTitle&gt;Counter auf SSR-Weise (mit komplexer <br/> Property und Cookies)&lt;/PageTitle&gt;<br/>@code {<br/> [Parameter]<br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> initCount { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-number">0</span>;<br/> [Parameter]<br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> increment { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-number">1</span>;<br/>}<br/>&lt;h2&gt;Counter auf SSR-Weise (mit komplexer Property und <br/> Cookies)&lt;/h2&gt;<br/>&lt;p&gt;Current count: @counterModel.CurrentCount&lt;/p&gt;<br/>&lt;EditForm method=<span class="hljs-string">"POST"</span> Model=<span class="hljs-string">"counterModel"</span> <br/> OnValidSubmit=<span class="hljs-string">"IncrementCount"</span> <br/> FormName=<span class="hljs-string">"CounterForm"</span>&gt;<br/> &lt;InputNumber @bind-Value=<span class="hljs-string">"counterModel.CurrentCount"</span><br/> style=<span class="hljs-string">"display:none"</span>/&gt;<br/> &lt;button <span class="hljs-keyword">class</span>=<span class="hljs-string">"btn btn-primary"</span>&gt;+@increment&lt;/button&gt;<br/>&lt;/EditForm&gt;<br/>@code {<br/> [CascadingParameter]<br/> <span class="hljs-keyword">public</span> HttpContext? HttpContext { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }<br/> <br/> [SupplyParameterFromForm]<br/> CounterModel? counterModel { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }<br/> <span class="hljs-keyword">class</span> <span class="hljs-title">CounterModel</span><br/> {<br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> CurrentCount { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-number">0</span>;<br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Increment { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-number">1</span>;<br/> }<br/> <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">OnInitializedAsync</span>(<span class="hljs-params"></span>)</span><br/><span class="hljs-function"> </span>{<br/> <span class="hljs-keyword">if</span> (counterModel <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span>) <br/> <span class="hljs-comment">// nur beim ersten Aufruf der Seite</span><br/><span class="hljs-comment"> {</span><br/><span class="hljs-comment"> counterModel = new();</span><br/><span class="hljs-comment"> counterModel.CurrentCount = initCount;</span><br/><span class="hljs-comment"> counterModel.Increment = increment;</span><br/><span class="hljs-comment"> }</span><br/><span class="hljs-comment"> }</span><br/><span class="hljs-comment"> private void IncrementCount()</span><br/><span class="hljs-comment"> {</span><br/><span class="hljs-comment"> counterModel.CurrentCount = HttpContext.Request</span><br/><span class="hljs-comment"> .Cookies["counter"] is null ? 0 : int.Parse(</span><br/><span class="hljs-comment"> HttpContext.Request.Cookies["counter"]);</span><br/><span class="hljs-comment"> counterModel.CurrentCount += increment;</span><br/><span class="hljs-comment"> HttpContext.Response.Cookies.Append("counter", </span><br/><span class="hljs-comment"> counterModel.CurrentCount.ToString());</span><br/><span class="hljs-comment"> }</span><br/><span class="hljs-comment">}</span>
Eine Blazor-SSR-Komponente bezieht die aktive Instanz von HttpContext also so:
[CascadingParameter]
<span class="hljs-keyword">public</span> HttpContext? HttpContext { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
Man liest dann das Cookie per HttpContext.Request.Cookies[”Name”]. Über den Aufruf der Append()-Methode setzt man einen Cookie-Wert in der Antwort:
HttpContext<span class="hljs-selector-class">.Response</span><span class="hljs-selector-class">.Cookies</span><span class="hljs-selector-class">.Append</span>(<span class="hljs-string">"Name"</span>,
<span class="hljs-string">"WertAlsZeichenkette"</span>)
Ausblick
Die bisherigen Beispiele in diesem Beitrag zum neuen Static Server-Side-Rendering in Blazor kamen gut ohne JavaScript aus. In zweiten Teil in der nächsten Ausgabe wird Blazor SSR seine Vorteile gegenüber MVC und Razor Pages voll ausspielen, weil durch Einsatz der Bibliothek blazor.web.js das Streamen von gerendertem HTML und das Beibehalten der Browserposition auch nach einem Roundtrip möglich wird.Fussnoten
- Microsoft Docs, Render-Modi in ASP.NET Core Blazor 8.0, http://www.dotnetpro.de/SL2404BlazorUnited1