22

I am learning Blazor, and I have a WebAssembly client application.

I created a WebAPI at the server which does some additional validation over and above the standard data annotation validations. For example, as it attempts to write a record to the database it checks that no other record exists with the same email address. Certain types of validation can't reliably happen at the client, particularly where race conditions could produce a bad result.

The API controller returns a ValidationProblem result to the client, and Postman shows the body of the result as:

{
    "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."
        ]
    }
}

Note that the validation error is in the "errors" array in the JSON.

Back in the Blazor Client application, I have the typical HandleValidSubmit function that posts the data to the API and receives a response, as shown here:

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?
    }
}

My question is, how to best process server-side validation errors? The user experience ought to be the same as any other validation error, with the field highlighted, the validation message shown, and the summary at the top of the page.

1
  • 1
    I don't think you should do it like this. You should validate if an email already exist ( Http call to the server as we used to do it with Ajax), and if it does displays an error message...if not, and all your data is considered 'valid', then send your data to the data base to be stored. Commented May 7, 2020 at 16:55

4 Answers 4

24

I ended up solving this by creating a ServerValidator component. I'll post the code here in case it is helpful for others seeking a solution to the same problem.

This code assumes you are calling a Web API endpoint that returns a ValidationProblem result if there are issues.

 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; }
    }
}

To use this new component, you will need to add the component within your EditForm:

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

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

</EditForm>

Lastly, you can kick off the validation in your @code section:

@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());
        }
    }

}

In theory, this ought to allow you to bypass client validation entirely and rely on your Web API to do it. In practice, I found that Blazor performs client validation when there are annotations on your model, even if you don't include a <DataAnnotationsValidator /> in your form. However, it will still catch any validation issues at the server and return them to you.

Sign up to request clarification or add additional context in comments.

4 Comments

Perfect! Good Solution!
Excellent solution. I ran into an issue when mixing this with client side validation. The validation status was not cleared when editing the field so the form could not be submitted for a retry. Adding a handler for the OnFieldChanged event that clears the messages solves this,
I am trying this technique out but in a modal popup. We use Blazorized and it it not currently working. It seems to somehow interfere with the rendering of the popup. The overlay and modal partially popup. Any thoughts as to why this might happen?
thanks. nice answer! "Blazor performs client validation when there are annotations", because you're using OnValidSubmit instead of OnSubmit
3

how to best process server-side validation errors? The user experience ought to be the same as any other validation error, with the field highlighted, the validation message shown, and the summary at the top of the page.

I don't know what comes in your response, so I made a generic version of a component that do what you need.

  1. Get the CascadingParameter of the EditContext

    [CascadingParameter]
    public EditContext EditContext { get; set; }
    
  2. Have a ValidationMessageStore to hold the errors and a function that will display the errors

    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. Call the function of the component using it's ref

    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);
        }
    }
    

5 Comments

FYI, the controller is returning a "ValidationProblem", and the resulting JSON appears to be a ValidationProblemsDetails (learn.microsoft.com/en-us/dotnet/api/…) object.
@RogerMKE So just change the text that is in upper case with the properties of ValidationProblem and it should work as expected
@Vencovsky I am getting "no suitable method found to override" on the Dispose method. Which ComponentBase class are you using. I am using Microsoft.AspNetCoreComponents.ComponentBase version 3.1.7.0.
@Ray to have the Dispose method, you need to inherit from IDisposable interface
Thanks! I combined the answer of @RogerMKE and yours. I had the same problem as Marnix van Valen.
2

https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation has an example of how to handle server-side validation errors:

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.";
    }
}

Comments

0

Use two phase validation.

Hook up an event for when the email is entered which calls an "IsEmailUnique" method on your api. This offers your user real time validation information. Perhaps disable the "Save" button until the email has been validated on the server.

You can then handle the Bad Request as you would any other server-side errors.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.