Search code examples
validationblazorblazor-server-sideremote-validation

Custom remote validations for complex models in blazor?


I am currently using <ObjectGraphDataAnnotationsValidator/> to validate complex models. So far so good, except that there is also a requirement to check against the database to see if a record with the same value already exists.

I have tried implementing the <CustomValidator/> as per advised in https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-5.0#validator-components

However, it seems to only work for the top level properties.

And the <ObjectGraphDataAnnotationsValidator/> does not work with remote validations (or does it!?)

So say that I have:

*Parent.cs*
public int ID {get;set;}
public List<Child> Children {get;set;}

*Child.cs*
public int ID {get;set;}
public int ParentID {get;set}
public string Code {get;set;}

<EditForm Model="@Parent">
.
.
.

Child.Code has a unique constraint in the database.

I want to warn users "This 'Code' already exists! Please try entering a different value.", so that no exceptions will be thrown.

For now, I am a bit lost as to where my next step is.

In the past with asp.net core mvc, I could achieve this using remote validations.

Is there an equivalent to remote validations in blazor?

If not, what should I do to achieve the same result, to remotely validate the sub properties for complex models?

Any advises would be appreciated. Thanks!


[Updated after @rdmptn's suggestion 2021/01/24]

ValidationMessageStore.Add() accepts the struct FieldIdentifier, meaning that I can simply add a overload of the CustomValidator.DisplayErrors to make it work:

        public void DisplayErrors(Dictionary<FieldIdentifier, List<string>> errors)
        {
            foreach (var err in errors)
            {
                messageStore.Add(err.Key, err.Value);
            }

            CurrentEditContext.NotifyValidationStateChanged();
        }

Full example below:


@using Microsoft.AspNetCore.Components.Forms
@using System.ComponentModel.DataAnnotations
@using System.Collections.Generic


<EditForm Model="parent" OnSubmit="Submit">
    <ObjectGraphDataAnnotationsValidator></ObjectGraphDataAnnotationsValidator>
    <CustomValidator @ref="customValidator"></CustomValidator>
    <ValidationSummary></ValidationSummary>
    @if (parent.Children != null)
    {
        @foreach (var item in parent.Children)
        {
            <div class="form-group">
                <label>Summary</label>
                <InputText @bind-Value="item.Code" class="form-control"></InputText>
            </div>
        }
    }
    <input type="submit" value="Submit" class="form-control"/>
</EditForm>

@code{
    private CustomValidator customValidator;
    private Parent parent;

    public class Parent
    {
        public int Id { get; set; }
        [ValidateComplexType]
        public List<Child> Children { get; set; }
    }

    public class Child
    {
        public int Id { get; set; }
        public int ParentId { get; set; }
        public string Code { get; set; }
    }

    protected override void OnInitialized()
    {
        parent = new Parent()
        {
            Id = 1,
            Children = new List<Child>()
            {
                new Child()
                {
                    Id = 1,
                    ParentId = 1,
                    Code = "A"
                },
                new Child()
                {
                    Id = 1,
                    ParentId = 1,
                    Code = "B"
                }
            }
        };
    }

    public void Submit()
    {
        customValidator.ClearErrors();

        var errors = new Dictionary<FieldIdentifier, List<string>>();

        //In real operations, set this when you get data from your db
        List<string> existingCodes = new List<string>()
        {
            "A"
        };

        foreach (var child in parent.Children)
        {
            if (existingCodes.Contains(child.Code))
            {
                FieldIdentifier fid = new FieldIdentifier(model: child, fieldName: nameof(Child.Code));
                List<string> msgs = new List<string>() { "This code already exists." };
                errors.Add(fid, msgs);
            }
        }

        if (errors.Count() > 0)
        {
            customValidator.DisplayErrors(errors);
        }
    }
}

Solution

  • The [Remote] validation attribute is tied to MVC and is not usable for Blazor.

    ObjectGraphDataAnnotationsValidator is not enough. In addition, each property, that represents an object with possible validation needs to be decorated with a [ValidateComplexType] attribute.

    In your CustomValidatior, you can see DI to get your API service to call your API and validate your constraint.

    public class Parent
    {
       ...other properties...
    
       [ValidateComplexType]
       public List<Child> Children {get; set; }
    }
    
    public class Child
    {
         ...other properties...
    
        [Required]
        [IsUnique(ErrorMessage = "This 'Code' already exists! Please try entering a different value.")]
        public String Code {get; set;}
    
    }
    
    
    public class IsUniqueAttribute : ValidationAttribute
    {
    
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var service = (IYourApiService)validationContext.GetService(typeof(IYourApiService));
    
            //unfortunately, no await is possible inside the validation
    
            Boolean exists = service.IsUnique((String)value);
            if(exists == false)
            {
                return ValidationResult.Success;
            }
    
            return new ValidationResult(ErrorMessage, new[] { validationContext.MemberName });
        }
    }
    

    You might want to check out FluentValidation as this library provide features for asynchronous validation. I'm not sure if this validator can be used inside Blazor WASM.