Two Ways to Validate Blazor Forms: Data Annotations | Custom Validation

Blazor offers a powerful form-handling and validation system that allows developers to create robust user input forms, but sometimes you will run into a more complex scenarios that will require you to implement some customization to the default form validation; sometimes, we need to implement our own custom attribute class.

This is a quick(sorta) tutorial on how to implement custom validation in Blazor by implementing the ValidationAttribute class.


Summary

Blazor’s built-in form validation system makes it easy to handle user input and ensure the data meets required formats. By using EditForm, DataAnnotationsValidator, and data annotations in the model class, you can create complex validation rules with minimal effort.

But Blazor also allows you to customize your own validation rules for more complex scenarios. And you do this by implementing the ValidationAttribute class.

The first part of this tutorial looks at how to validate Blazor forms using the normal way, that is, using built-in data annotations to enforce proper input formatting. In the second part, we will convert that previous application into a custom validation via ValidationAttribute class.


Contents


Use Data Annotations to Quickly Handle User Inputs

Using Data Annotations such as [Required], [EmailAddress], [RegularExpression], or [StringLength(5)], we can easily and quickly enforce rules for user inputs. In this section we will look at how we validate forms using the built-in data annotations.

Imagine we have the following form that we want to implement in Blazor, as part of a registration process.

Requirements:

  • The user inputs a social security number, name, email, all of which are required fields.
  • The form validates that the data is entered in the correct format.
  • Social Security Number must be in the form of XXX-XX-XXXX with dashes.
  • Email address must have at least the “@” character after the username and before the domain name.
  • When the form is submitted correctly, a success message is displayed: "Hello {userModel.Name}. You have been registered."
  • The validation message should looks something similar to the image below. Notice that I included the validation summary at the top of the form.

The application will merely display the message without a backend service. We will NOT actually create a database for this tutorial for demonstration purposes.

Setting Up the UserModel model

To implement this using the built-in data annotations, we’ll begin by creating the UserModel class, which will store the form data, including the user’s name and email address. This model will use data annotations to specify validation rules.

using System;
using System.ComponentModel.DataAnnotations;

public class UserModel
{
    [Required(ErrorMessage = "Social Security Number is required")]
    [RegularExpression(@"^\d{3}-\d{2}-\d{4}$", ErrorMessage = "SSN must be in the format XXX-XX-XXXX")]
    public string SocialSecurityNumber { get; set; }

    [Required(ErrorMessage = "Name is required")]
    public string Name { get; set; }

    [Required(ErrorMessage = "Email address is required")]
    [EmailAddress(ErrorMessage = "Invalid email format")]
    public string EmailAddress { get; set; }
}

Explanation

Let’s break down how the validation works for each field:

  • Social Security Number: The [RegularExpression] enforces the XXX-XX-XXXX format for the SSN using a regular expression.
  • Name: Required and validated with the [Required] attribute.
  • Email Address: The [EmailAddress] attribute ensures that the email follows a valid email format (e.g., user@example.com).

Building the Form in Blazor

Next, we’ll create the form using Blazor’s EditForm component, binding the form inputs to the UserModel properties and enabling validation with the DataAnnotationsValidator.

@page "/registration"
@using System.ComponentModel.DataAnnotations

<EditForm Model="@userModel" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="form-group">        
        <label for="ssn">Social Security Number</label>        
        <InputText id="ssn" class="form-control" @bind-Value="userModel.SocialSecurityNumber" />        
        <ValidationMessage For="@(() => userModel.SocialSecurityNumber)" />
    </div>

    <div class="form-group">
        <label for="name">Name</label>
        <InputText id="name" class="form-control" @bind-Value="userModel.Name" />
        <ValidationMessage For="@(() => userModel.Name)" />
    </div>

    <div class="form-group">
        <label for="email">Email Address</label>
        <InputText id="email" class="form-control" @bind-Value="userModel.EmailAddress" />
        <ValidationMessage For="@(() => userModel.EmailAddress)" />
    </div>

    <button type="submit" class="btn btn-primary">Submit</button>
</EditForm>

@if (message != null)
{
    <div class="alert alert-success">@message</div>
}

Key Components

  • EditForm: Binds the form fields to the userModel instance and handles the form submission.
  • DataAnnotationsValidator: Enables validation using the data annotations defined in UserModel.
  • ValidationMessage: Displays individual validation errors under each field.
  • ValidationSummary: Shows a summary of all validation errors if any are present.

Handling the Form Submission

