Upload images from a rich text editor in Razor Pages

RichTextEditor on the Razor Pages sample page with an uploaded photo of a city at night inserted into the text

In a Razor Pages app there are two different posts around the editor. Each picture is uploaded the moment it is inserted, to an endpoint that returns its URL. Later, the page's own form posts the finished HTML, with those URLs in it, to OnPost. This page shows both and why they are kept apart: the form keeps its antiforgery token, and the HTML stays small enough for Razor Pages' form limits.

The form

Razor · Pages/Index.cshtml
<form method="post" id="post">
    <input type="hidden" asp-for="Html" />
    <div id="editor"></div>
    <p><button type="submit">Save</button> @Model.Message</p>
</form>
<script>
    var editor = new RichTextEditor("#editor", {
        toolbar: "full",
        galleryEndpoint: "/rte/gallery",
        maxUploadFileSize: 4000000 // same limit as the server, checked before the upload starts
    });
    var html = document.getElementById("Html");
    editor.setHTMLCode(html.value);
    document.getElementById("post").addEventListener("submit", function () {
        html.value = editor.getHTMLCode();
    });
</script>
  • <input type="hidden" asp-for="Html" /> carries the HTML. The <form method="post"> tag helper adds the hidden __RequestVerificationToken field on its own.
  • The editor starts from the hidden field's value, so after a post the page shows the saved content again.
  • The submit handler copies editor.getHTMLCode() into the field right before the form is sent.

The page model

C# · Pages/Index.cshtml.cs
// Razor Pages validates the antiforgery token on this POST by default; the <form> tag helper
// in Index.cshtml adds the hidden __RequestVerificationToken field. Save Html to your database here.
public void OnPost() => Message = $"Saved {Html?.Length ?? 0} characters of HTML.";

[BindProperty] on Html binds the field on POST. Razor Pages validates the antiforgery token before OnPost runs; a post without it got a 400. Sanitize the HTML on the server before you store it or show it to other users: what the editor sends is still input from the browser.

Why the pictures are uploaded first

Without an upload handler, the editor embeds a pasted picture in the HTML as a base64 data: URL, which is long text. A Razor Pages form value is limited by FormOptions.ValueLengthLimit, 4,194,304 characters by default. With a valid token, a form post of 100,000 characters was saved, and one of 4,300,000 characters got a 400 before OnPost ran. With the upload route, the HTML holds only a short URL per picture. After inserting an uploaded picture into an empty editor and clicking Save, the page reported "Saved 108 characters of HTML."

The upload route

rte-files.js, loaded on the page, sends every picture to /rte/upload: drag and drop, paste, the Insert Image dialog and the gallery's Upload button. The route is a Minimal API endpoint mapped in Program.cs with app.MapRteFiles():

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);

It cannot be a page handler, because handlers are addressed as /?handler=Upload and every POST handler requires the antiforgery token, which the editor's XHR cannot send. Add an image gallery to a rich text editor in Razor Pages explains that choice. The two limits keep the request under 4,065,536 bytes; Kestrel would otherwise accept 30,000,000. RteFiles.Save then checks the origin, the folder, the size, the extension and the first bytes, and stores the file under a new name. Those checks are the same as in the ASP.NET Core upload guide.

The Insert Gallery dialog of the Razor Pages sample listing the blog, products and team folders and four uploaded photos, one selected
The gallery on the Razor Page, after four uploads.

Test it

Run the sample project for this guide with dotnet run and open http://localhost:5303. Insert a picture from the gallery, upload one with its Upload button if you like, and click Save: the message shows the length of the HTML, and the picture is still there after the page reloads. From a shell:

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

curl -s -o /dev/null -w "%{http_code}\n" -d "Html=x" http://localhost:5303/
400

The upload works without a token. The bare form post is refused, because it has none.

The same guide for other platforms

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

Read next