31

This is my ViewModel class:

public class CreatePersonModel
{
    public string Name { get; set; }
    public DateTime DateBirth { get; set; }
    public string Email { get; set; }
}

CreatePerson.cshtml

@model ViewModels.CreatePersonModel
@{
    ViewBag.Title = "Create Person";
}

<h2>@ViewBag.Title</h2>

@using (Html.BeginForm())
{
    <fieldset>
        <legend>RegisterModel</legend>

        @Html.EditorForModel()

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

CreatePersonValidator.cs

public class CreatePersonValidator : AbstractValidator<CreatePersonModel>
{
    public CreatePersonValidator()
    {
        RuleFor(p => p.Name)
            .NotEmpty().WithMessage("campo obrigatório")
            .Length(5, 30).WithMessage("mínimo de {0} e máximo de {1} caractéres", 5, 30)
            .Must((p, n) => n.Any(c => c == ' ')).WithMessage("deve conter nome e sobrenome");

        RuleFor(p => p.DateBirth)
            .NotEmpty().WithMessage("campo obrigatório")
            .LessThan(p => DateTime.Now).WithMessage("a data deve estar no passado");

        RuleFor(p => p.Email)
            .NotEmpty().WithMessage("campo obrigatório")
            .EmailAddress().WithMessage("email inválido")
            .OnAnyFailure(p => p.Email = "");
    }
}

When trying to create a person with an invalid date format:

Error trying to save the person

Observations

As in my CreatePersonModel class the DateBirth property is a DateTime type, the asp.net MVC validation has done for me.

But I want to customize the error message using the FluentValidation.

I do not want to change the type of property for various reasons such as:

In a CreatePersonValidator.cs class, validation is to check if the date is in the past:

.LessThan (p => DateTime.Now)

Question

How to customize the error message without using DataAnnotations (using FluentValidator).

ridermansb
  • 10,779
  • 24
  • 115
  • 226

5 Answers5

35
public CreatePersonValidator()
{
    RuleFor(courseOffering => courseOffering.StartDate)
       .Must(BeAValidDate).WithMessage("Start date is required");

    //....
}

private bool BeAValidDate(DateTime date)
{
    return !date.Equals(default(DateTime));
}
Bern
  • 7,808
  • 5
  • 37
  • 47
Dharmesh
  • 560
  • 1
  • 6
  • 16
5

Have a look at the Fluent Validation documentation on GitHub:

https://github.com/JeremySkinner/FluentValidation/wiki

Try adding a RegEx Validator to ensure that the user's input (a string) can be parsed as a date correctly, prior to applying the Less Than Validator.

EDIT

Having run few test cases and looked at the source code for Fluent Validator I concede that the above approach won't work.

The standard error you get is added during the Model Binding phase, which happens before the fluent validation framework can access and check the model.

I assumed that the framework's authors had been clever and were injecting their validation code into the model binding phase. Looks like they aren't.

So the short answer is what you want to do does not appear to be possible.

Bern
  • 7,808
  • 5
  • 37
  • 47
Stewart Ritchie
  • 926
  • 7
  • 13
  • As I said in the question, **"I do not want to change the type of property."** RegEx would apply in this case whether the type `string`! Not in a `DateTime` – ridermansb Oct 18 '11 at 14:04
  • The user's input is a string... the MVC framework needs to dynamically cast it to a DateTime in order to get it into your model class. I'm not suggesting changing the datatype in your model class. Perform a regex check on the user's input first to ensure that the cast can succeed, and respond appropriately to those cases that cannot. Then perform the less than check. – Stewart Ritchie Oct 18 '11 at 15:49
  • Ok... I reviewed the FluentValidation framework again, and you're right, it's approach is to provide extension methods based upon the model type that your trying to validate, so they're saying that regexes aren't allowed because it's a DateTime. If this framework really works this way I personally wouldn't use it! You need to be able to intercept the user input and validate it before it gets mapped into your model classes. If someone wants to point out why the design of this framework isn't flawed, I'd be interested to hear it. ;) – Stewart Ritchie Oct 18 '11 at 16:19
  • Exactly, my model is DateTime. To validate it with regex would have to convert to string. I thought of something like, manipulate DataAnnotations (I believe he is responsible for this validation but I could be wrong), so could customize the error message without changing my model. – ridermansb Oct 18 '11 at 16:57
  • As an aside, fluent validation describes itself as "A small validation library for .NET that uses a fluent interface and lambda expressions for building validation rules for your business objects." The key words here are 'business objects'. I have always preferred to keep my business objects in a separate layer, and treat model classes as a contract between controllers and views. It's extra work, but gives you good separation of concerns. – Stewart Ritchie Oct 18 '11 at 17:31
  • Yeah, I also do this! I have the model classes and my business classes. To validate the **business classes**, I use **DataAnnotations** and to validate the **model classes** (ViewModel), use the **FluentValidator**. I was even thinking in use, FluentValidator to validate my business classes. – ridermansb Oct 18 '11 at 17:53
4

Try this one

RuleFor(f =>
        f.StartDate).Cascade(CascadeMode.StopOnFirstFailure).NotEmpty()
                    .Must(date => date != default(DateTime))
                    .WithMessage("Start date is required");
Pure.Krome
  • 84,693
  • 113
  • 396
  • 647
oddmike
  • 195
  • 1
  • 5
  • Update as of Sept 2022, replace `Cascade(CascadeMode.StopOnFirstFailure)` with either of these two constructor-level property assignments `public MyValidator(){ RuleLevelCascadeMode = CascadeMode.Stop; } ` if you want the validator to stop a rule if a part of it fails (remaining rules are tested) or `public MyValidator(){ ClassLevelCascadeMode = CascadeMode.Stop; } ` if you want the whole validator to stop (no further rules are tested) – Ed HP Sep 02 '22 at 10:30
1

I got this to work with DateTime? using really simple code. If you use the built-in validator NotNull(), then you get client side validation, which has 2 benefits.

  1. You don't have to worry about all the model binding stuff people are talking about in other answers. It's client side! ;)
  2. The date the user entered does not get "wiped out". An invalid date will get set by to null by the Post, Model binding to null, Validation error, responding with the view now with a null value in the date. This happens fast, so the user does not see what is wrong with the date.

This is the code I used:

RuleFor(x => x.CompleteDate).NotNull().WithMessage("Complete Date is not a valid date.");

I tested it with a date of 11/31/2021 (There is no day 31 in November) and it worked great with client-side validation.

Win!

Jess
  • 23,901
  • 21
  • 124
  • 145
1

As Stewart mentioned, it's not possible to use FluentValidation alone to get in front of the model binding in this way. I'd offer up two ideas/suggestions though:

  1. If you really can't change the ViewModel type from DateTime to string, you could always clear the model state yourself after model binding and then run the validator manually (I'm assuming you've wired FluentValidation to execute automatically after model binding).
  2. In scenarios like this, I would change the property to a string, but then use AutoMapper to map that into a DateTime for whatever business object / domain model / service contract request I need it to ultimately become. That way, you get the most flexibility with parsing and conversion on both sides of the model binding.
Brandon Linton
  • 4,373
  • 5
  • 42
  • 63