Once the user fills out the form, we need to handle what happens when the form is validly submitted. This logic will display the success message if everything is entered correctly.

@code {
    private UserModel userModel = new UserModel();
    private string message;

    private void HandleValidSubmit()
    {
        message = $"Hello {userModel.Name}. Your email information has been received.";
    }
}

In the HandleValidSubmit method:

  • We set a personalized success message using the name and email entered by the user.
  • This message will be displayed in the UI if the form submission is successful.

Testing the Application

After implementing the form, you can test the validation logic by entering various values. For example:

  • Enter an invalid SSN format (e.g., “12345”) to see the SSN error message.
  • Leave required fields empty to trigger the “Field is required” messages.
  • Enter an incorrectly formatted email to see the email validation message.

If all inputs are entered correctly, you’ll see the success message with the user’s name.


Implement ValidationAttribute For Custom Validation

Using ValidationAttribute provides more flexibility when the built-in data annotations (like [Required], [EmailAddress], [RegularExpression], and [Phone]) do not meet specific validation needs or when you need custom error handling logic.

Here are a few cases when you might prefer to use ValidationAttribute:

  • Custom validation logic: When you need validation logic that goes beyond the built-in attributes (e.g., more complex string matching, specific domain requirements, or context-sensitive validation).
  • Reusable attributes: If you need to validate similar fields across multiple models, a custom ValidationAttribute ensures consistency.
  • Error message customization: You can customize error messages in more advanced ways than the standard annotations allow.
  • Conditional validation: When validation depends on other fields or external factors, a custom attribute gives you full control.

When to Use Built-in Data Annotations vs ValidationAttribute

  • Use built-in data annotations when the standard validation rules (required, regex patterns, email, etc.) meet your needs.
  • Use ValidationAttribute when you have more advanced or custom validation requirements that cannot be fulfilled by the default attributes.

New Requirements

Let’s change the previous requirements to make the validation a little more complex and interesting:

  • In addition to the SSN, Name, and Email Address, we add Phone and Birthday fields to our userModel.Name to make it a little more interesting.
  • Except for Birthday, all of the fields are required.
  • Name shouldn’t be more than 50 characters.
  • For email address, enforce these rules: 1) make sure that “@” is present, 2) a dot is present for the domain part as in “gmail.com”, 3) a limit of 50 character for the username, and a limit of 50 characters for the domain part.
  • Phone number must be preceded by a country code as in +1817333444.
  • Birth date must be a valid date in the past.
  • When the form is submitted correctly, a success message is displayed: "Hello {userModel.Name}. You have been registered."

Rewriting the Code with Custom Validation Attributes

Now, let’s rewrite the code by creating custom attributes for social security number, name, email, phone number, and birthday, all inheriting from ValidationAttribute.

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

public class CustomSocialSecurityAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var ssn = value as string;
        if (!string.IsNullOrEmpty(ssn) && System.Text.RegularExpressions.Regex.IsMatch(ssn, @"^\d{3}-\d{2}-\d{4}$"))
        {
            return ValidationResult.Success;
        }
        return new ValidationResult("SSN must be in the format XXX-XX-XXXX");
    }
}
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

public class CustomNameAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var name = value as string;

        if (string.IsNullOrEmpty(name))
        {
            return new ValidationResult("Name is required");
        }

        if (name.Length > 50)
        {
            return new ValidationResult("Name cannot exceed 50 characters");
        }

        return ValidationResult.Success;
    }
}
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

public class CustomEmailAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value == null)
        {
            return new ValidationResult("Email address is required.");
        }

        string email = value.ToString();

        // Check if '@' is present and not at the start or end
        int atIndex = email.IndexOf('@');
        if (atIndex <= 0 || atIndex == email.Length - 1)
        {
            return new ValidationResult("Invalid email format. '@' must be present and not at the start or end.");
        }

        string username = email.Substring(0, atIndex);
        string domain = email.Substring(atIndex + 1);

        // Check username length
        if (username.Length > 50)
        {
            return new ValidationResult("Username part of the email must not exceed 50 characters.");
        }

        // Check domain length
        if (domain.Length > 50)
        {
            return new ValidationResult("Domain part of the email must not exceed 50 characters.");
        }

        // Check if domain contains a dot
        if (!domain.Contains("."))
        {
            return new ValidationResult("Invalid domain format. Domain must contain a dot (e.g., 'gmail.com').");
        }

        // If all checks pass, the email is valid
        return ValidationResult.Success;
    }
}

 

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

