I love the web and HTML. It’s certainly come a long way since its inception and what it provides as a core experience for web developers. While folks can certainly build HTML-exclusive experiences, adding forms on a page inevitably leads to introducing a backend tech stack. Recently, I’ve been experimenting with Blazor Server-side Rendering (SSR) and how developers can use its component-driven approach while still building the web experience they know and love.

In this post, we’ll see how to use the plain-old form tag with a Blazor SSR page, handle form posts, and attach antiforgery tokens.

Why not use EditForm?

Anyone familiar with Blazor would likely immediately think, “Why not use the EditForm component?” Well, for my taste, the EditForm component has so many hooks, fields, and requirements that it begins to feel like a burden compared to the humble HTML form. In my opinion, much of the EditForm functionality is overkill for an SSR scenario.

You’re welcome to use EditForm if you find its features useful.

HTML Form Blazor Basics

Blazor is a component-driven framework, and even top-level pages are considered components. In a way, it’s simpler to think of each component as a tree of other components, and you have to start somewhere. This approach means that a component page must handle all incoming requests and “route” those requests to the appropriate handlers.

In a Blazor application, there are two levels of “routing”. The first is what you’d consider traditional HTTP path routing using the @page directive. The second uses the @formname attribute on a form to inject a handler name into forms, in which you can use that additional information for application logic.

Let’s add a basic form to a page and submit it to a Blazor component. THIS WILL NOT WORK

@page "/"

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

@if (Name is { Length: >0 } name)
{
    <h2>@name</h2>
}
else
{
    <h2>Welcome to your new app.</h2>
}

<div class="my-2">
    <hr>
    <form action="" method="post">
        <div class="input-group mb-3">
            <input type="text" class="form-control" 
                   name="name" placeholder="Say your name...">
            <button
                id="button-addon2"
                class="btn btn-outline-secondary"
                type="submit">
                Say Hello
            </button>
        </div>
    </form>
</div>

@code
{
    [SupplyParameterFromForm] public string? Name { get; set; }
}

As soon as we submit the page, we get the following error.

The POST request does not specify which form is being submitted. To fix this, ensure <form> elements have a @formname attribute with any unique value, or pass a FormName parameter if using <EditForm>.

To fix this, we need to add the @formname SSR attribute and give it a unique name within the page scope.

@page "/"

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

@if (Name is { Length: >0} name)
{
    <h2>Hello, @name!</h2>
}
else
{
    <h2>Welcome to your new app.</h2>
}

<div class="my-2">
    <hr>
    <form action="" method="post" @formname="main">
        <div class="input-group mb-3">
            <input type="text" class="form-control" 
                   name="name" placeholder="Say your name...">
            <button
                id="button-addon2"
                class="btn btn-outline-secondary"
                type="submit">
                Say Hello
            </button>
        </div>
    </form>
</div>

@code
{
    [SupplyParameterFromForm] public string? Name { get; set; }
}

We still have an issue as you may notice when you submit this form.

A valid antiforgery token was not provided with the request. Add an antiforgery token, or disable antiforgery validation for this endpoint.

Blazor has an AntiforgeryToken component we forgot to add to the form.

    <form action="" method="post" @formname="main">
        <div class="input-group mb-3">
            <input type="text" class="form-control" 
                   name="name" placeholder="Say your name...">
            <button
                id="button-addon2"
                class="btn btn-outline-secondary"
                type="submit">
                Say Hello
            </button>
        </div>
        <AntiforgeryToken/>
    </form>

Woot! Now it works. If we look at the HTML rendered to the page, you’ll see what Blazor is doing to transform our form into one compatible with the Blazor request pipeline.

<form action="" method="post">
<input type="hidden" name="_handler" value="main"><div class="input-group mb-3"><input type="text" class="form-control" name="name" placeholder="Say your name...">
            <button id="button-addon2" class="btn btn-outline-secondary" type="submit">
                Say Hello
            </button></div>
        <input type="hidden" name="__RequestVerificationToken" value="CfDJ8LTlmMRHw3JNmUOvTfhPRjst1GbXskBXT7OtvUmsHbD9sMekv4N4xfoGi1hZlx-XqE_xVTjkPJ2U2T_ZN02Z92RfdhmdofvYJYlPn4QwD_Pno-HJ_z6JkjMTtgOcTkO3q72vEYX_Hl9MaHvju50tTz8"></form>

The hidden inputs are notable, as they provide values for _handler and our antiforgery token.

Being Extra with @onsubmit

I hinted at this in the previous section, but Blazor is processing the “HTML” in our components to inject and add input elements that we didn’t specify. We can take advantage of another attribute, @onsubmit, to ensure that all submissions are handled by the appropriate handler on the page.

@page "/"

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

@if (Message is not null)
{
    <h2>@Message</h2>
}
else
{
    <h2>Welcome to your new app.</h2>
}

<div class="my-2">
    <hr>
    <form action="" method="post" @formname="main" @onsubmit="Submit">
        <div class="input-group mb-3">
            <input type="text" class="form-control" 
                   name="name" placeholder="Say your name...">
            <button
                id="button-addon2"
                class="btn btn-outline-secondary"
                type="submit">
                Say Hello
            </button>
        </div>
        <AntiforgeryToken/>
    </form>
</div>

@code
{
    [SupplyParameterFromForm(FormName = "main")] public string? Name { get; set; }
    string? Message { get; set; }

    void Submit()
    {
        if (!string.IsNullOrWhiteSpace(Name))
        {
            Message = $"Hello, {Name} to your Blazor App!";
        }
    }
}

You’ll notice a few new elements in the previous code examples.

  1. Our form now has an @onsubmit attribute. This allows Blazor to execute the method at the time of a request according to the _handler value passed on a submit.
  2. The SupplyParameterFromForm now has a FormName property to match our form name. This is optional and only necessary when dealing with multiple forms on a single page.
  3. We have a Submit method with a bit more complex logic for our component.

Cool! We have a functioning form on a Blazor SSR page and haven’t sacrificed security or HTML readability. We also now understand how to add additional forms and handlers as we expand the page’s functionality.

Conclusion

HTML is great, and using it with ASP.NET Core is pretty great as well. Having Blazor SSR support makes it easy to write performant server-rendered pages while still maintaining a component-driven approach that so many folks like. I hope you found this article helpful. Cheers.