- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Guided: Building Validated Forms and User Input Workflows in ASP.NET Core 10 Blazor
This code lab teaches you how to build validated forms in ASP.NET Core Blazor using EditForm, typed input components, and data annotation validation. You'll learn how to apply built-in and custom validation attributes, bind form fields to a model, display field-level and summary error messages, and handle form submission to update application state. By the end of this lab, you'll have practical experience building a complete form workflow that you can adapt to your own Blazor applications.
Lab Info
Table of Contents
-
Challenge
Introduction
Welcome to the lab, Guided: Building Validated Forms and User Input Workflows in ASP.NET Core 10 Blazor.
Web applications need to collect reliable input from users, whether for booking a service, registering an account, or submitting a support request. Without proper validation, invalid data can reach the back end, causing errors, poor user experiences, or even security issues. Blazor's built-in form infrastructure addresses this by integrating C# data annotations directly with the UI, allowing you to define a validation rule once on the model and Blazor enforces it automatically at the form level.
In this lab, you'll build on a partially completed ASP.NET Core 10 Blazor application for Globomantic Escapes, a fictional escape room venue. Users can navigate to a booking page, enter their team details, select an escape room, and submit a reservation. By the time you finish, the application will:
- Present an interactive booking form with a variety of input types.
- Validate all fields using data annotations and a custom validation attribute.
- Display inline, field-level error messages and a summary of all validation errors.
- Process a valid submission, display a confirmation, and reset the form for new entries. ---
Familiarizing with the application structure
In the
EscapeRoomBookingdirectory, you can see the following files are fully implemented:Program.cs: A class that configures the ASP.NET Core application with Razor components and interactive server-side rendering.Components/App.razor: The root HTML shell that loads Bootstrap and the Blazor framework script.Components/Routes.razor: The router component that directs navigation to pages.Components/Layout/MainLayout.razor: The layout component with the venue header.Components/Pages/Home.razor: A landing page with a link to the booking form.
Files you will work with
Models/BookingModel.cs: The data model for a room booking. You'll add data annotation attributes to each property to define validation rules.Validation/MustBeTrueAttribute.cs: A custom validation attribute skeleton. You'll implement itsIsValidmethod.Components/Pages/BookingForm.razor: The main form component. You'll wire upEditForm, add validation components, bind input fields to model properties, add field-level error messages, and implement the submission handler.
If you try to run the application at this point, it won't work correctly. The
MustBeTrueAttribute.IsValidmethod throws aNotImplementedException, andBookingForm.razorincludes missing bindings that depend on this and other attributes. You'll address each issue step by step throughout the lab.Get started
Begin by examining
Models/BookingModel.cs,Validation/MustBeTrueAttribute.cs, andComponents/Pages/BookingForm.razorto understand the structure and review theTODOcomments.If you need it, a
solutionsdirectory is available for reference. It contains subdirectories for each step, with solution files named following the convention[step]-[task]-[file](for example,3-1-MustBeTrueAttribute.csin thestep3directory). -
Challenge
Defining validation rules on the booking model
In Blazor, form validation starts at the model level. You decorate model properties with attributes from the
System.ComponentModel.DataAnnotationsnamespace to describe what constitutes valid input. When a form is submitted, Blazor automatically reads these attributes and reports any violations to the user, without you writing manualif/elsechecks.Common data annotation attributes
The following attributes cover the most common validation scenarios:
| Attribute | Purpose | |---|---| |
[Required]| The field must have a non-null, non-empty value. | |[StringLength(max)]| The string value must not exceed the given character limit. | |[EmailAddress]| The string value must follow the format of a valid email address. | |[Range(min, max)]| The numeric value must fall within the specified range. | |[RegularExpression(re)]| The string value must match the given regular expression pattern. |Each attribute accepts an
ErrorMessageparameter that controls the text shown to the user when validation fails:[Required(ErrorMessage = "This field is required.")] [StringLength(100, ErrorMessage = "This field cannot exceed 100 characters.")] public string Title { get; set; } = string.Empty;You can stack multiple attributes on the same property. Each attribute is evaluated independently, and all failures are reported.
In the following tasks, you'll add the appropriate attributes to each property in
Models/BookingModel.cs. ---Validating text format and numeric ranges
Beyond requiring a value or limiting length, you can constrain the format of a string or the range of a number:
- The
[EmailAddress]attribute checks that user input follows a valid email structure. - The
[Range]attribute checks that a numeric property falls between a minimum and maximum value.
These attributes are especially useful when you need to reject values that are technically present but structurally wrong. For example, an email that is missing the
@symbol, or a participant count below the minimum that makes a team valid. ---Validating format with regular expressions
The
[RegularExpression]attribute restricts a field to values that match a specific pattern. This is useful for optional fields that must follow a precise format when they are provided, like an ID code, a postal code, or a voucher number.The attribute takes a pattern string using standard .NET regular expression syntax. If the field is
nullor empty, the attribute does not report an error (the validation is skipped). Only when the user enters a value does the pattern check apply. ---Applying a custom validation attribute
Some validation rules cannot be expressed with a built-in attribute. The
[Required]attribute, for example, does not work as expected onboolproperties. Because abooldefaults tofalseand is nevernull,[Required]always passes, even when a checkbox is unchecked. In cases like this, you need a custom attribute.You'll implement that custom attribute in the next step. For now, apply it to the model property that represents the "agree to terms" checkbox so that the connection between the model and the attribute is in place.
- The
-
Challenge
Implementing custom validation
The
System.ComponentModel.DataAnnotationsnamespace provides many built-in attributes, but every application eventually has a rule that none of them can express. ASP.NET Core lets you create your own validation attribute by inheriting fromValidationAttributeand overriding theIsValidmethod.Extending
ValidationAttributeTo create a custom attribute, define a class that inherits from
ValidationAttribute:public class MyCustomAttribute : ValidationAttribute { public MyCustomAttribute() : base("Default error message.") { } public override bool IsValid(object? value) { // Return true if the value is valid, false otherwise. } }The
IsValidmethod receives the current property value as anobject?. Its job is simple: returntruewhen the value satisfies the rule, andfalsewhen it does not. The framework calls this method during validation and uses your attribute'sErrorMessageto populate the error collection whenfalseis returned.Because the parameter type is
object?, you may need to handle multiple cases:- The value may be
null. - The value may be of an unexpected type.
- The value may be a correctly typed value that fails the rule. Using a C# pattern-matching expression is a concise way to address all three cases at once:
public override bool IsValid(object? value) { return value is SomeType typedValue && typedValue.MeetsSomeCondition(); }If the value is
nullor the wrong type, theispattern returnsfalsewithout a separatenullcheck, which keeps the implementation short and clear.Why
[Required]alone is not sufficient for boolean fieldsA
boolproperty in C# defaults tofalseand is nevernull. The[Required]attribute considers a field invalid only when its value isnullor an empty string. Becausefalseis neithernullnor an empty string,[Required]always passes on aboolproperty, regardless of whether the user checked the checkbox. A custom attribute that explicitly checks fortrueis the correct solution. --- Note: If you want to enforce that a checkbox must be checked, an alternative to usingbooland a custom attribute would be usingbool?with[Required]:[Required(ErrorMessage = "You must accept the license.")] public bool? AcceptLicense { get; set; }Now:
nullwould be invalid (user didn’t interact)truewould be validfalsewould still be valid, so this only works if you care about presence, not truth
So remember the catch: even with
bool?,[Required]still won't enforcetrue, only non-null. - The value may be
-
Challenge
Building the interactive form
With the model annotated and a custom attribute implemented, the next step is to build the form component. Blazor's
EditFormcomponent replaces the plain HTML<form>element and acts as the coordinator for the entire validation lifecycle. Understanding howEditFormworks, and how its child components relate to it, is the foundation for everything you'll do in this step.The
EditFormcomponentEditFormis a Blazor component that manages anEditContext, which tracks the state of a form-bound model instance. You configure it with two key pieces:Model: the C# object that the form edits.EditFormcreates anEditContextfrom this object automatically.- Submission event callbacks:
OnValidSubmit: fires when all validation passesOnInvalidSubmit: fires when any validation error is present
<EditForm Model="myModel" OnValidSubmit="HandleValidSubmit" OnInvalidSubmit="HandleInvalidSubmit"> ... </EditForm>Submission event handlers must match the delegate signature expected by the callback, typically a
voidmethod with no parameters (forOnValidSubmitandOnInvalidSubmit). ---Activating validation with
DataAnnotationsValidatorandValidationSummaryPlacing
<EditForm>in the markup does not enable validation on its own. You need two additional components inside the form:<DataAnnotationsValidator />: connects theEditContextto the .NET data annotations system. Without it, none of the attributes you placed on the model have any effect at runtime.<ValidationSummary />: renders a consolidated list of all current validation errors. It reads from the sameEditContextand updates automatically as the user corrects or introduces errors.
Place both components at the top of the form, before any input fields, so the summary is visible above the fields it describes. ---
Binding input components to model properties
Blazor provides typed input components that work inside
EditForm:InputText,InputNumber,InputSelect,InputDate, andInputCheckbox. Each maps to a specific HTML input type and participates in theEditContextautomatically.To connect an input component to a model property, you use the
@bind-Valuedirective:<InputText @bind-Value="myModel.SomeProperty" />The
@bind-Valuedirective is a two-way binding: it reads the current property value to populate the input and writes the user's input back to the property when the field changes. Without this binding, changes in the input do not flow back to the model, which means validation has no data to evaluate. -
Challenge
Displaying validation feedback
ValidationSummarycollects all active errors into one block at the top of the form, which helps users see the full picture at a glance. However, users often find it more helpful to see an error next to the field that caused it, especially in longer forms where scrolling back to the summary is inconvenient. Blazor providesValidationMessage<T>for exactly this purpose.Using
ValidationMessage<T>for inline errorsValidationMessage<T>renders an error message directly beside an individual form field. It takes a single parameter,For, which uses a lambda expression to identify the model property it targets:<ValidationMessage For="@(() => myModel.SomeProperty)" />The lambda
@(() => myModel.SomeProperty)is a C# expression that tells the component which property's validation state to observe. When theEditContextreports an error for that property, the component renders the corresponding error message inline. When the error is resolved, the component renders nothing.A few things to keep in mind:
- Place
<ValidationMessage>after the input component it corresponds to, so the error appears directly below the field. - The lambda must reference the exact model property that you annotated.The component uses reflection to match property names.
- You can use
ValidationMessagealongsideValidationSummaryin the same form. They both read from the sameEditContext, so there is no duplication risk in the underlying data, only in what is rendered.
In the following task, you'll add inline messages to four fields of the form in
BookingForm.razor. - Place
-
Challenge
Handling form submission and updating application state
Collecting and validating input is only half of the form's job. Once all validation passes, your code needs to act on the submitted data. In Blazor, you express this logic inside the
OnValidSubmithandler you wired up earlier. TheEditFormcomponent guarantees that this method is called only if all validation attributes pass, so you can work with the model data directly without additional checks.Processing a valid submission
A typical submission handler does the following:
- Records the submitted data: Store the current model instance in a list or sends it to a service.
- Updates UI state: Set a flag or store a reference so the component knows what to render next.
- Resets the form: Replaces the bound model instance with a fresh one, which clears all inputs and restores the form to its initial state.
Here's an example:
private void HandleValidSubmit() { savedItems.Add(currentItem); lastSavedItem = currentItem; showResult = true; currentItem = new ItemModel(); }Notice the order of operations: the current model is saved to
lastSavedItembeforecurrentItemis replaced. If you reset the model first,lastSavedItemwould point to the new empty instance instead of the booking that was just submitted.Displaying conditional content with
@ifAfter a successful submission, it is common to show a confirmation message. Blazor's Razor syntax supports standard C#
ifstatements with the@ifdirective:@if (showResult && lastSavedItem is not null) { <div class="alert alert-success"> <strong>Success!</strong> @lastSavedItem.Title has been saved. </div> }The block re-renders automatically whenever the component state changes, so setting your flag to
trueinHandleValidSubmitis all that is needed to make the confirmation appear immediately after the form resets. ---Note: This approach uses
Modelbinding on theEditForm, so resetting the form is as simple as assigning a newBookingModel(). For more control over form state, you can useEditContextdirectly instead:private EditContext? editContext; protected override void OnInitialized() { editContext = new EditContext(booking); } private void HandleValidSubmit() { confirmedBookings.Add(booking); lastBooking = booking; showConfirmation = true; booking = new BookingModel(); editContext = new EditContext(booking); }This requires binding the form with
<EditForm EditContext="editContext" ...>instead ofModel. It's useful in advanced scenarios like manual validation triggering or dynamic form fields. -
Challenge
Conclusion
Congratulations on successfully completing this Code Lab!
You can compile and run the application either by clicking the Run button in the bottom-right corner of the screen or by using the Terminal to run the following command from the root directory:
dotnet run --project EscapeRoomBookingThen, click the following link to open the application in a new browser tab: {{localhost:5000}}.
On the main page, click the Book your escape room session now link to open the booking form. Try submitting the form with empty fields to see the validation errors, then submit a valid booking to see the confirmation. ---
Extending the application
Here are some ideas to deepen your understanding of Blazor forms and validation:
-
Server-side persistence: Replace the in-memory
confirmedBookingslist with a call to a service that writes bookings to a database using Entity Framework Core. Inject the service into the component and call it asynchronously fromHandleValidSubmit. -
Cross-field validation: Add a rule that the
PreferredDatemust be at least 24 hours in the future. Implement this as a second custom attribute, or useIValidatableObjecton theBookingModelclass itself to validate multiple properties together. -
Booking history display: Below the confirmation, add a table that lists all entries in
confirmedBookings. Use an@foreachloop over the list and display the team name, room, date, and participant count for each entry.
By implementing these enhancements, you'll gain a broader understanding of how Blazor's form and validation infrastructure scales to handle real-world application requirements. ---
Related courses
If you'd like to continue building your ASP.NET Core Blazor skills or explore related topics, check out these courses:
- Exploring Web Frameworks in ASP.NET Core 10
- ASP.NET Core 10 Blazor Foundations
- Building Web Applications with ASP.NET Core 10 Blazor
These courses cover a wide range of topics and can help you continue your learning journey with ASP.NET Core Blazor!
-
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.