public class CustomPhoneAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var phone = value as string;

        if (string.IsNullOrEmpty(phone))
        {
            return new ValidationResult("Phone number is required");
        }

        if (System.Text.RegularExpressions.Regex.IsMatch(phone, @"^\+\d{1,3}[\s-]?\d{7,14}$"))
        {
            return ValidationResult.Success;
        }

        return new ValidationResult("Phone number must start with a country code (e.g., +1) and be in the format +1-1234567890");
    }
}

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

public class CustomBirthdayAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value is DateTime birthday && birthday <= DateTime.Today)
        {
            return ValidationResult.Success;
        }
        return new ValidationResult("Birthday must be a valid date in the past");
    }
}

Updated UserModel with Custom Attributes

using System;
using System.ComponentModel.DataAnnotations;

public class UserModel
{
    [CustomSocialSecurity]
    public string SocialSecurityNumber { get; set; }

    [CustomNameAttribute]
    public string Name { get; set; }
        
    [CustomEmail]
    public string EmailAddress { get; set; }

    [CustomPhone]
    public string PhoneNumber { get; set; }

    [CustomBirthday]
    public DateTime Birthday { get; set; }
}

Updated Blazor Form

@page "/registration"
@using System.ComponentModel.DataAnnotations

<div class="form-container">
<EditForm Model="@userModel" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="form-group">
        <label for="ssn">SSN</label>
        <InputText id="ssn" class="form-control" @bind-Value="userModel.SocialSecurityNumber" />
        <ValidationMessage For="@(() => userModel.SocialSecurityNumber)" />
    </div>
    <div class="form-group">
        <label for="name">Name</label>
        <InputText id="name" class="form-control" @bind-Value="userModel.Name" />
        <ValidationMessage For="@(() => userModel.Name)" />
    </div>

    <div class="form-group">
        <label for="email">Email</label>
        <InputText id="email" class="form-control" @bind-Value="userModel.EmailAddress" />
        <ValidationMessage For="@(() => userModel.EmailAddress)" />
    </div>

    <div class="form-group">
        <label for="phone">Phone</label>
        <InputText id="phone" class="form-control" @bind-Value="userModel.PhoneNumber" />
        <ValidationMessage For="@(() => userModel.PhoneNumber)" />
    </div>

    <div class="form-group">
        <label for="birthday">Birthday</label>
        <InputDate id="birthday" class="form-control" @bind-Value="userModel.Birthday" />
        <ValidationMessage For="@(() => userModel.Birthday)" />
    </div>

    <button type="submit" class="btn btn-primary">Submit</button>
</EditForm>

@if (message != null)
{
    <hr/>
    <div class="alert alert-success">@message</div>
}
</div>

@code {
    private UserModel userModel = new UserModel();
    private string message;

    private void HandleValidSubmit()
    {
        message = $"Hello {userModel.Name}. Your email information has been received.";
    }
}

And that’s it!

Notice that the validation executes (without submit), as soon as you cursor out of the field. And notice that the error message disappears as soon as you correct the error and you cursor out of the field.

Everything works perfectly, except the email address. I was able to enforce the presence of “@” and “.”. However, I entered “g@mail.com” and my validation accepted it. It is nearly impossible to validate email perfectly unless you use a database of all possible email domains in the market.

In any case, as you do this exercise on your own, you will find that customizing your own validation is very flexible and it is hard to go back to the built-in validation when you start implementing your own!

Conclusion

Using custom validation attributes in Blazor offers greater flexibility and control over the validation process. It is a great approach when you need more specific validation rules than the built-in data annotations can provide. By inheriting from ValidationAttribute, you can create reusable custom attributes that simplify the logic while keeping your codebase clean and organized.

This approach is recommended for:

  • Complex validation logic.
  • Reusability of validation attributes across multiple forms.
  • Precise control over validation error messages.

Incorporating custom attributes helps future-proof your application, allowing for easy modification and extension as requirements evolve.

Alejandrio Vasay
Alejandrio Vasay

Welcome to Coder Schmoder! I'm a .NET developer with a 15+ years of web and software development experience. I created this blog to impart my knowledge of programming to those who are interested in learning are just beginning in their programming journey.

I live in DFW, Texas and currently working as a .NET /Web Developer. I earned my Master of Computer Science degree from Texas A&M University, College Station, Texas. I hope, someday, to make enough money to travel to my birth country, the Philippines, and teach software development to people who don't have the means to learn it.

Articles: 23

Leave a Reply

Your email address will not be published. Required fields are marked *