Data model validation in ASP.NET Core 8.0 based on HTMX

Model validation is confirmation of the fact that the data received from the user is suitable for association with the model. Otherwise, you need to show the user a message describing the problem. The first part of the process – validating the received data – allows us to maintain the integrity of our application domain. By discarding unusable data, we prevent unwanted errors from occurring. Well, the second part of the process is helping the user correct his error. When the user is not provided with useful information about the cause of the failure, he becomes dissatisfied. We all want the user to be satisfied, so let’s start creating a project for examples.

Creating an example project

Before we begin, we'll create a simple application based on a standard MVC pattern, to which we'll apply various model validation techniques. Create a new project called HypermediaValidation based on the “ASP.NET Core Web Application (Model-View-Controller)” template.

Project preparation

Open the project and change the contents of the launch settings file launchSettings.json in the Properties folder. Correct the ports in the lines that are marked with comments, as shown in the listing below.

Listing: Corrected contents of the launchSettings.json file in the Properties folder

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:48233",
      "sslPort": 44391
    }
  },
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5000", // Исправить
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      // Исправить следующую строку
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

In this file we configure the addresses and ports on which our web application will run in debug mode.

Replace the entire contents of the Home controller file with the content shown in the listing below. Remove unnecessary constructor and unused action methods, leaving only the starting point Index. Let only the default visual representation be shown when the application is initially launched.

Listing: Replacing the contents of the HomeController.cs file

using Microsoft.AspNetCore.Mvc;

namespace HypermediaValidation.Controllers;

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

This visual representation needs to change. The listing below shows the contents of the Index.cshtml file in the Views/Home folder. It is necessary to bring this file into the form presented in this listing. Let it display basic information about this experienced application for now.

And at the end of the preparatory stage, let’s slightly correct the visual representation template, as in the next listing.

Listing: Visual Views Template Views/Shared/_Layout.cshtml

<!DOCTYPE html>
<html lang="en">
<head>
…
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light 
          bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                @* Изменить название в следующей строке *@
                <a class="navbar-brand" asp-area="" asp-controller="Home" 
                  asp-action="Index">Опытное приложение</a>
                <button class="navbar-toggler" type="button" 
                  data-bs-toggle="collapse" data-bs-target=".navbar-collapse" 
                  aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex 
                  justify-content-between">
                    @* Удалить лишний блок *@
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            @* Изменить название пункта меню *@
                            <a class="nav-link text-dark" asp-area="" 
                             asp-controller="Home" asp-action="Index">Главная</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>


    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>


    <footer class="border-top footer text-muted">
        @* Изменить содержимое блока *@        
        <div class="container">
            &copy; 2024 - Опытное приложение
        </div>
    </footer>
    <script src="https://habr.com/ru/articles/852110/~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

For brevity, the contents of the block are not shown. In this article, we make the most of the ready-made MVC application template provided by the ASP.NET Core platform.

Unused visual views Views/Home/Privacy.cshtml and Views/Shared/Error.cshtml should be removed.

Support for hypermedia systems

It's time to add client libraries and hypermedia support packages to your application. Add the htmx.js client library to our application using the libman client library package manager. Right-click on the project – “Add” – “Client Library”. In the dialog box that opens, type htmx in the search bar and select the latest version of the library. After clicking the “Install” button, the necessary files will be automatically added to the wwwroot/lib/htmx folder.

Next, add Nuget packages to support the Htmx and Htmx.TagHelpers hypermedia systems. They are shown in the screenshot below.

Add support for tag helpers to support hypermedia systems in the Views/_ViewImports.cshtml file. Add a commented line to this file.

Listing: Adding support for new tag helpers in Views/_ViewImports.cshtml

@using ProgrammersClub
@using ProgrammersClub.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Htmx.TagHelpers // Добавить

You need to add the use of the Htmx.js client library to your visual template. Edit the template file Views/Shared/_Layout.cshrml as shown in the following listing. Also add the commented line to the file.

Listing: Page Template Views/Shared/_Layout.cshtml

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - HypermediaValidation</title>
    <link rel="stylesheet" href="https://habr.com/ru/articles/852110/~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <script src="~/lib/htmx/htmx.js"></script> // Добавить
    <link rel="stylesheet" href="~/HypermediaValidation.styles.css"
       asp-append-version="true" />
</head>
<body>
…
</body>
</html>

