How to display specific ModelState errors in ASP.NET Core MVC?
Asked Answered
I

1

5

In one of my controller actions, the first thing I do is pass the model to a new action that essentially just parses input to determine whether or not the user entered a valid date. The model is then returned and ModelState.IsValid is inspected.

public Import ValidateUploadModel(Import Model)
    {
        // Do not allow future dates
        if (Model.CurrMoInfo.CurrMo > DateTime.Now)
        {
            ModelState.AddModelError("FutureDate", "You cannot assign a future date.");
        }
        // Do not allow dates from the same month (we run the processing a month behind)
        if (Model.CurrMoInfo.CurrMo.Month == DateTime.Now.Month)
        {
            ModelState.AddModelError("SameMonth", "You must process a previous month.");
        }

        // Ensure day is last day of a previous month
        if (Model.CurrMoInfo.CurrMo.Day != DateTime.DaysInMonth(Model.CurrMoInfo.CurrMo.Year, Model.CurrMoInfo.CurrMo.Month))
        {
            ModelState.AddModelError("LastDay", "You must enter the last day of the month.");
        }

        // Do not allow dates older than 12 months back
        if (Model.CurrMoInfo.CurrMo < DateTime.Now.AddMonths(-12))
        {
            ModelState.AddModelError("TooOld", "Date must not be older than a year.");
        }

        return Model;
    }

At the point where I know I have model state errors, I am able to correctly show them in my razor view by putting the following

<span class="text-danger">@Html.ValidationSummary(false)</span>

So, since all of my model state errors are for the same input on the page I can safely do this. But what if I have various inputs with various errors that I need to display independently of one another? How would I go about doing this? Additionally, is there a better (or more appropriate) way to do this aside from using @Html.ValidationSummary?

I have searched through Microsoft docs and a few dozen StackOverflow questions to try and translate older answers into the .Net Core way of doing things with no luck.

Edit for clarity:

Here is the entire card in the razor view:

<div class="card-body">

                @if (Model.StagingCount == 0)
                {
                    <input asp-for="@Model.CurrMoInfo.CurrMo" type="date" required class="col-lg-12" />
                }
                else
                {
                    <input asp-for="@Model.CurrMoInfo.CurrMo" type="date" disabled="disabled" required class="col-lg-12" />
                }

                <span class="text-danger">@Html.ValidationSummary(false)</span>

            </div>

The input is for a model property however it is not annotated. I've written my own rules and manually add errors to the model state if the rules are not adhered to. The code I have works however it's not scalable when I start needing to validate more fields. I just want to know what a better way of doing this is.

