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.)
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 :
- La première partie permet d'interroger l'API ASP.NET Core pour récupérer les règles de validation
FluentValidation
au formatJSON
, - La seconde partie va se charger de récupérer le nom du
Validator
demandé (= argumentvalidatorName
) et de trouver leJSON
contenant les règles correspondantes, - Enfin la dernière partie va se charger de convertir ces règles en
ValidationRules
compréhensibles parreact-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 desModel
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 !
Commentaires