To support hypermedia systems, you only need to add one line to the block of the visual template. For brevity, the contents of the block are not shown.

Basic types

Regardless of the validation method you use, you need to add primitives to your application to receive and pass data between the visual representation and the controller action method. As a rule, the controller action method checks whether the model is filled in correctly. And based on this check, a decision is made on what to return as the result of executing the controller action method. You need to create a base class for transmitting/receiving user data with support for data validation.

Create a new class in the Models folder called UserWebModel and fill it with the content of the following listing.

Listing: Web user model UserWebModel

using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace HypermediaValidation.Models;

public class UserWebModel
{
    [Display(Name = "Имя")]
    [Required(ErrorMessage = "Пожалуйста, введите свое имя")]
    [StringLength(10, MinimumLength = 3, ErrorMessage = "Длинна имени должна быть от 3 до 10 символов")]
    public string? Name { get; init; }

    [Display(Name = "Дата рождения")]
    [Required(ErrorMessage = "Пожалуйста, ведите дату рождения")]
    [DataType(DataType.Date)]
    public DateTime? BirthDay { get; init; }
    
    [Display(Name = "Согласен с правилами")]
    public bool IsTermsAccepted { get; init; }

    public void Validate(ModelStateDictionary state)
    {
        if (IsTermsAccepted == false)
        {
            state.AddModelError(nameof(IsTermsAccepted), 
                "Необходимо подтвердить согласие с правилами");
        }
        if (BirthDay >= DateTime.Now)
        {
            state.AddModelError(nameof(BirthDay), 
                "Дата рождения должна быть в прошлом");
        }
    }
}

This new class will be responsible for storing the registered user's data. Validation attributes are immediately set in this class. The Required attribute is for checking that the name was entered at all, StringLength is for checking the length of the entered username, and DataType is for checking that the date of birth was entered correctly. The Validate() method will perform additional server-side validation. Remove the unused file with the ErrorViewModel class from this folder.

Simplest hypermedia validation

This option is characterized by the fact that all data validation is performed on the server side. Of course, client-side checking can help correct the problem before the request is sent to the server, but it is generally not sufficient to fully check the status. I can give a user registration form as a typical example. Although a username can meet verification criteria such as the length of the username, the uniqueness of the name can be verified solely on the server side by performing a uniqueness check on the username among all existing users.

We can use HTMX to submit the form to the server and use the mechanisms of the ASP.NET Core platform to highlight and display issues. This avoids the problem of both creating incomplete checks on the client side and eliminating duplication of checks on both sides – on the client side and on the server side. All validation rule code can be concentrated in one class, on the server, which will be responsible for transferring data from the visual representation. Regardless of what stage the validation occurs – filling out a form or sending a completed form with data – the same code is called. This solution will slightly increase the productivity of application development, make it easier to maintain the application in the future, and protect against stupid mistakes. And all because data validation is not duplicated anywhere and is located in one place.

The simplest validation will be shown using the example of a user registration form. First, add a new controller called SampleController and fill it with the content of the following listing.

Listing: Controller with the simplest validation option SampleController

using Htmx;
using HypermediaValidation.Models;
using Microsoft.AspNetCore.Mvc;

namespace HypermediaValidation.Controllers;

public class SimpleController : Controller
{
    public IActionResult Registration()
    {
        return View();
    }

    [HttpPost]
    public IActionResult Registration(UserWebModel model)
    {
        model.Validate(ModelState);
        if (Request.IsHtmx()) 
          return PartialView("Partial/RegistrationPartial", model);
        if (!ModelState.IsValid) return View(model);

        return View("Complete");
    }
}

In this controller, the first action method called Registration() returns as a result a visual representation of the participant's registration form. The second method in this controller takes the data model from the registration form as parameters. If a request was made from a hypermedia system, then the second method validates the data and returns a partial view with the validation results. And only if the data form was sent by clicking the “Submit” button, the standard branch of the action method is executed. In this thread, when problems are detected during validation, the problems are displayed to the user. And in case of successful validation, the user is shown a visual representation with information about the success of registration.

Add a new user registration visual in the Views\Simple path called Registration.cshtml. Fill this visual with the following content.

Listing: Visual representation of registration Views/Simple/Registration.cshtml

@model UserWebModel
@{
    ViewBag.Title = "Простейшая гипермедийная валидация";
}