India answered 5/6, 2019 at 13:42 Comment(4)
Review the following from docs learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/…Reeder
Which show how to put validation messages for individual fields. for example <span asp-validation-for="FutureDate" class="text-danger"></span>Reeder
If you know in advance what model fields you want to display (i.e. you're not defining arbitrary or dynamically-generated keys in the AddModelError method) then you can use controls with asp-validation-for tag helpers on them. These will act as a place to display a specific error message when it's generated. see learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/… for more detailTomtom
@Tomtom In this case the model is not annotated and I am just creating my own errors in a method in my controller. the asp-validation-for will not work here since the errors are specifically being added to the ModelState.India
T
7

In the example above, you are not really following standard practice.

For simple validations like this (where you're only validating the value in one field) the key you use to place the error message against in the ModelState is supposed to be the same as the name of the affected field in the model. In your case, really all your errors should be logged against the CurrMoInfo.CurrMo key. Only the error message itself needs to differ. Using a custom key for each specific different error doesn't add any value to your application as far as I can tell. You're not using it the way it was intended.

If you log them all against CurrMoInfo.CurrMo then you can use an asp-validation-for tag helper to create a field which displays errors specifically for that field, e.g.

<span asp-validation-for="CurrMoInfo.CurrMo" class="text-danger"></span>

You can then (optionally) use a ValidationSummary to (as the title suggests) summarise all the errors for the whole model - and to display any extra model errors you may have created which don't relate to a single specific field.

Complete example:

public Import ValidateUploadModel(Import Model)
{
    // DO not allow future dates
    if (Model.CurrMoInfo.CurrMo > DateTime.Now)
    {
        ModelState.AddModelError("CurrMoInfo.CurrMo", "You cannot assign a future date.");
    }
    //Do not allow dates from the same month (we run the processing a month behind)
    if (Model.CurrMoInfo.CurrMo.Month == DateTime.Now.Month)
    {
        ModelState.AddModelError("CurrMoInfo.CurrMo", "You must process a previous month.");
    }

    //Ensure day is last day of a previous month
    if (Model.CurrMoInfo.CurrMo.Day != DateTime.DaysInMonth(Model.CurrMoInfo.CurrMo.Year, Model.CurrMoInfo.CurrMo.Month))
    {
        ModelState.AddModelError("CurrMoInfo.CurrMo", "You must enter the last day of the month.");
    }

    //Do not allow dates older than 12 months back
    if (Model.CurrMoInfo.CurrMo < DateTime.Now.AddMonths(-12))
    {
        ModelState.AddModelError("CurrMoInfo.CurrMo", "Date must not be older than a year.");
    }

    return Model;
}


<div class="card-body">
    @if (Model.StagingCount == 0)
    {
        <input asp-for="CurrMoInfo.CurrMo" type="date" required class="col-lg-12" />
    }
    else
    {
        <input asp-for="CurrMoInfo.CurrMo" type="date" disabled="disabled" required class="col-lg-12" />
    }
    <span asp-validation-for="CurrMoInfo.CurrMo" class="text-danger"></span>
</div>

Further reading: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/validation?view=aspnetcore-2.2

P.S. I don't think this general principle has changed from .NET Framework to .NET Core.

Tomtom answered 5/6, 2019 at 14:5 Comment(8)
Thanks. With this answer and your comments you've given me a lot of good information and corrected some misconceptions I had with how this is supposed to work. This is a great solution.India
@India No problem. But I'd be more modest, it's not so much a "great" solution as simply the "standard" solution :-). Anyway glad it helped you grok the concepts.Tomtom
I've gone and changed all of my ModelState.AddModelError calls to now all have the same key CurrMoInfo.CurrMo and it's correctly working as described except for that if two errors are added, only one is displayed. Also, is this an acceptable approach or should I find a way to add these as annotations on my model properties? (If so, how would I make custom annotations like this rather than doing it in a controller action?)India
Sensible questions. I'll do my best to answer in a brief comment. "The key in this case should be whatever model property is currently being validated, correct?". Yes. That's what I mean by the "standard" solution. The exception might be where the validation rule is based on evaluating multiple fields together (e.g. maybe checking that 3 separate values always equal a specific total or something). In that case you might add a separate key. Or you could just arbitrarily attach it to the first property for convenience.Tomtom
Poor timing on my part but I actually replaced that comment to make it a bit more clear about what I was asking.India
"should I find a way to add these as annotations on my model properties". It's not essential, no. Your approach is basically ok. If you have a rule (such as comparing two dates) which could potentially be applied to other fields then you could consider writing your own custom attribute to make it re-usable. Or, there are NugetPackages (such as FluentValidation, FoolproofValidation, ExpressiveAnnotations and others) which aim to make it possible to write much more powerful expressions in your annotation rules than is possible by default. You could maybe look into those.Tomtom
Seen your update. So... "if two errors are added, only one is displayed"...yes I think unfortunately that is the case. If you use annotations then I'm fairly sure you can get multiple errors displayed simultaneously - certainly for client-side validation at least (since you've only got server-side validation currently, that doesn't apply). A quick google finds people who have re-written the tag helper code (at least in .NET Framework, not sure about .NET Core) to make it display multiple (server-side) errors at once. It's a silly limitation IMHO, but there you go, I didn't write it :-)Tomtom
I will mark this as the accepted answer after allowing others some time to respond just to see potential alternate approaches. Appreciate the clarification!India

© 2022 - 2024 — McMap. All rights reserved.