Welcome to today’s post.
In today’s post I will discuss how to implement server-side form validations from a hosted Blazor WebAssembly client application.
There are different ways in which to implement form validations within a Blazor web application. These variations depend on the hosted model you use for your Blazor application (Server, standalone WebAssembly, hosted WebAssembly etc.).
In this post I will cover the following objectives:
- Comparison of the difference between Blazor WebAssembly and a Blazor Server application.
- How to implement a Server Validation API controller in the Server project.
- How to make a call from a Razor component within the Client project to the validation server API.
- Using the ASP.NET Core web validation components to validate data entry using a custom validation.
- Process error responses from the server validation API, display the errors and highlight affected fields in the data entry fields.
Comparing Validations within Blazor WebAssembly and Blazor Server Applications
If you have a standalone Blazor WebAssembly application, then you will have local form validations that are confined to the client-side, or server-side validations that can run through an external ASP.NET Core web API application. In a previous post I showed how to call external HTTP REST web API methods from a Blazor WebAssembly client.
If you have a Blazor Server ASP.NET Core application, then you can have form validations implemented using server resources such as SQL databases. All resources reside on the same server as the Blazor Server, with client-side Razor components and server-side resources are built-in to the same deployed application. Razor components are downloaded to the client browser where they are rendered, and any server-side interactions, including any server-side validations, require calls to the server-side components of the application.
When both the client-side components and server-side components are hosted on the same URL space, which is the case with the hosted Blazor WebAssembly architecture, you can implement and include Web API controller classes into the server project of the application and make calls from the client project of the application. This hosted architecture does not require the Web API controller to be hosted within a separate application process. If we were not able to combine the Web API controller into the same URL space, then we would have the same architecture as an offline WebAssembly making calls to a Web API service.
To be able to use server-side validations in the same URL space for WebAssembly applications, requires you to use the hosted option when creating a Blazor WebAssembly application from the Visual Studio templates, which is what I showed in a previous post. I also showed how to make a basic call from the WebAssembly Client project to a basic service within the Server project. Once you have the basic hosted Blazor WebAssembly application implemented, I will show how to incorporate a Web API validation into the Server project and integrate access to the Client project in the next section.
Implementation of a Server-Side Web API Validation
In this section I will show to implement a basic validation Web API controller service on the Server project. The purpose of the validation service is to allow it to be called from the client part of the hosted WebAssembly.
I will also show how to configure the startup of the Server project so that the Http client connections can be called from the client project when making calls to the validation method.
As a starting point, to see how Web API controllers are hosted in the Server project, refer to my previous post where I showed how to implement Web API controllers in a hosted Blazor WebAssembly application.
I will start off with the Server project.
Add a folder called Controllers and implement an API validation method to handle errors in submitted record fields.
What we implement is a HTTP POST method Post()
[HttpPost]
public async Task<IActionResult> Post(BookCreateModel model)
{
…
}
The above method takes the submitted record parameter as a model class, BookCreateModel, which is in the source BookCreateModel.cs in the Model sub-folder (or project sub-folder \BookLoanBlazorHostedWASMApp\Shared\Models) in the Shared project. The same model class will also be referenced from within the Client project and used within the client-side validation during data entry within the forms. I will show how the client forms are validated using the model in one of the latter sections.
The model class is shown below:
namespace BookLoanBlazorHostedWASMApp.Models
{
public class BookCreateModel: BookViewModel
{
Dictionary<int, string> _media_types;
Dictionary<int, string> _genres;
public Dictionary<int, string> Genres
{
get { return _genres; }
}
public Dictionary<int, string> MediaTypes
{
get { return _media_types; }
}
public BookCreateModel(BookViewModel vm)
{
ISBN = vm.ISBN;
Author = vm.Author;
Edition = vm.Edition;
Genre = vm.Genre;
Location = vm.Location;
MediaType = vm.MediaType;
Title = vm.Title;
YearPublished = vm.YearPublished;
DateCreated = vm.DateCreated;
DateUpdated = vm.DateUpdated;
CreateLookups();
}
public BookCreateModel()
{
CreateLookups();
}
private void CreateLookups()
{
this._genres = new Dictionary<int, string>()
{
{ 1, "Fiction" },
{ 2, "Non-Fiction" },
{ 3, "Educational" },
{ 4, "Childrens" },
{ 5, "Family" },
{ 6, "Fantasy" },
{ 7, "Finance" },
{ 8, "Cooking" },
{ 9, "Technology" },
{ 10, "Gardening" }
};
this._media_types = new Dictionary<int, string>()
{
{ 1, "Book" },
{ 2, "DVD" },
{ 3, "CD" },
{ 4, "Magazine" }
};
}
}
}
The lookup properties for the media types and genres are used within the client UI data entry form for the selectable drop-down lists.
Within the Post() method, we go through the following process:
- We clear the model state, ModelState. The model state is an instance that is used to store the collection of invalidly submitted fields and values.
- Check the property/member values of submitted parameters within the model against one or more validation rules, then any errors are added to the ModelState instance.
- If there are no errors detected, then return an Ok state with the model:
Ok(ModelState
- If errors have been detected, then return a BadRequest with the model:
BadRequest(ModelState)
- Below is an implementation of the validation method Post() within the BookLoanValidationController controller class with three custom field validation checks included to build the model state:
using BookLoanBlazorHostedWASMApp.Models;
using Microsoft.AspNetCore.Mvc;
namespace BookLoanBlazorHostedWASMApp.Server.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class BookLoanValidationController: ControllerBase
{
private readonly ILogger<BookLoanValidationController> logger;
public BookLoanValidationController(
ILogger<BookLoanValidationController> logger)
{
this.logger = logger;
}
[HttpPost]
public async Task<IActionResult> Post(BookCreateModel model)
{
try
{
int rslt = 0;
bool noError = true;
ModelState.Clear();
if (model.YearPublished < 1000 || model.YearPublished > 3000)
{
ModelState.AddModelError(nameof(model.YearPublished),
"For a Book " +
"Book 'YearPublished' range must be between 1000 and 3000.");
noError = false;
}
if (string.IsNullOrEmpty(model.Title))
{
ModelState.AddModelError(nameof(model.Title),
"For a Book " +
"'Title' is required.");
noError = false;
}
if (System.Int32.TryParse(model.Edition, out rslt) == false)
{
ModelState.AddModelError(nameof(model.Edition),
"For a Book " +
"'Edition' must be an integer value.");
noError = false;
}
if (noError)
{
// can do some server side processing here ..
return Ok(ModelState);
}
}
catch (Exception ex)
{
logger.LogError("Validation Error: {Message}", ex.Message);
}
return BadRequest(ModelState);
}
}
}
Below is what an instance of the ModelState object shows when errors have been added:

The details of each record within the model state contains a key, validation state, and a description:

For multiple validation errors, you can see the entries more clearly when expanding the Value property of the ModelState:

In the next section I will show how to make client calls to the validation API method.
Calling Server-Side Web API Validations from the Client
On the Client project I will show how to setup your API endpoint to the validation Web API controller and how to make calls from a Razor form component to the validation API endpoint.
Given the implementation and routing attribute of the Web API validation controller, the API endpoint should be of the form:
[App URL Space]/api/BookLoanValidation
To use your API validation endpoints within the client application, you can store them within a settings file under the wwwroot subfolder:
\BookLoanBlazorHostedWASMApp\Client\wwwroot
The settings containing Web API and Validation Web API endpoints is shown below:
appsettings.json:
{
"UrlBackendAPI": "http://localhost:5188/api/BookLoan",
"UrlBackendValidationAPI": "http://localhost:5188/api/BookLoanValidation"
}
Note that both the Web API and Validation Web API URIs both shared the sane URL space. In some application scenarios, we may have one or both these endpoints referring to a URL that are external to the URL space of the hosted WebAssembly.
To be able to make Http client calls from the client, you will have to add the HttpClient service to the service collection on application startup. We do this my using:
builder.Services.AddHttpClient()
or the overloaded service that allows named connections:
builder.Services.AddHttpClient([named connection])
In the program files startup under \BookLoanBlazorHostedWASMApp\Client, it is unchanged from the example I showed in the previous post on API controllers:
Program.cs:
using BookLoanBlazorHostedWASMApp.Client;
using BookLoanBlazorHostedWASMApp.Client.Services;
using BookLoanBlazorHostedWASMApp.Data;
using BookLoanBlazorHostedWASMApp.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddHttpClient(
"BookLoanBlazorHostedWASMApp.ServerAPI",
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
);
builder.Services.AddScoped(sp =>
sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("BookLoanBlazorHostedWASMApp.ServerAPI")
);
builder.Services.AddTransient<IBookService, BookService>();
await builder.Build().RunAsync();
In our Razor page, to call our validation API, we use dependency injection to read the API endpoint our local app settings within the component initialization event:
@inject IConfiguration Config
…
@code {
…
private string apiEndpoint;
protected override async Task OnInitializedAsync()
{
…
apiEndpoint = Config.GetValue<string>("UrlBackendValidationAPI");
}
Then we use the HttpClient method PostAsJsonAsync() to call our validation API endpoint and process the returned message content:
// Create entered book.
private async void SaveBook()
{
…
try
{
var response = await Http.PostAsJsonAsync<BookCreateModel>(
apiEndpoint, (BookCreateModel)createbook);
var errors = await response.Content
.ReadFromJsonAsync<Dictionary<string, List<string>>>() ??
new Dictionary<string, List<string>>();
…
Calls made from the Blazor client to the server validation controller can be debugged as you would do for a typical self-contained Blazor server application with breakpoints in the API method as shown:

Now you know how to execute calls to the validation API, I will show how to validate the data entered within the Razor form in the next section using a custom validation class. A will also how to process any errors returned to the form from the server validation API response.
Custom Validation in Client Forms
Before we can call the custom server validation, we will need to validate the fields that are on a data entry form.
Below is a typical data entry form, with default edit field values for creating a new book record, which will form the basis for our client-side validations:

One useful utility we will need to help process errors arising from the above form is a custom validation class that can handle storing, displaying, and clearing errors that occur during the changing and tabbing between fields in the form. I will show the class that I will use and then explain what it does.
In the client project, in a new folder Validators in \BookLoanBlazorHostedWASMApp\Client\Validators, I then add the following CustomValidation class:
CustomValidation.cs:
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components;
namespace BookLoanBlazorHostedWASMApp.Client.Validators
{
public class CustomValidation : ComponentBase
{
private ValidationMessageStore? messageStore;
[CascadingParameter]
private EditContext? CurrentEditContext { get; set; }
protected override void OnInitialized()
{
if (CurrentEditContext is null)
{
throw new InvalidOperationException(
$"{nameof(CustomValidation)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. " +
$"For example, you can use {nameof(CustomValidation)} " +
$"inside an {nameof(EditForm)}.");
}
messageStore = new(CurrentEditContext);
CurrentEditContext.OnValidationRequested += (s, e) =>
messageStore?.Clear();
CurrentEditContext.OnFieldChanged += (s, e) =>
messageStore?.Clear(e.FieldIdentifier);
}
public void DisplayErrors(Dictionary<string, List<string>> errors)
{
if (CurrentEditContext is not null)
{
foreach (var err in errors)
{
messageStore?.Add(CurrentEditContext.Field(err.Key), err.Value);
}
CurrentEditContext.NotifyValidationStateChanged();
}
}
public void ClearErrors()
{
messageStore?.Clear();
CurrentEditContext?.NotifyValidationStateChanged();
}
}
}
The CustomValidation class provides the following helpful utility functions:
- Provides an edit context class (of type EditContext) to handle different states and events for the validation.
- Providing a message store (of type ValidationMessageStore), to store and track the validation error messages for each field change.
- Hooks the Edit Context class to the OnValidationRequested event.
- Hooks the Edit Context class to the OnFieldChanged event.
- Clears the message store when the validation is initially requested.
- Clears the message store for a field that has changed.
- Display all the errors in the message store (with DisplayErrors()).
- Clear all errors in the message store (with ClearErrors()).
Below is a typical set of data that will cause a client-side validation error:

Below is the HTML markup for the data entry fields that are bound to our model within the Razor form component in folder \BookLoanBlazorHostedWASMApp\Client\Pages:
CreateBook.razor:
@page "/createbook"
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Forms
@using BookLoanBlazorHostedWASMApp.Models
@using BookLoanBlazorHostedWASMApp.Services
@using BookLoanBlazorHostedWASMApp.Client.Validators
@using System.Net
@inject IBookService LibraryBookService
@inject HttpClient Http
@inject NavigationManager PageNavigation
@inject ILogger<CreateBook> Logger
@inject IConfiguration Config
<PageTitle>Create Book</PageTitle>
<h3>Create Book</h3>
<EditForm Model="createbook" OnValidSubmit="SaveBook">
<DataAnnotationsValidator />
<CustomValidation @ref="customValidation" />
<ValidationSummary />
<div class="form-field">
<label>Title:</label>
<div>
<InputText @bind-Value=createbook!.Title></InputText>
</div>
</div>
<div class="form-field">
<label>Author:</label>
<div>
<InputText @bind-Value=createbook.Author></InputText>
</div>
</div>
<div class="form-field">
<label>Year Published:</label>
<div>
<InputNumber @bind-Value=createbook.YearPublished></InputNumber>
</div>
</div>
<div class="form-field">
<label>Genre:</label>
<div>
<InputSelect @bind-Value="createbook.Genre" required>
<option value="">Select genre ...</option>
@foreach (var genres in createbook.Genres)
{
<option value="@genres.Value.ToString()">@genres.Value.ToString()</option>
}
</InputSelect>
</div>
</div>
<div class="form-field">
<label>Edition:</label>
<div>
<InputText @bind-Value=createbook.Edition></InputText>
</div>
</div>
<div class="form-field">
<label>ISBN:</label>
<div>
<InputText @bind-Value=createbook.ISBN></InputText>
</div>
</div>
<div class="form-field">
<label>Location:</label>
<div>
<InputText @bind-Value=createbook.Location></InputText>
</div>
</div>
<div class="form-field">
<label>Media Type:</label>
<div>
<InputSelect @bind-Value="createbook.MediaType">
<option value="">Select media type ...</option>
@foreach (var mediatypes in createbook.MediaTypes)
{
<option value="@mediatypes.Value.ToString()">@mediatypes.Value.ToString()</option>
}
</InputSelect>
</div>
</div>
<br />
<button class="btn btn-primary"
@onclick="SaveBook" disabled="@disabled">
Save Book
</button>
</EditForm>
<br />
In the above HTML Razor script markup, I have bound the edit fields to the same model class createbook and bound the submission event to the SaveBook() handler in the code behind:
<EditForm Model="createbook" OnValidSubmit="SaveBook">
Below is a debug snapshot of the createbook model being populated with entered data in the form:

The content of the model will be posted to the server validation API, with any resulting errors displayed in the validation summary components within the form.
Below are the HTML tags of the ASP.NET Core web components which provide the data annotations validation, a validation summary, and bind the custom validation class to the edit fields:
<DataAnnotationsValidator />
<CustomValidation @ref="customValidation" />
<ValidationSummary />
In our code behind variable declarations, we have the variables for the model class instance, createbook, the custom validation class instance, customValidation, and the initial instance of the book instance that is converted to the createbook instance using a copy constructor within the BookViewModel class:
@code {
private BookViewModel book = new();
private BookCreateModel? createbook;
private CustomValidation? customValidation;
private bool disabled;
private bool isFormInvalid = true;
private string? message = "";
private string messageStyles = "visibility:hidden";
private string apiEndpoint;
The instance for the model class and the endpoint are established in the OnInitializedAsync() event as shown:
protected override async Task OnInitializedAsync()
{
createbook = new BookCreateModel(book);
apiEndpoint = Config.GetValue<string>("UrlBackendValidationAPI");
}
Below is the first part of the SaveBook() event handler that uses the custom validation instance method ClearErrors() to clear custom validation errors, then make the call to the validation API endpoint, read the error response as JSON.
// Create entered book.
private async void SaveBook()
{
customValidation?.ClearErrors();
try
{
var response = await Http.PostAsJsonAsync<BookCreateModel>(
apiEndpoint, (BookCreateModel)createbook);
var errors = await response.Content
.ReadFromJsonAsync<Dictionary<string, List<string>>>() ??
new Dictionary<string, List<string>>();
We then check the status code of the response to see if it was a 400 error code (Bad Request), which you can also check from the browser Developer tools, where you will also see the 400 Bad Request status code in the Request header from the HTTP POST request:

If so, then we use the custom validation instance method DisplayErrors() to display the errors:
if (response.StatusCode == HttpStatusCode.BadRequest && errors.Any())
{
customValidation?.DisplayErrors(errors);
}
The errors are returned as a JSON structure, which can be viewed from the Response tab of the submitted request in the browser Developer tools:

The earlier data entry form we saw with the Year Published field having a value of 1 and the Edition field left blank yields the following validation summary and error field highlighting arising from the combined custom validation, validation summary and data annotations validations:

If the error response turns out to be an unhandled error, such as a 500 error, then it is re-thrown and send to the exception block:
else if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"Validation failed. Status Code: {response.StatusCode}");
}
If there are no errors, and all inputs satisfy the validation rules, then a call is made to the backend to create the data.
else
{
disabled = true;
messageStyles = "color:green";
message = "The form has been processed.";
var bookId = await LibraryBookService.SaveBook(createbook!);
PageNavigation.NavigateTo($"statuspage/?id={bookId}");
}
}
Below is the catch block which intercepts non-validation related errors that come from the validation API and logs them to the browser console log:
catch (Exception ex)
{
Logger.LogError("Form processing error: {Message}", ex.Message);
disabled = true;
messageStyles = "color:red";
message = "There was an error processing the form.";
}
}
}
In the next section, I will show some common errors that can occur when calling a validation server API.
Common Errors that can occur when calling the Validation Server API
Below are two errors I have experienced during implementation and testing of calls to the Server Validation API.
System.Net.Http.HttpRequestException: TypeError: Failed to fetch
This error typically occurs when the URL space (or origin) of the client does not match that of the server.
In the Visual Studio Debugger Console you will see:
fail: BookLoanBlazorHostedWASMApp.Client.Pages.CreateBook[0]
Form processing error: TypeError: Failed to fetch
info: System.Net.Http.HttpClient.BookLoanBlazorHostedWASMApp.ServerAPI. LogicalHandler[100]
Start processing HTTP request GET https://localhost:5188/api/BookLoan
info: System.Net.Http.HttpClient.BookLoanBlazorHostedWASMApp.ServerAPI. ClientHandler[100]
Sending HTTP request GET https://localhost:5188/api/BookLoan
crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering. WebAssemblyRenderer[100]
Unhandled exception rendering component: TypeError: Failed to fetch
System.Net.Http.HttpRequestException: TypeError: Failed to fetch
When the error returns, the content cannot be rendered as the error cannot be parsed by the Blazor Razor component renderer.
The same error can be viewed within the browser Developer Tools Debug Console:
info: System.Net.Http.HttpClient.BookLoanBlazorHostedWASMApp.ServerAPI. LogicalHandler[100]
Start processing HTTP request POST https://localhost:5188/api/BookLoanValidation
_bound_js_globalThis_console_info:7 info: System.Net.Http.HttpClient.BookLoanBlazorHostedWASMApp.ServerAPI.ClientHandler[100]
Sending HTTP request POST https://localhost:5188/api/BookLoanValidation
dotnet.7.0.16.xdh5314hjk.js:5
On closer inspection of the errors within the console, I notice an SSL protocol error:
POST https://localhost:5188/api/BookLoanValidation net::ERR_SSL_PROTOCOL_ERROR
(anonymous) @ dotnet.7.0.16.xdh5314hjk.js:5
_bound_js_globalThis_console_error:7 fail: BookLoanBlazorHostedWASMApp.Client.Pages.CreateBook[0]
Form processing error: TypeError: Failed to fetch
A screenshot within the browser developer tools console showing the error is below:

The cause of the above error was because my configuration of the API endpoints in the client application settings was incorrectly set to use https instead of http. My client settings file \Client\wwwroot\appsettings.json was:
{
"UrlBackendAPI": "https://localhost:5188/api/BookLoan",
"UrlBackendValidationAPI": "https://localhost:5188/api/BookLoanValidation",
}
I then changed it to:
{
"UrlBackendAPI": "http://localhost:5188/api/BookLoan",
"UrlBackendValidationAPI": "http://localhost:5188/api/BookLoanValidation"
}
Following the above change, the error vanished!
The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.
This error only occurs during debugging or when there is a long running operation during server-side validation.
In the Visual Studio Debugger Console you will see:
_bound_js_globalThis_console_error:7
fail: BookLoanBlazorHostedWASMApp.Client.Pages.CreateBook[0]
Form processing error: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.
A screenshot within the browser developer tools console showing the error is below:

The above has been an overview of how to implement and configure server-side validation of a Hosted Blazor WebAssembly application.
That is all for today’s post.
I hope that you have found this post useful and informative.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.