Loupe

ASP.NET Core : Validation partagée avec React - partie 2

Dans une SPA (Single Page Application), il est souvent nécessaire de vérifier 2 fois la donnée :

  • Une fois côté client, pour pouvoir présenter une réponse visuelle à l'utilisateur si le format des données n'est pas bon,
  • Une fois côté serveur pour protéger le "vrai point d'entrée" du système ainsi que l'intégrité des données (en cas de modification malicieuse des scripts clients, mauvaise utilisation, etc.)

validation-partagee-aspnetcore-react-hook-form.gif
Exemple de rendu simple une fois tout le système mis en place.

Une mise en place d'exemple du système complet est disponible sur Github.

Dans l'article précédent, nous avons vu la mise en place du système côté ASP.NET Core pour retourner toutes les règles de FluentValidation mises en place.

Voyons maintenant comment récupérer ces règles puis les exploiter via react-hook-form pour obtenir un système de validation partagé entre ASP.NET Core et React.

Définition de l'usage cible du système en React

Côté React, nous allons avoir besoin d'un système qui permette de transformer les règles de FluentValidation retournées au format JSON en ValidationRules exploitables par la librairie de gestion de formulaire react-hook-form.

Pour cela, nous avons mis en place un custom hook appelé useValidation qui prend comme argument le nom du Validator que nous souhaitons récupérer (exemple : CreateUserDtoValidator), et qui retourne un Dictionary avec comme clés les noms des champs à valider (exemple: FirstName, LastName, Email, etc.) renvoyant à leur tour la liste des ValidationRules associées.

L'utilisation dans notre formulaire peut donc se faire sous la forme suivante :

...
const  { register, handleSubmit, errors }  =  useForm();
const  onSubmit  =  (data: any)  =>  console.log(data);
const { validationRules } = useValidation('CreateUserDtoValidator');

return (<div>
  <form onSubmit={handleSubmit(onSubmit)}>
    <div className="input__block">
      <label className="input__label" htmlFor="firstName">firstName:</label>
      <input id="firstName" name="firstName" ref={register(validationRules && validationRules["FirstName"])} />
      <ErrorMessage errors={errors} name={"firstName"} />
    </div>
    ...
  </form>
</div>

Pour plus de détail, voir le fichier Home.tsx

Définition du custom hook useValidation

Notre custom hook comporte 3 parties :

  1. La première partie permet d'interroger l'API ASP.NET Core pour récupérer les règles de validation FluentValidation au format JSON,
  2. La seconde partie va se charger de récupérer le nom du Validator demandé (= argument validatorName) et de trouver le JSON contenant les règles correspondantes,
  3. Enfin la dernière partie va se charger de convertir ces règles en ValidationRules compréhensibles par react-hook-form.

La partie 1 correspond au useEffect ligne 13 du fichier useValidation.ts. Son fonctionnement est très simple : Il va se charger d'initialiser le state validationData s'il ne l'est pas encore en appelant l'api api/validation via la fetch API, et en convertissant le résultant au format JSON, correspondant à un type Dictionary<Dictionary<PropertyValidatorInfo[]>>. Le type PropertyValidatorInfo correspondant au format JSON de la règle de validation FluentValidation.

useEffect(() => {
    if (!validationData) {
        (async function () {
            let response = await fetch("api/validation");
            setValidationData(await response.json());
        })();
    }
}, [validationData])

Note: Si nswag est utilisé sur le projet, ce type peut-être récupéré automatiquement depuis la génération des Model utilisés dans les endpoints ASP.NET Core, listés depuis swagger.

Note 2: Pour faire en sorte que les validations soient téléchargées une seule fois au démarrage de l'application, on pourrait rajouter également un petit coup de Context API (Merci Jérémy), mais j'ai préféré garder cet exemple simple au possible ;)

La partie 2 correspond au useEffect ligne 62 du fichier useValidation.ts. Son fonctionnement est également très simple : Il est chargé d'initialiser le state validationRules tant que le state validationRulesProcessed est égal à false (une petite optimisation très rudimentaire pour éviter que les règles soient recalculés plusieurs fois). Pour cela, il va récupérer le JSON correspondant au validatorName passé en argument au custom hook et appeller la fonction _transformValidationRules pour transformer les définitions de validations FluentValidation de chaque propriété en ValidationRules.

useEffect(() => {
    if (validationData && !validationRulesProcessed) {
        let validatorInfo = validationData[validatorName];

        if (validatorInfo) {
            var result: Dictionary<ValidationRules> = {};
            let propertiesInfo = validatorInfo as Dictionary<PropertyValidatorInfo[]>;
            Object.keys(propertiesInfo).forEach(propertyName => {
                result[propertyName] = _transformValidationRules(propertyName, propertiesInfo[propertyName]);
            });
            setValidationRules(result);
            setValidationRulesProcessed(true);
        }
    }
}, [validationData, _transformValidationRules, validationRulesProcessed, validatorName]);

La partie 3 correspond donc à cette fonction _transformValidationRules ligne 22 du fichier useValidation.ts et vient configurer l'objet ValidationRules en fonction des règles de validation définies, en faisant un switch sur la propriété name de celles-ci.

switch (rulesInfo.name) {
    case ValidationPropertyType.NotNull:
        validationRules.required = { value: true, message: rulesInfo.errorMessage?.replace("{PropertyName}", propertyName) ?? ValidationPropertyType.NotNull };
        break;
    case ValidationPropertyType.MaximumLength:
        validationRules.maxLength = rulesInfo.Max && { value: rulesInfo.Max, message: rulesInfo.errorMessage?.replace("{PropertyName}", propertyName) ?? ValidationPropertyType.MaximumLength }
        break;
    ...
    default:
        console.warn(`Not Implemented Rule name ${rulesInfo.name}`);
}

Sur l'exemple ci-dessous, les propriétés de validation mises à disposition de la librairie react-hook-form sont utilisées (required, maxLength, etc.), mais il est également très simple de créer ses propres règles de validation si besoin. Exemple :

switch (rulesInfo.name) {
	...
    case ValidationPropertyType.AspNetCoreCompatibleEmail:
        validationRules.validate = { value: value => customEmailSimpleValidation(value, rulesInfo.errorMessage?.replace("{PropertyName}", propertyName) ?? ValidationPropertyType.AspNetCoreCompatibleEmail) };
        break;
    ...
    default:
        console.warn(`Not Implemented Rule name ${rulesInfo.name}`);
}

...

const customEmailSimpleValidation = (value: string, message: string) => {
     const index = value.indexOf('@');
     if (index > 0 && index !== value.length - 1 && index === value.lastIndexOf('@')) {
         return;
     }
     return message;
 }

En conclusion

Et voilà ! C'est tout pour la mise en place de ce système de customisation partagé. J'espère que ces articles auront pu vous donner une idée de la mise en place d'un tel système. Personnellement cela fait maintenant parti des mes systèmes que je trouve indispensable pour la bonne maintenance d'un projet ASP.NET Core + React.

Dans tous les cas, n'hésitez pas à me contacter sur Twitter @vivienfabing ou en commentaire, et que le code soit avec vous !

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus