6
votes

Gestion des erreurs de validation d'API dans Blazor WebAssembly

J'apprends Blazor et j'ai une application client WebAssembly.

J'ai créé une WebAPI sur le serveur qui effectue une validation supplémentaire en plus des validations d'annotation de données standard. Par exemple, lorsqu'il tente d'écrire un enregistrement dans la base de données, il vérifie qu'aucun autre enregistrement n'existe avec la même adresse électronique. Certains types de validation ne peuvent pas se produire de manière fiable chez le client, en particulier lorsque des conditions de concurrence peuvent produire un mauvais résultat.

Le contrôleur d'API renvoie un résultat ValidationProblem au client et Postman affiche le corps du résultat comme suit:

private async void HandleValidSubmit()
{
    var response = await Http.PostAsJsonAsync<TestModel>("api/Test", testModel);

    if (response.StatusCode != System.Net.HttpStatusCode.Created)
    {
        // How to handle server-side validation errors?
    }
}

Notez que l'erreur de validation se trouve dans le tableau "errors" du JSON.

De retour dans l'application Blazor Client, j'ai la fonction HandleValidSubmit typique qui publie les données sur l'API et reçoit une réponse, comme indiqué ici:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "|f06d4ffe-4aa836b5b3f4c9ae.",
    "errors": {
        "Email": [
            "The email address already exists."
        ]
    }
}

Ma question est la suivante: comment traiter au mieux les erreurs de validation côté serveur? L'expérience utilisateur doit être la même que toute autre erreur de validation, avec le champ en surbrillance, le message de validation affiché et le résumé en haut de la page.


1 commentaires

Je ne pense pas que tu devrais le faire comme ça. Vous devez valider si un email existe déjà (appel Http au serveur comme nous le faisions avec Ajax), et s'il affiche un message d'erreur ... sinon, et toutes vos données sont considérées comme `` valides '', alors envoyez vos données à la base de données à stocker.


4 Réponses :


1
votes

comment traiter au mieux les erreurs de validation côté serveur? L'expérience utilisateur doit être la même que toute autre erreur de validation, avec le champ en surbrillance, le message de validation affiché et le résumé en haut de la page.

Je ne sais pas ce qui vient dans votre response , j'ai donc créé une version générique d'un composant qui fait ce dont vous avez besoin.

  1. Obtenez le CascadingParameter du EditContext

    private async void HandleValidSubmit()
    {
        var response = await Http.PostAsJsonAsync<TestModel>("api/Test", testModel);
    
        if (response.StatusCode != System.Net.HttpStatusCode.Created)
        {
            // How to handle server-side validation errors?
    
            // You could also have a foreach or a function that receives an List for multiple fields error display
            MyHandleErrorComponent.AddFieldError(response.ERROR_PROPERTY);
        }
    }
    
  2. Avoir un ValidationMessageStore pour contenir les erreurs et une fonction qui affichera les erreurs

    private ValidationMessageStore _messageStore;
    
    private EventHandler<ValidationRequestedEventArgs> OnValidationRequested => (s, e) =>
        {
            _messageStore.Clear();
        };
    private EventHandler<FieldChangedEventArgs> OnFieldChanged => (s, e) =>
        {
            _messageStore.Clear(e.FieldIdentifier);
        };
    
    protected override void OnInitialized()
    {
        base.OnInitialized();
    
        if (EditContext != null)
        {
            _messageStore = new ValidationMessageStore(EditContext);
            EditContext.OnFieldChanged += OnFieldChanged;
            EditContext.OnValidationRequested += OnValidationRequested;
        }
    }
    
    public override void Dispose()
    {
        base.Dispose();
    
        if (EditContext != null)
        {
            EditContext.OnFieldChanged -= OnFieldChanged;
            EditContext.OnValidationRequested -= OnValidationRequested;
        }
    }
    
    private void AddFieldError(ERROR_CLASS_YOU_ARE_USING validatorError)
    {
        _messageStore.Add(EditContext.Field(validatorError.FIELD_NAME), validatorError.ERROR_MESSAGE);
    }
    
  3. Appelez la fonction du composant en utilisant sa référence

    [CascadingParameter]
    public EditContext EditContext { get; set; }
    

4 commentaires

Pour info, le contrôleur renvoie un "ValidationProblem" et le JSON résultant semble être un objet ValidationProblemsDetails ( docs.microsoft.com/en-us/dotnet/api/… ).


@RogerMKE Il suffit donc de changer le texte en majuscules avec les propriétés de ValidationProblem et cela devrait fonctionner comme prévu


@Vencovsky Je n'obtiens "aucune méthode appropriée trouvée pour remplacer" sur la méthode Dispose. Quelle classe ComponentBase utilisez-vous. J'utilise Microsoft.AspNetCoreComponents.ComponentBase version 3.1.7.0.


@Ray pour avoir la méthode Dispose, vous devez hériter de l'interface IDisposable



4
votes

J'ai fini par résoudre ce problème en créant un composant ServerValidator. Je publierai le code ici au cas où cela serait utile pour d'autres qui cherchent une solution au même problème.

Ce code suppose que vous appelez un point de terminaison d'API Web qui renvoie un résultat ValidationProblem en cas de problèmes.

@code {
    private TestModel testModel = new TestModel();
    private ServerValidator serverValidator;

    private async void HandleValidSubmit()
    {
        var response = await Http.PostAsJsonAsync<TestModel>("api/TestModels", testModel);

        if (response.StatusCode != System.Net.HttpStatusCode.Created)
        {
            serverValidator.Validate(response, testModel);
        }
        else
        {
            Navigation.NavigateTo(response.Headers.Location.ToString());
        }
    }

}

Pour utiliser ce nouveau composant, vous devrez ajouter le composant dans votre EditForm:

<EditForm Model="agency" OnValidSubmit="HandleValidSubmit">
        <ServerValidator @ref="serverValidator" />
        <ValidationSummary />

        ... put all your form fields here ...

</EditForm>

Enfin, vous pouvez lancer la validation dans votre section @code :

 public class ServerValidator : ComponentBase
 {
    [CascadingParameter]
    EditContext CurrentEditContext { get; set; }

    protected override void OnInitialized()
    {
        base.OnInitialized();

        if (this.CurrentEditContext == null)
        {
            throw new InvalidOperationException($"{nameof(ServerValidator)} requires a cascading " +
                $"parameter of type {nameof(EditContext)}. For example, you can use {nameof(ServerValidator)} " +
                $"inside an EditForm.");
        }
    }

    public async void Validate(HttpResponseMessage response, object model)
    {
        var messages = new ValidationMessageStore(this.CurrentEditContext);

        if (response.StatusCode == HttpStatusCode.BadRequest)
        {
            var body = await response.Content.ReadAsStringAsync();
            var validationProblemDetails = JsonSerializer.Deserialize<ValidationProblemDetails>(body);

            if (validationProblemDetails.Errors != null)
            {
                messages.Clear();

                foreach (var error in validationProblemDetails.Errors)
                {
                    var fieldIdentifier = new FieldIdentifier(model, error.Key);
                    messages.Add(fieldIdentifier, error.Value);
                }
            }
        }

        CurrentEditContext.NotifyValidationStateChanged();
    }

    // This is to hold the response details when the controller returns a ValidationProblem result.
    private class ValidationProblemDetails
    {
        [JsonPropertyName("status")]
        public int? Status { get; set; }

        [JsonPropertyName("title")]
        public string Title { get; set; }

        [JsonPropertyName("type")]
        public string Type { get; set; }

        [JsonPropertyName("errors")]
        public IDictionary<string, string[]> Errors { get; set; }
    }
}

En théorie, cela devrait vous permettre de contourner complètement la validation client et de vous fier à votre API Web pour le faire. En pratique, j'ai trouvé que Blazor effectue la validation du client lorsqu'il y a des annotations sur votre modèle, même si vous n'incluez pas de <DataAnnotationsValidator /> dans votre formulaire. Cependant, il détectera toujours les problèmes de validation sur le serveur et vous les renverra.


0 commentaires

0
votes

Utilisez la validation en deux phases.

Connectez un événement lorsque l'e-mail est entré qui appelle une méthode "IsEmailUnique" sur votre api. Cela offre à votre utilisateur des informations de validation en temps réel. Peut-être désactiver le bouton "Enregistrer" jusqu'à ce que l'e-mail soit validé sur le serveur.

Vous pouvez ensuite gérer la demande incorrecte comme vous le feriez pour toute autre erreur côté serveur.


0 commentaires

0
votes

https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation a un exemple de la façon de gérer les erreurs de validation côté serveur:

private async Task HandleValidSubmit(EditContext editContext)
{
    customValidator.ClearErrors();

    try
    {
        var response = await Http.PostAsJsonAsync<Starship>(
            "StarshipValidation", (Starship)editContext.Model);

        var errors = await response.Content
            .ReadFromJsonAsync<Dictionary<string, List<string>>>();

        if (response.StatusCode == HttpStatusCode.BadRequest && 
            errors.Count() > 0)
        {
            customValidator.DisplayErrors(errors);
        }
        else if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException(
                $"Validation failed. Status Code: {response.StatusCode}");
        }
        else
        {
            disabled = true;
            messageStyles = "color:green";
            message = "The form has been processed.";
        }
    }
    catch (AccessTokenNotAvailableException ex)
    {
        ex.Redirect();
    }
    catch (Exception ex)
    {
        Logger.LogError("Form processing error: {Message}", ex.Message);
        disabled = true;
        messageStyles = "color:red";
        message = "There was an error processing the form.";
    }
}


0 commentaires