2

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

Zoka
  • 2,312
  • 4
  • 23
  • 33
  • Are you sure you're making a post request with those properties included? – johnny 5 Nov 16 '20 at 14:54
  • Yes. As is written - model **is bound** (if I enter the value e.g. for CPU index, it get to the model). It is **not validated** (so if I leave it empty I do not get warning that the CPU index is required). – Zoka Nov 16 '20 at 15:00

2 Answers2

5

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.

Zoka
  • 2,312
  • 4
  • 23
  • 33
1

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

Yiyi You
  • 16,875
  • 1
  • 10
  • 22
  • Thanks for effort, but... your solution does exactly the same I have described, but more complicated (I believe I must write binder only for something, the system cannot bind by itself, which is abstract `Device` and not `DeviceWrapper`. `DeviceWrapper` is usual class, which system knows how to bind). As I written - my solution **binds the model correctly**, but **does not perform validation** on `CPUIndex/ScreenSize` properties of `Laptop/SmartPhone` classes (for example `[Required]` attribute). Your example has the same issue - it does not validate :-(. – Zoka Nov 17 '20 at 13:48
  • I have updated my answer,you can use `TryValidateModel`. – Yiyi You Nov 18 '20 at 07:16
  • Now it work somehow, but, it is not still perfect. Check the ModelState results - now you have the CPU index validated twice with different results, but yes, it gets validated. Meanwhile I have find the solution I consider more right, although I believe it is an error in ASP.NET Core (reported). I will add my own answer and keep both answers not-accepted, so anyone may decide the right solution for him. Thank you for your time and help. – Zoka Nov 19 '20 at 08:50