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.

4
  • Review the following from docs learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/…
    – Nkosi
    Commented Jun 5, 2019 at 13:44
  • Which show how to put validation messages for individual fields. for example <span asp-validation-for="FutureDate" class="text-danger"></span>
    – Nkosi
    Commented Jun 5, 2019 at 13:45
  • 2
    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 detail
    – ADyson
    Commented Jun 5, 2019 at 13:46
  • @ADyson 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.
    – Mkalafut
    Commented Jun 5, 2019 at 13:52

1 Answer 1

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.

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.
    – Mkalafut
    Commented Jun 5, 2019 at 14:09
  • 1
    @Mkalafut 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.
    – ADyson
    Commented Jun 5, 2019 at 14:11
  • 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?)
    – Mkalafut
    Commented Jun 5, 2019 at 14:36
  • 1
    "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.
    – ADyson
    Commented Jun 5, 2019 at 14:39
  • 1
    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 :-)
    – ADyson
    Commented Jun 5, 2019 at 14:42

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.