Reuse model validation in Blazor client

March 3rd 2023 Blazor

In my last blog post about sharing contract types between the Blazor client and the backend, I mentioned that this can include business logic. A common part of business logic that can be shared in this way is model validation.

The recommended built-in approach to model validation in ASP.NET web API is validation attributes for model properties:

public class Message
{
    [Required(AllowEmptyStrings = false)]
    [MaxLength(100)]
    public string Name { get; set; } = string.Empty;

    [EmailAddress]
    public string Email { get; set; } = string.Empty;

    [Required(AllowEmptyStrings = false)]
    [MaxLength(100)]
    public string Subject { get; set; } = string.Empty;

    [Required(AllowEmptyStrings = false)]
    public string Body { get; set; } = string.Empty;
}

When such an annotated model is used as a parameter in an action method:

[HttpPost]
public async Task<IActionResult> Post([FromBody] Message message)
{
    // ---
}

the input value is automatically validated and a ProblemDetail 400 response is generated for invalid requests:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-c0a733157609f293481d7ea7124f4bc2-72582d6302308275-00",
  "errors": {
    "Body": ["The Body field is required."]
  }
}

The nice thing about validation attributes is that they can also be used in a Blazor client application. Here you can see what a web form would normally look like without any kind of client validation in Blazor:

<EditForm Model="@message" OnSubmit="@HandleSubmit">
  <div class="form-group">
    <label for="name">Name</label>
    <InputText class="form-control" id="name" @bind-Value="message.Name" />
  </div>
  <div class="form-group">
    <label for="email">Email</label>
    <InputText class="form-control" id="email" @bind-Value="message.Email" />
  </div>
  <div class="form-group">
    <label for="subject">Subject</label>
    <InputText
      class="form-control"
      id="subject"
      @bind-Value="message.Subject"
    />
  </div>
  <div class="form-group">
    <label for="body">Body</label>
    <InputTextArea class="form-control" id="body" @bind-Value="message.Body" />
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</EditForm>

Only three small changes are required to add validation support based on model class validation attributes:

  • add the DataAnnotationsValidator component to the EditForm to enable annotation-based validation support,
  • add the ValidationSummary component to display resulting validation messages
  • handle the OnValidSubmit event instead of the OnSubmit event, which fires only when the form state is valid, instead of every time the user tries to submit the data

This would be the resulting markup after applying the changes:

<EditForm Model="@message" OnValidSubmit="@HandleValidSubmit">
  <DataAnnotationsValidator />
  <ValidationSummary />
  <div class="form-group">
    <label for="name">Name</label>
    <InputText class="form-control" id="name" @bind-Value="message.Name" />
  </div>
  <div class="form-group">
    <label for="email">Email</label>
    <InputText class="form-control" id="email" @bind-Value="message.Email" />
  </div>
  <div class="form-group">
    <label for="subject">Subject</label>
    <InputText
      class="form-control"
      id="subject"
      @bind-Value="message.Subject"
    />
  </div>
  <div class="form-group">
    <label for="body">Body</label>
    <InputTextArea class="form-control" id="body" @bind-Value="message.Body" />
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</EditForm>

In my GitHub repository, you can find the full source code for a project that shares the above model with validation attributes between the web API backend and the Blazor client.

There are advantages to using the same technology for both the backend and the frontend. In this post, I described how you can easily use the same validation logic in both layers. This is more than an OpenAPI definition can provide.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License