Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

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 platform
Lab Info
Level
Beginner
Last updated
Apr 10, 2026
Duration
40m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. 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 EscapeRoomBooking directory, 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 its IsValid method.
    • Components/Pages/BookingForm.razor: The main form component. You'll wire up EditForm, 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.IsValid method throws a NotImplementedException, and BookingForm.razor includes 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, and Components/Pages/BookingForm.razor to understand the structure and review the TODO comments.

    If you need it, a solutions directory 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.cs in the step3 directory).

  2. 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.DataAnnotations namespace 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 manual if/else checks.

    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 ErrorMessage parameter 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 null or 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 on bool properties. Because a bool defaults to false and is never null, [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.

  3. Challenge

    Implementing custom validation

    The System.ComponentModel.DataAnnotations namespace 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 from ValidationAttribute and overriding the IsValid method.

    Extending ValidationAttribute

    To 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 IsValid method receives the current property value as an object?. Its job is simple: return true when the value satisfies the rule, and false when it does not. The framework calls this method during validation and uses your attribute's ErrorMessage to populate the error collection when false is 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 null or the wrong type, the is pattern returns false without a separate null check, which keeps the implementation short and clear.

    Why [Required] alone is not sufficient for boolean fields

    A bool property in C# defaults to false and is never null. The [Required] attribute considers a field invalid only when its value is null or an empty string. Because false is neither null nor an empty string, [Required] always passes on a bool property, regardless of whether the user checked the checkbox. A custom attribute that explicitly checks for true is the correct solution. --- Note: If you want to enforce that a checkbox must be checked, an alternative to using bool and a custom attribute would be using bool? with [Required]:

    [Required(ErrorMessage = "You must accept the license.")]
    public bool? AcceptLicense { get; set; }
    

    Now:

    • null would be invalid (user didn’t interact)
    • true would be valid
    • false would 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 enforce true, only non-null.

  4. 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 EditForm component replaces the plain HTML <form> element and acts as the coordinator for the entire validation lifecycle. Understanding how EditForm works, and how its child components relate to it, is the foundation for everything you'll do in this step.

    The EditForm component

    EditForm is a Blazor component that manages an EditContext, which tracks the state of a form-bound model instance. You configure it with two key pieces:

    1. Model: the C# object that the form edits. EditForm creates an EditContext from this object automatically.
    2. Submission event callbacks:
      • OnValidSubmit: fires when all validation passes
      • OnInvalidSubmit: 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 void method with no parameters (for OnValidSubmit and OnInvalidSubmit). ---

    Activating validation with DataAnnotationsValidator and ValidationSummary

    Placing <EditForm> in the markup does not enable validation on its own. You need two additional components inside the form:

    • <DataAnnotationsValidator />: connects the EditContext to 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 same EditContext and 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, and InputCheckbox. Each maps to a specific HTML input type and participates in the EditContext automatically.

    To connect an input component to a model property, you use the @bind-Value directive:

    <InputText @bind-Value="myModel.SomeProperty" />
    

    The @bind-Value directive 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.

  5. Challenge

    Displaying validation feedback

    ValidationSummary collects 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 provides ValidationMessage<T> for exactly this purpose.

    Using ValidationMessage<T> for inline errors

    ValidationMessage<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 the EditContext reports 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 ValidationMessage alongside ValidationSummary in the same form. They both read from the same EditContext, 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.

  6. 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 OnValidSubmit handler you wired up earlier. The EditForm component 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:

    1. Records the submitted data: Store the current model instance in a list or sends it to a service.
    2. Updates UI state: Set a flag or store a reference so the component knows what to render next.
    3. 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 lastSavedItem before currentItem is replaced. If you reset the model first, lastSavedItem would point to the new empty instance instead of the booking that was just submitted.

    Displaying conditional content with @if

    After a successful submission, it is common to show a confirmation message. Blazor's Razor syntax supports standard C# if statements with the @if directive:

    @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 true in HandleValidSubmit is all that is needed to make the confirmation appear immediately after the form resets. ---

    Note: This approach uses Model binding on the EditForm, so resetting the form is as simple as assigning a new BookingModel(). For more control over form state, you can use EditContext directly 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 of Model. It's useful in advanced scenarios like manual validation triggering or dynamic form fields.

  7. 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 EscapeRoomBooking
    

    Then, 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:

    1. Server-side persistence: Replace the in-memory confirmedBookings list 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 from HandleValidSubmit.

    2. Cross-field validation: Add a rule that the PreferredDate must be at least 24 hours in the future. Implement this as a second custom attribute, or use IValidatableObject on the BookingModel class itself to validate multiple properties together.

    3. Booking history display: Below the confirmation, add a table that lists all entries in confirmedBookings. Use an @foreach loop 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:

    These courses cover a wide range of topics and can help you continue your learning journey with ASP.NET Core Blazor!

About the author

Esteban Herrera has more than twelve years of experience in the software development industry. Having worked in many roles and projects, he has found his passion in programming with Java and JavaScript. Nowadays, he spends all his time learning new things, writing articles, teaching programming, and enjoying his kids.

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.

Get started with Pluralsight