blazor-data-annotations-validation

Validation in Blazor with DataAnnotations

  • 4 min

Validation with DataAnnotations consists of declaring rules on the model using standard .NET attributes.

Instead of repeating checks in every handler, we decorate classes with metadata describing which values each property accepts.

Blazor embraces this standard and integrates it natively into its forms.

The System.ComponentModel.DataAnnotations Namespace

The first step is to define the rules of the game. This is done in the C# class that acts as the model, using attributes.

Let’s imagine a registration form.

using System.ComponentModel.DataAnnotations;

public class RegistroModel
{
    [Required(ErrorMessage = "Name is required")]
    [StringLength(50, MinimumLength = 3, ErrorMessage = "Name must be between 3 and 50 characters")]
    public string Name { get; set; } = string.Empty;

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

    [Range(18, 99, ErrorMessage = "You must be of legal age")]
    public int Age { get; set; }

    [Required]
    [RegularExpression(@"^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$",
        ErrorMessage = "Password must have 8 characters, letters and numbers")]
    public string Password { get; set; } = string.Empty;
}
Copied!

Notice that we have centralized the logic. It doesn’t matter if we use this class in Blazor, an API, or a console application: the rules travel with the data.

The most common attributes are:

  • :idea[[Required]]: The field cannot be null or empty.
  • :idea[[StringLength]]: Maximum (and optionally minimum) character length.
  • :idea[[Range]]: For numbers, defines a minimum and maximum.
  • :idea[[EmailAddress]], [Phone], [Url]: Predefined format validations.
  • :idea[[RegularExpression]]: The Swiss army knife for complex patterns.

Activating Validation with DataAnnotationsValidator

If you put these attributes on your model and run the EditForm we created in the previous article, you’ll see that… nothing happens. Blazor will let you submit the empty form.

Why? Because EditForm is agnostic to the validation system. It doesn’t assume you want to use DataAnnotations (you might want to use FluentValidation, for example).

To activate the DataAnnotations engine, we need to add the component inside the form.

<EditForm Model="@registro" OnValidSubmit="ProcessRegistration">

    <DataAnnotationsValidator />

</EditForm>
Copied!

By adding this component, Blazor scans the model, reads the attributes, and subscribes to field change events. Now, if you try to submit the empty form, OnValidSubmit will not execute.

Displaying Errors on Screen

The form now knows there are errors, but the user doesn’t. We need to tell them. Blazor offers two components for this.

ValidationSummary

Displays a list with all the form errors. It’s useful for debugging or placing it at the header of very large forms.

<EditForm Model="@registro" OnValidSubmit="Save">
    <DataAnnotationsValidator />

    <div class="alert alert-danger">
        <ValidationSummary />
    </div>

</EditForm>
Copied!

ValidationMessage

The ideal is to show the error right below the field that failed. For that, we use ValidationMessage, linking it to the specific field with For.

We need a lambda expression: For="@(() => registro.Name)".

<div class="mb-3">
    <label>Name:</label>
    <InputText class="form-control" @bind-Value="registro.Name" />

    <ValidationMessage For="@(() => registro.Name)" />
</div>
Copied!

By default, this component renders a div with the class validation-message (which is usually red).

Complete Example

Let’s see how it all fits together. Notice how we combine the EditForm, InputComponents, and validation.

<h3>User Registration</h3>

<EditForm Model="@registro" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />

    <div class="mb-3">
        <label class="form-label">Full Name</label>
        <InputText class="form-control" @bind-Value="registro.Name" />
        <ValidationMessage For="@(() => registro.Name)" />
    </div>

    <div class="mb-3">
        <label class="form-label">Email Address</label>
        <InputText class="form-control" @bind-Value="registro.Email" />
        <ValidationMessage For="@(() => registro.Email" />
    </div>

    <div class="mb-3">
        <label class="form-label">Age</label>
        <InputNumber class="form-control" @bind-Value="registro.Age" />
        <ValidationMessage For="@(() => registro.Age)" />
    </div>

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

</EditForm>

@code {
    private RegistroModel registro = new();

    private void HandleValidSubmit()
    {
        // This method ONLY executes if all [Required], [Range], etc. rules are met.
        Console.WriteLine("Valid form submitted.");
    }
}
Copied!

Validation CSS Classes

As mentioned in the Inputs article, when a field fails validation, Blazor automatically adds the invalid CSS class.

This allows us to add a red border without extra logic:

/* Blazor adds this class automatically to inputs with errors */
.form-control.invalid {
    border-color: #dc3545;
    background-image: url("data:image/svg+xml,..."); /* Error icon */
}

/* Style for the error message text */
.validation-message {
    color: #dc3545;
    font-size: 0.875em;
}
Copied!

Customizing Messages

The default messages (“The field Name is required”) are functional but ugly, and they are in English.

You can define the text using the ErrorMessage property of each attribute:

[Required(ErrorMessage = "Hey! I need to know your name.")]
public string Name { get; set; }
Copied!

Validation in the browser or by an interactive component improves the experience, but does not replace server-side validation. Any data received from the client must be re-validated before saving it or using it in a sensitive operation.