Polymorphic binder: validation not working when custom model bound is wrapped in another class

Viewed 495

I have changed a little the Polymorphic binding example from this article.

  1. Add [Required] attribute to the CPUIndex and ScreenSize properties of Laptop respectively SmartPhone classes.
  2. Run example and create whatever kind of device without filling in CPU index nor Screen size.
  3. It runs correctly - model is bound and validated (shows you the error, that "CPU index / Screen size are required".

So far ok.

Now add new class:

public class DeviceWrapper
{
    public Device Device { get; set; }
}

and modify the AddDevice.cshtml.cs file:

...
[BindProperty]
public DeviceWrapper Device { get; set; } // model type changed here

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    switch (Device.Device) // added Device. prefix to respect new model structure
    {
        case Laptop laptop:
            Message = $"You added a Laptop with a CPU Index of {laptop.CPUIndex}.";
            break;

        case SmartPhone smartPhone:
            Message = $"You added a SmartPhone with a Screen Size of {smartPhone.ScreenSize}.";
            break;
    }

    return RedirectToPage("/Index");
}

Modify also the page AddDevice.cshtml to respect new model structure. I.e. prefix every Device.{prop} with Device. in all name attributes. Example:

<select id="Device_Kind" name="Device.Device.Kind" asp-items="Model.DeviceKinds" class="form-control"></select>

Now run the application. Put breakpoint into the AddDeviceModel.OnPost method. Do the same as in the first example - create device without filling neither CPU index nor Screen size. Check the value of ModelState.IsValid which is now true. Model is bound, but not validated. What should I do to apply validation also for this wrapped model.

Try it out on my fork of sample: https://github.com/zoka-cz/AspNetCore.Docs/tree/master/aspnetcore/mvc/advanced/custom-model-binding/3.0sample/PolymorphicModelBinding

2 Answers

I have found, there are more problems:

Firstly, the official example here and also in the sample source codes contains the error:

// Setting the ValidationState ensures properties on derived types are correctly 
bindingContext.ValidationState[newBindingContext.Result] = new ValidationStateEntry
{ Metadata = modelMetadata };

which should be

// Setting the ValidationState ensures properties on derived types are correctly 
bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
{ Metadata = modelMetadata };

(note the indexer - it should be the model itself. Model instance is the key, by which the ValidationState dictionary is searched).

But, it doesn't solve the original issue with no validating the wrapped model. There is another problem, which I believe is error in ASP.NET Core (already reported - see for details). The abstract Device class is not validated, because it gets skipped (for which reason?) because DeviceWrapper says that there are no validators for its sub-properties, because it doesn't take into account the metadata for real type (Laptop or SmartPhone) but only the metadata for abstract type Device, which really do not have validators.

This finding let me to the solution I believe is better. I must force the validator to validate the Device. I must say that the property of type Device has validator. This may be achieved either by implementing IValidatableObject on the abstract class Device:

public abstract class Device : IValidatableObject
{
    public string Kind { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        yield return ValidationResult.Success; // just to force validator to validate according to the correct metadata
    }
}

It may happen you cannot change the abstract class, then you may find the second way more viable:

public class ForceValidationAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        return true;
    }
}
public class DeviceWrapper
{
    [ForceValidation] // this will force the validator to work according to the correct metadata
    public Device Device { get; set; }
}

Both methods are not good, as I believe, it should be fixed in ASP.NET Core, but as workaround it seems to me more clean solution, as the produced ModelState is correct.

You can try to use TryValidateModel: cshtml.cs:

public IActionResult OnPost()
        {
            TryValidateModel(Device.Device);
            if (!ModelState.IsValid) {
                return Page();
            }

            switch (Device.Device)
            {
                case Laptop laptop:
                    Message = $"You added a Laptop with a CPU Index of {laptop.CPUIndex}.";
                    break;

                case SmartPhone smartPhone:
                    Message = $"You added a SmartPhone with a Screen Size of {smartPhone.ScreenSize}.";
                    break;
            }

            return RedirectToPage("/Index");
        }

result: enter image description here

If you still cannot work,add ModelState.Clear(); before TryValidateModel(Device.Device);

Related