Reuse model validation in Blazor client
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 theEditForm
to enable annotation-based validation support, - add the
ValidationSummary
component to display resulting validation messages - handle the
OnValidSubmit
event instead of theOnSubmit
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.