1

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);
        }
    }
}
Josh
  • 575
  • 7
  • 17

1 Answers1

1

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.

Just the benno
  • 2,306
  • 8
  • 12
  • A more async approach with the native code can be validating on valid form submit for duplicates so you don't call the server API all the time. Just handle error responses in the service call for updating the model – rdmptn Jan 23 '21 at 20:23
  • 1
    Hi @rdmptn, thanks for your suggestion! This gives me an idea that I can just use the native ValidationMessageStore.Add(). I think this is the best answer because 1. I don't have to create a new custom attribute/db service for every validation 2. No need for calling the server every time I validate. 3. It can be async. I have updated the answer in my question. – Josh Jan 24 '21 at 04:28
  • @Wlbjtsthy, I'm not sure I understand how ValidationMessageStore.Add() can help you here... You don't have to create a new custom attribute/db service for every validation , only in case you need to validate data entered against a value in the database..."No need for calling the server every time I validate." How come ? Where would you take the data to validate against the data the user entered ? From the the ValidationMessageStore ;} I'm not sure you comprehend the whole picture... Note: Async coding is important, but... Realize that your code executes on the server, not on the browser, thus, – enet Jan 24 '21 at 07:54
  • you can cache the relevant data (say a list of phones) in a look up table against which validation would be performed. The difference in performance in such a case, and async coding, I believe, won't be noticeable. But if you can code asynchronously, do that, and please let us know how are you faring. I also want to learn new tricks. IMPORTANT: The idea of AJAX from the very beginning was about responsiveness. – enet Jan 24 '21 at 07:54
  • We were told that our app should promptly respond to data entered by the user. When the Remote validation attribute was introduced, we were told that it was created so that the app would promptly respond to data entered by the user, even if validation involves querying a database located thousands of miles away from the user's browser on which the validation is performed... Thus performing validation only when the user tries to save the data is not acceptable – enet Jan 24 '21 at 07:54
  • Hi @enet, Thanks for the response. I'm still very new to the c# world so I am probably not getting the whole picture. Getting things to work already takes quite a bit of an effort so when I see a solution which can quickly achieve what I want to, and is relatable to my current knowledge pool, I tend to think that's a good solution. I have definitely tried out the customs attribute approach, but I think it takes more effort and is harder to maintain compare to using ValidationMessageStore. Perhaps my architect is not optimized... Maybe when I learn more I will start to understand your point. – Josh Jan 24 '21 at 13:00