Skip to content

Forms & Validation

Key Points

  • <EditForm> with a Model. Built-in: DataAnnotations validator, validation summary, validation message.
  • InputText, InputNumber, InputSelect, InputCheckbox, InputDate, InputRadioGroup, InputTextArea — typed inputs with two-way bind + validation.
  • FluentValidation integration via Blazored.FluentValidation package — most senior teams prefer FluentValidation.
  • Server-side validation always — client-side is UX, not security.
  • Static SSR forms post via HTTP; Interactive forms validate live.

Concepts (deep dive)

Basic EditForm

<EditForm Model="@_user" OnValidSubmit="@HandleValid">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <InputText @bind-Value="_user.Name" />
    <ValidationMessage For="@(() => _user.Name)" />

    <InputNumber @bind-Value="_user.Age" />
    <ValidationMessage For="@(() => _user.Age)" />

    <button type="submit">Save</button>
</EditForm>

@code {
    User _user = new();

    void HandleValid()
    {
        // _user is valid; save
    }
}

public class User
{
    [Required, StringLength(100)]
    public string? Name { get; set; }

    [Range(0, 150)]
    public int Age { get; set; }
}

EditForm events

<EditForm Model="@_m"
    OnSubmit="@OnAnySubmit"
    OnValidSubmit="@OnValid"
    OnInvalidSubmit="@OnInvalid" />

Built-in inputs

Component For type
InputText string
InputNumber<T> int, decimal, double
InputSelect<T> enum / item
InputCheckbox bool
InputDate<T> DateTime / DateOnly
InputRadioGroup<T> + InputRadio<T> enum
InputTextArea multi-line
InputFile file upload

FluentValidation integration

<PackageReference Include="Blazored.FluentValidation" />
public class UserValidator : AbstractValidator<User>
{
    public UserValidator()
    {
        RuleFor(u => u.Name).NotEmpty().MaximumLength(100);
        RuleFor(u => u.Age).InclusiveBetween(0, 150);
        RuleFor(u => u.Email).EmailAddress().When(u => !string.IsNullOrEmpty(u.Email));
    }
}

builder.Services.AddValidatorsFromAssemblyContaining<UserValidator>();
<EditForm Model="@_user" OnValidSubmit="@Save">
    <FluentValidationValidator />
    <ValidationSummary />
    <!-- ... -->
</EditForm>

Same validator for client (live) and server (form post). Single source of truth.

Custom validation

public class FutureDateAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(object? value, ValidationContext ctx)
    {
        if (value is DateTime d && d < DateTime.UtcNow)
            return new ValidationResult("Must be in the future", new[] { ctx.MemberName! });
        return ValidationResult.Success;
    }
}

Server validation in Static SSR

@page "/register"
@inject IUserService Users

<EditForm Model="@Input" method="post" OnValidSubmit="@RegisterAsync" FormName="Register">
    <DataAnnotationsValidator />
    <InputText @bind-Value="Input.Email" />
    <ValidationMessage For="@(() => Input.Email)" />
    <button>Register</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    public RegisterModel Input { get; set; } = new();

    async Task RegisterAsync()
    {
        if (await Users.EmailExists(Input.Email))
        {
            // Re-render with error
            return;
        }
        // ...
    }
}

[SupplyParameterFromForm] binds POST data. Static SSR forms work like classic POST.

File upload

<InputFile OnChange="@OnFileSelected" multiple />

@code {
    async Task OnFileSelected(InputFileChangeEventArgs e)
    {
        foreach (var file in e.GetMultipleFiles())
        {
            using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
            // process
        }
    }
}

EditContext

For lower-level control:

private EditContext _ctx = null!;

protected override void OnInitialized()
{
    _ctx = new EditContext(_model);
    _ctx.OnFieldChanged += (s, e) => Console.WriteLine($"Changed {e.FieldIdentifier}");
}
<EditForm EditContext="_ctx">...</EditForm>

Server-side anti-forgery

For Static SSR forms, <EditForm> auto-includes anti-forgery token via <AntiforgeryToken>.

<form method="post">
    <AntiforgeryToken />
    <!-- ... -->
</form>

Validation messages styling

CSS classes: valid, modified, invalid. Bootstrap integration:

<input class="form-control @(...)">

Customize via FieldCssClassProvider.

Real-time validation

Default: validate on field blur. For keystroke validation:

<InputText @bind-Value="_x" @bind-Value:event="oninput" />

Validation in Interactive Server vs WASM

  • Server: validation runs on server (over SignalR).
  • WASM: validation runs in browser (no server roundtrip).
  • Static SSR: validation runs on form post (server).

Cross-field validation

public class AppointmentValidator : AbstractValidator<Appointment>
{
    public AppointmentValidator()
    {
        RuleFor(a => a).Must(a => a.End > a.Start).WithMessage("End must be after start");
    }
}

Or DataAnnotations IValidatableObject.


Code: correct vs wrong

❌ Wrong: client-only validation

// Validate on client; trust the data

✅ Correct: validate on server too

[HttpPost]
public IActionResult Save(User u)
{
    if (!ModelState.IsValid) return BadRequest(ModelState);
    /* save */
}

❌ Wrong: file upload without size limit

file.OpenReadStream();   // default 500 KB; fails for larger

✅ Correct: explicit limit

file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);

Design patterns for this topic

Pattern 1 — "FluentValidation single-source"

  • Intent: same rules client + server.

Pattern 2 — "EditContext for advanced"

  • Intent: per-field events, dynamic.

Pattern 3 — "Server-validate even with client-validate"

  • Intent: UX + security.

Pattern 4 — "Static SSR forms with [SupplyParameterFromForm]"

  • Intent: classic POST workflow.

Pattern 5 — "Real-time validation via oninput"

  • Intent: keystroke feedback.

Pros & cons / trade-offs

Validator Pros Cons
DataAnnotations Built-in Limited
FluentValidation Powerful; fluent Extra dep
Custom IValidatableObject Flexible Boilerplate

When to use / when to avoid

  • Use FluentValidation in mid+ projects.
  • Use server validation always.
  • Use Static SSR forms for simple submit flows.
  • Avoid trusting client validation alone.

Interview Q&A

Q1. EditForm purpose? Provides EditContext, validation, event hooks for forms.

Q2. DataAnnotations vs FluentValidation? DA: built-in attributes. FV: code-based; richer.

Q3. ValidationSummary vs ValidationMessage? Summary: all errors. Message: per-field.

Q4. Server validation needed? Always. Client is UX; server is security.

Q5. [SupplyParameterFromForm]? Binds form POST data to component property. Static SSR.

Q6. Cross-field validation? FluentValidation RuleFor(m => m).Must(...) or IValidatableObject.

Q7. Real-time validation? @bind-Value:event="oninput" updates on every keystroke.

Q8. EditContext use case? OnFieldChanged events; manual validation; advanced control.

Q9. File upload size limit? Pass maxAllowedSize. Default 500 KB.

Q10. Anti-forgery in Blazor forms? Auto with EditForm in Static SSR. AntiforgeryToken if hand-rolled.

Q11. InputDate vs DateTime input? InputDate is the typed Blazor wrapper. Auto bind + validation.

Q12. Validation differs by render mode? Logic is the same; runs on server (Server/Static) or client (WASM).


Gotchas / common mistakes

  • ⚠️ No server validation.
  • ⚠️ File size limits default-low.
  • ⚠️ DataAnnotations only for complex rules.
  • ⚠️ Forgot anti-forgery in Static SSR custom forms.
  • ⚠️ EditForm without Model OR EditContext.

Further reading