<div class="p-2 bg-light border rounded">
    <h2>@ViewBag.Title</h2>

    <partial name="Partial/RegistrationPartial" model="Model" />
</div>

In this visualization, you only need to place the partial visualization display element. And in this partial visual representation called RegistrationPartial.cshtml the elements of the user registration form will be located. Add it to the Views\Simple\Partial folder. By the way, this partial visual representation – its listing is shown below – is where all the fun is.

Listing: Partial View Views/Simple/Partial/RegistrationPartial.cshtml

@model UserWebModel

<form asp-action="Registration" method="post"
      hx-target="this"
      hx-swap="outerHTML">
    <h5>Регистрация пользователя</h5>
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>

    <div class="my-1">
        <label asp-for="Name" class="form-label"></label>
        <input asp-for="Name" class="form-control"
               hx-post hx-action="Registration"
               hx-trigger="change, keyup delay:500ms changed"/>
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>
        
    <div class="my-1">
        <label asp-for="BirthDay" class="form-label"></label>
        <input asp-for="BirthDay" class="form-control"
               hx-post hx-action="Registration"/>
        <span asp-validation-for="BirthDay" class="text-danger"></span>
    </div>
        
    <div class="form-check my-1">
        <input asp-for="IsTermsAccepted" class="form-check-input" 
               hx-post hx-action="Registration"/>
        <label asp-for="IsTermsAccepted" class="form-check-label"></label>
        <span asp-validation-for="IsTermsAccepted" class="text-danger"></span>
    </div>

    <div>
        <input class="btn btn-primary my-2" type="submit"
          value="Зарегистрироваться" />
    </div>
</form>

The

tag needs to add an unusual use of the hx-target and hx-swap attributes. The hx-target attribute points to the form itself, and the hx-swap attribute declares the form replacement itself. Together they allow you to reduce the code in form controls. In fact, form elements now only need to specify, using the hx-post and hx-action attributes, which action method the request should be sent to when the trigger fires. Only for the first form element, the name input field, it is necessary to replace the standard trigger with a new value. The value change means to send a request whenever an edit is completed. And the value keyup delay:500ms changed means to send the request after 500 milliseconds have passed after pressing the button on the keyboard.

This unusual use of the hypermedia system allows you to equip the registration form with server-side validation. In the same Views/Simple folder, you need to add a new visual representation displaying information about the successful completion of validation and registration.

Listing: Partial View Views/Simple/Complete.cshtml

@{
    ViewBag.Title = "Успешная регистрация";
}

<div class="p-2 bg-light border rounded">
    <h2>@ViewBag.Title</h2>

    <p>Валидация успешно пройдена.</p>
</div>

All that remains is to add a link to this new controller to the main menu of the experimental application. Add changes to the Views/Shared/_Layout.cshtml visual template as in the following listing.

Listing: Changes in the Views/Shared/_Layout.cshtml template

<!DOCTYPE html>
<html lang="en">
<head>
…
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light
          bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
…
                <div class="navbar-collapse collapse d-sm-inline-flex
                  justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area=""
                               asp-controller="Home" 
                               asp-action="Index">Главная</a>
                        </li>
                        @* Добавить новый блок *@
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area=""
                               asp-controller="Simple"
                               asp-action="Registration">
                                 Простейшая гипермедийная валидация</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
…
</body>
</html>

For the sake of brevity of the article, the remaining elements are not shown. Only one element is added to this template with a link to the page with the registration form. Run the application and check that the validation works.

It is enough to enter only one character in the name input field and validation problems will be displayed to the user. Only after entering the correct data will the validation messages be hidden and the “Register” button will become available.

Conclusion

In this article, based on a pilot project, we analyzed the simplest option for validating a data model using hypermedia systems using a simple example. On the server side, we used the simplest communication method – HTTP. You have learned about a new way to validate data on the server side. I think the idea of ​​”no client-side validation” is very promising. Promising in the sense that it can slightly improve productivity and make application creation easier. But only when developing web applications based on the ASP.NET Core platform and the Htmx.js library.

I will be glad if you found the material in this article useful. I would be grateful for constructive criticism and comments in the comments to this article. I wish you every success in your projects.

Application

I am attaching a link to my repository with an example application from this article.

HypermediaValidation repository on GitHub.

Similar Posts

Leave a Reply

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