Bilder aus einem Rich-Text-Editor in Blazor hochladen

Diagramm: Home.razor erstellt den Editor per JS-Interop, und der Editor sendet Galerie- und Upload-Requests über einfaches HTTP an Endpoints in Program.cs, nicht über die SignalR-Circuit

In Blazor Server ist es verlockend, alles über die Komponente zu schicken: Die Circuit ist bereits verbunden, und C# ist direkt zur Hand. Für die Bild-Uploads des Editors funktioniert das nicht, und die Gründe sind spezifisch für Blazor. Diese Seite erklärt, warum die Bilder an einfache HTTP-Endpoints in derselben App gehen, wie diese Endpoints neben MapRazorComponents eingebunden sind und was die Antiforgery-Middleware der Blazor-Vorlage mit ihnen macht.

Warum nicht die Circuit

  • Der Editor sendet an eine URL. Drag & Drop, Einfügen, der Dialog „Insert Image“ und die Upload-Schaltfläche der Galerie senden alle einen multipart/form-data-XHR über rte-files.js an /rte/upload. Eine Razor-Komponente hat keine URL, die einen POST annimmt.
  • Die Circuit hat ein Nachrichtenlimit von 32 KB. Nachrichten vom Browser an den Server über SignalR sind standardmäßig auf 32 KB begrenzt, und das gilt auch für Ergebnisse von JS-Interop. Wir haben das HTML des Editors als String über InvokeAsync<string> zurückgegeben: 20,000 Zeichen kamen an, 40,000 schlossen die Circuit mit „Connection closed with an error“. Bild-Bytes über Interop zu schieben, würde an dieselbe Grenze stoßen. Blazors eigenes <InputFile> umgeht sie per Streaming, aber der Editor verwendet InputFile nicht.

Die Komponente erstellt also nur den Editor (Eine Bildergalerie für einen Rich-Text-Editor in Blazor zeigt, wie), und die Dateien gehen über HTTP, wie in jeder anderen ASP.NET-Core-App.

Program.cs

C# · Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
var app = builder.Build();

app.UseStaticFiles();
app.UseRteStaticFiles();   // /uploads, /richtexteditor, /rte-files.js
app.UseAntiforgery();      // Blazor's own forms; the /rte endpoints opt out (RteEndpoints.cs)

app.MapRteFiles();         // GET+POST /rte/gallery, POST /rte/upload
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

app.Run();

MapRteFiles() bildet drei Minimal-API-Endpoints ab: GET und POST /rte/gallery sowie POST /rte/upload. UseRteStaticFiles() liefert die gespeicherten Bilder unter /uploads aus, den Editor unter /richtexteditor und rte-files.js. All das läuft im selben Prozess wie die Circuit, sodass die Endpoints dieselben Dienste und dieselbe Anmeldung nutzen können.

Die Antiforgery der Vorlage

Die Vorlage für Blazor Web Apps ruft app.UseAntiforgery() für Blazors eigene Formulare auf. In .NET 8 trägt ein Endpoint, der IFormFile bindet, Antiforgery-Metadaten, daher erhielt ein Upload ohne Token mit dieser Middleware ein 400. In Development ist der Grund sichtbar: „Invalid anti-forgery token found when reading parameter "IFormFile file" from the request body as form.“ Die XHRs des Editors enthalten kein Token, deshalb nehmen sich die beiden POST-Endpoints aus:

C# · RteEndpoints.cs
// Binding [FromForm] or IFormFile makes .NET 8 demand an antiforgery token on these endpoints.
// The editor's XHRs cannot send one, so it is switched off here and RteFiles checks the
// request's origin instead (see RequireSameOrigin). guide-notes.md has the details.
rte.MapPost("/gallery", (HttpContext http, [FromForm] string? action, [FromForm] string? folder, [FromForm] string? name) =>
    Json(RteFiles.CreateFolder(http, action, folder, name)))
    .DisableAntiforgery();
C# · RteEndpoints.cs
rte.MapPost("/upload", async (HttpContext http, IFormFile? file, [FromForm] string? folder) =>
    Json(await RteFiles.Save(http, file, folder)))
    .DisableAntiforgery()
    // Kestrel accepts 30,000,000-byte bodies by default; this route needs ~4 MB.
    .WithMetadata(new RequestSizeLimitAttribute(RteFiles.MaxRequestBytes))
    .WithFormOptions(multipartBodyLengthLimit: RteFiles.MaxRequestBytes);

Anstelle des Tokens lehnt RteFiles POSTs ab, die ein Browser über Sec-Fetch-Site und Origin als von einer anderen Website stammend kennzeichnet. Blazors eigene Formulare behalten ihren Schutz. Die Upload-Route senkt außerdem das Request-Limit auf 4,065,536 Bytes, gegenüber dem Standardwert von Kestrel mit 30,000,000; Größenlimits für Uploads in ASP.NET Core behandelt, was darüber passiert. Dann prüft RteFiles.Save Ordner, Größe, Dateiendung und die ersten Bytes und speichert die Datei unter einem neuen Namen, wie in der Upload-Anleitung für ASP.NET Core.

RichTextEditor im Blazor-Beispiel mit einem hochgeladenen Foto einer Stadt bei Nacht im Text, ausgewählt, darüber die vollständige Symbolleiste
Ein Bild aus der Galerie, eingefügt auf der Blazor-Seite. Seine URL stammt vom HTTP-Endpoint.

Testen

Starten Sie das Beispielprojekt zu dieser Anleitung mit dotnet run und öffnen Sie http://localhost:5304. Öffnen Sie Insert Gallery, klicken Sie auf Upload und wählen Sie ein Foto: Es wird gespeichert, aufgelistet und per Doppelklick eingefügt. In einer Shell dieselbe Route:

Shell
curl -F "file=@photo.jpg" http://localhost:5304/rte/upload
{"url":"/uploads/photo-e2f78227.jpg"}

Im Netzwerkbereich des Browsers ist das Bild ein normaler Request POST /rte/upload neben dem WebSocket _blazor.

Dieselbe Anleitung für andere Plattformen

JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · MVC 5 · Web Forms

Weiterlesen