5

I am developing a website built on EntityFrameworkCore and targeting ASP.NET Core 2.1. I want to specify an error message for an enum field in my model like so:

[Required(ErrorMessage = "Select an item from the list.")]
public MyEnum MyEnum { get; set; }

However, the stock message is still generated: The value '0' is invalid. The problem appears to be that the Enum type is validated prior to any of my code being evaluated. The two approaches presented here (https://www.codeproject.com/Articles/1204077/ASP-NET-Core-MVC-Model-Validation), either creating a class that inherits from ValidationAttribute, or having the model inherit from IValidatableObject both suffer from this.

I have found a workaround: declare the field as an int, and then use a custom validation attribute:

[EnumCheck(typeof(MyEnum), ErrorMessage = "Select an item form the list.")]
public int MyEnum { get; set; }

...and then subclass from ValidationAttribute:

sealed public class EnumCheck : ValidationAttribute
{
    readonly Type t_;

    public EnumCheck(Type t)
    {
        t_ = t;
    }

    public override bool IsValid(object value)
    {
        return Enum.IsDefined(t_, value);
    }
}

This approach has some drawbacks as now I need to cast the field to the Enum type in many places that it is used.

Is there a way to provide an ErrorMessage for Enum field types?

UPDATE

The following is a minimal example (No longer using EnumCheck subclass from ValidationAttribute, but rather the EnumDataType mentioned by @PéterCsajtai):

Model

namespace web.Models
{
    public enum Day
    {
        Sunday = 1,
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday,
        Saturday
    }

    public class Form
    {
        [EnumDataType(typeof(Day), ErrorMessage = "Select an item from the list.")]
        public Day Day { get; set; }
    }
}

Controller

namespace web.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult Save(Form model)
        {
            if(!ModelState.IsValid)
            {
                return View("Index");
            }

            return View("Index", model);
        }
    }
}

View

<form asp-controller="Home">
    <div asp-validation-summary="All" class="text-danger"></div>
    <fieldset>
        <label asp-for="@Model.Day"></label>
        <select asp-for="@Model.Day" asp-items="Html.GetEnumSelectList<Day>()">
            <option value="">Select...</option>
        </select>
        @Html.ValidationMessageFor(m => m.Day)
        <span asp-validation-for="@Model.Day" class="text-danger"></span>
    </fieldset>
    <fieldset>
        <input type="submit" asp-action="Save" />
    </fieldset>
</form>

And the output after form post:

form output

Matthew Peltzer
  • 128
  • 1
  • 10
  • Enum is an int. Maybe 0 is your problem. Have you tried to set the first enum at 1? – Manta Oct 31 '18 at 22:25
  • @Manta Yes I have. In fact, that's how I first noticed the issue. MyEnum is populated form a select box, which at first only contained each element form the enum. I then decided I waned a 'Choose One...' option at the top of the list, and set its value to 0. – Matthew Peltzer Nov 01 '18 at 18:27
  • Sort of feels like you are searching for this: https://stackoverflow.com/q/14381564/125981 – Mark Schultheiss Nov 01 '18 at 18:50

3 Answers3

3
  • In your original case, [Required(ErrorMessage = "Select an item from the list.")] you are setting the message to be shown if MyEnum is missing. But like all ValueTypes, it can never be missing so it will never trigger that validation. The solution for this is the nullable ValueType.

  • Your second effort still doesn't work because the model-binding failure – "Can a blank value be converted to a Day? No it can't." kicks in before your validation can kick in.

Validation for a Type presupposes that you have an instance of that Type to validate. The way Aspnetcore turns a form post into that typed value, is modelbinding. If the posted value can't be model-bound -- for instance if you post "boo" to a property declared as an int, or an empty string to an Enum -- then validation never even starts. Instead the modelbinding error is shown.

The simple solution is

  • Use a nullable enum, Day? so that modelbinding a blank succeeds (blank resolves to null).
  • Use [Required()] so that that null value then fails validation.

Conclusion: change your form to:

public class Form
{
    [Required(ErrorMessage = "Select an item from the list.")]
    public Day? Day { get; set; }
}

And then it will work as you'd expect.

Reference: Model Validation in AspNet Core MVC

NB unlike other ValidationAttributes, the documentation for EnumDataType, although it inherits from ValidationAttribute, doesn't give an example of using it for validation. Instead the example is of using it for MetaData.

Chris F Carroll
  • 11,146
  • 3
  • 53
  • 61
2

I think you are searching for the EnumDataTypeAttribute:

[EnumDataType(typeof(MyEnum), ErrorMessage = "Select an item form the list.")]
public MyEnum MyEnum { get; set; }
Péter Csajtai
  • 878
  • 6
  • 9
  • This is useful, it appears to do exactly what my `EnumCheck` attribute class does, so I can remove that. However, if I declare the field as `public MyEnum MyEnum { get; set;}` then I get the `The value '0' is invalid` message rather than the specified `ErrorMessage`. I still have to declare the field as an int. – Matthew Peltzer Nov 01 '18 at 18:40
  • That is weird, i got the proper validation message in my sample project, how do you make the validation exactly? – Péter Csajtai Nov 01 '18 at 19:09
  • Exactly as you have it. I've gone and made a new soultion/project as a minimal example with the same results. Perhaps I need a different method of displaying the error? I have tried both: `@Html.ValidationMessageFor(m => m.MyEnum)` and `` – Matthew Peltzer Nov 24 '18 at 05:37
  • I think this plan will never work. If the posted value can't be turned into an enum, then the ValidationAttributes are never reached. The error happens a stage earlier, in the model-binding. If the posted value _can_ be turned into an enum, then validation always passes! Either way, this ValidationAttribute can never return a fail. – Chris F Carroll Nov 24 '18 at 19:22
0

Define Your Model:

public enum Day
{
    None=0,
    Sunday = 1,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

and then define your Custome Validation Attribute:

sealed public class EnumCheck : ValidationAttribute
  {
    readonly Type t_;

    public EnumCheck(Type t)
     {
       t_ = t;
     }

   public override bool IsValid(object value)
    {

      if (((int)value)==0)
        {
            return false;

        }
      return Enum.IsDefined(t_, value);
    }
}

and finally use from this in your view:

<form asp-controller="Home">
<div asp-validation-summary="All" class="text-danger"></div>
<fieldset>
    <label asp-for="@Model.Day"></label>
    <select asp-for="@Model.Day" asp-items="Html.GetEnumSelectList<Day>()">
        <option value="0">Select...</option>
    </select>
    @Html.ValidationMessageFor(m => m.Day)
    <span asp-validation-for="@Model.Day" class="text-danger"></span>
</fieldset>
<fieldset>
    <input type="submit" asp-action="Save" />
</fieldset>