How to create a custom validator in ASP.NET Core that fires for invalid input too? - asp.net-core

I have created a custom validator for a DateTime field in ASP.NET Core 3.1 as shown below:
[CustomDate]
public DateTime DOB { get; set; }
public class CustomDate : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
//… some logic
}
}
However, my problem is that this custom validator fires only when I put a date value in the text box control. It does not fire for invalid inputs like e.g. when I put a string 'aaa' in the text box.
My question is how to make this custom validator fire even for invalid inputs like 'string', etc.
The reason is I want to make this custom validator replace [Required], [ReqularExpression], etc. A sort of 'One ring (validator) to rule them all'. How can I achieve that?

TL;DR: When you submit a value that can't be converted to DateTime, model binding fails. Since there is already a validation error associated with the property, subsequent validation—including your CustomDate validator—doesn't fire. Your property is still getting validated, however: If you enter a value of aaa, ModelState.IsValid will return false.
The code you had originally posted should be working fine—but I suspect it's not working the way you're expecting it to. Most notably, your confusion likely stems from the following statement:
"…this custom validator fires only when I put a date value in the text box control."
That is also true! Let me walk through the process.
Original Code
To help illustrate this, I hope you don't mind me resurrecting your original code sample, as it's useful to have concrete reference to work off of.
[CustomDate]
public DateTime DOB { get; set; }
public class CustomDate : Attribute, IModelValidator
{
public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
{
if (Convert.ToDateTime(context.Model) > DateTime.Now)
return new List<ModelValidationResult> {
new ModelValidationResult("", "Invalid - future date")
};
else if (Convert.ToDateTime(context.Model) < new DateTime(1970, 1, 1))
return new List<ModelValidationResult> {
new ModelValidationResult("", "Invalid - date is less than 1970 year")
};
else
return Enumerable.Empty<ModelValidationResult>();
}
}
Validation Process
Before I walk through the process, there are four underlying considerations that are important to be aware of here:
Model binding occurs before model validation.
Model binding will raise its own validation errors if binding fails.
Validation attributes are only evaluated on properties that remain IsValid.
The ModelValidationContext.Model property is typed to the validated property—so, in this case, a DateTime value.
Use Case #1: Invalid Value
Given these considerations, here's what's happening when you submit a value of e.g. aaa in the field mapped to your validated DOB property:
The model binder attempts to bind a value of aaa to a DateTime property.
The model binder fails, adding a ModelError to your ModelStateDictionary.
Your CustomDate validator never fires because the field has already failed validation.
Use Case #2: Missing Value
It's instructive to look at another test case. Instead of putting in aaa, just don't put a value in at all. In this case, the process looks a bit different:
The model binder doesn't find a value for your DateTime property, so no binding occurs.
Your model's property initializes to DateTime's default value of 0001-01-01 00:00:00.
Your CustomDate validator fires, adding a ModelError because "Invalid - date is less than 1970 year".
Analysis
As you can see above, it is true that your CustomDate validator isn't firing when a bogus date is submitted. But that doesn't mean that validation isn't occurring. Instead, validation has already happened and failed. If you enter a valid date—or don't enter a date at all—then a model binding error won't occur, and your CustomDate validator will be executed as expected.
Revisiting Your Question
"How to make this custom validator to fire even for invalid inputs like 'string' etc."
Ultimately, I haven't answered that question. But I think my answer will explain why that's happening, and why your input is still getting validated despite that. Keep in mind that even if your CustomDate validator did fire, it would act the same as if you hadn't submitted a value at all, since the context.Model value would have defaulted to 0001-01-01 00:00:00. The main difference is that you're not getting the same error message, since the error is coming from a different source.
Forcing Validation
I don't recommend this, but if you really wanted your CustomDate validator to fire, you could apply it to a string property instead. In that case, model binding wouldn't fail and your CustomDate validator would still get called. If you pursue this, you'll need to put in additional validation to ensure that the date is in the correct format (e.g., by preempting or handling InvalidFormatExceptions). But, of course, your date would be stored as a string, which likely isn't what you want.
Code Suggestions
This is a bit outside the scope of your original question, but while I'm here I'd recommend the following:
You won't need to do a Convert.ToDateTime() in your validator; your context.Model field is already a DateTime. You just need to cast it back to a DateTime object (e.g., (DateTime)context.Model) so your code knows that.
At minimum, you should consider using <input type="date" /> (reference) which, on most browsers, will restrict input to a correct date while also providing a basic date picker.
Alternatively, there are a number of more sophisticated date/time controls written in JavaScript that you might consider implementing if you require more control over the presentation and client-side validation.

Related

Razor Pages ModelState is different (invalid) when "nullable" types are enabled. WHY?

I have tried to copy/rewrite that code for learning purposed and realized after far to much hours:
sneakly hidden before all the using statements: #nullable disable
It was driving me insane, that in my code (opposed to example) that the ModelState was always invalid with the parameter returnUrl present in the method signature:
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
if (ModelState.IsValid)
{
//...
}
}
WHY ?
I was under the impression that, enabling nullable types, WILL NEVER break backwards-compatibility as in simply enabling/disabling it would never change how the same code works. But appareantly, the framework does some black magic and reports a different value for ModelState. This is extremely unexpected (for me). That means that actually a bool holds different value, simply by enabling/disabling nullability!
Sidenote: Since I try to work through the example, I really don't need the paramter for my implementation, and I assume, maybe, when using parameters (I assume from queries) like this, the implementaion could be made more stable, that either nulls are never generated or nulls are accaptable for the action handler ??
But I'm scared now, do I have to pay special attention to the code in various places, where the RazorPages/ASP.NET Core/Blazor framework behaves differently for nullability enabled/disabled?
What are best practices? Simply disable nullable for RazorPages like in example?
Critique about 'nullable' in Razor Pages:
https://andrewlock.net/fighting-with-nullable-reference-types-in-razor-pages/
Prior to .NET 6, new projects do not include the Nullable element. Beginning with .NET 6, new projects include the <Nullable>enable</Nullable> element in the project file.
The returnUrl property is non-nullable(Equivalent to hermit adding a [Required] property) by default, So if you don't bind any value to this property, ModelState.IsValid will return false.
If you don't want this, you can disable this by deleting the below line from the csproj file or setting it as disable. By default(.Net 6) value is enable.
<Nullable>enable</Nullable>
Or, you can just add ? to make it nullable:
(string? returnUrl = null)
Or, make ModelState ignore this property:
if (ModelState.Remove("returnUrl"))
{
*save form, go somewhere*
}

Why is my complex [FromBody] parameter null?

I am having trouble with the [FromBody] parameter of my method not binding.
example C#:
[Route("api/path")]
[HttpPost]
public void Post([FromBody] ComplexType param)
{
// param is null
}
public class ComplexType
{
public string name { get; set;}
}
I've checked the POST body content and content-type and it looks correct.
Why is it null despite throughly checking that the data being posted and content type all match what is expected?
N.B. This is a deliberatly vague question since I was having a lot of trouble diagnosing an issue and I couldn't find a suitable question and answer.
When I eventually found the problem I kicked myself for it, but I feel the need to share how I found the problem to hopefully spare others the pain.
As it happens there may well be nothing wrong with the example given.
In my case there was a problem with the definition of the complex type, I had a parameter marked as string while it should have been string[] and so the JSON parsed did not match the model.
The important part though is how I found this out:
When debugging any API method there is the magic ModelState property.
This property gives you information about any failures that occur while binding the data received to the expected parameters.
e.g:
here we can see the parameter (uploaded), and the property within that parameter which failed to bind correctly.
Check the definition of that property and you'll probably find an error.

Web API Model Validation - Error Message is empty

I'm using standard model validation attributes (e.g. Required) as a first line of defense against invalid model, later on I have additional checks that can not be done by using standard model validation attributes.
I have one ActionFilterAttribute where I check for model state, null model properties and so on..
What is strange here is the following
in method OnActionExecuting we have HttpActionContext and there is ModelState property which has property IsValid indicating if model is valid or not (excluding cases when property of model is null, but that I cover later on...).
So, when the model is not valid, than actionContext.ModelState.First().Value.Errors (collection of ModelError) contains all model validation errors. So, ModelError contains ErrorMessage and Exception attributes, and when I check for each of them, ErrorMessage is not initialized but Exception property is initialized and contains details of validation failure. Further more, ModelError class has 3 constructors:
public ModelError(Exception exception);
public ModelError(string errorMessage);
public ModelError(Exception exception, string errorMessage);
So it seems that for some reason the first one is called by framework, instead of the last one.
Any ideas why this happens? What can I do in order to get ErrorMessage Initialized?
Regards,
Novak

Initialize a Struts 1 ActionForm with Data from HttpSession Object

I've done this a half dozen times so I know it's possible. I just can't remember how.
I would like to initialize a property of a Struts 1 ActionForm with data from the user's HttpSession object, but only when the form is first created. Actually don't worry too much about the fact that it comes from HttpSession, important is just the fact that the data is dynamic, per-user, and should only be initialized once.
Additionally, if the user changes the data in this field, the user's entry should persist. In other words, when the user first sees the form they will see the initialized data. If they then change the field and submit the form (by calling the associated action) and subsequently come back to this form later, they should see THEIR entry in that field.
Obviously initializing the field in struts-config.xml won't work because the data is dynamic and per-user. Same can be said for the form's constructor. I see the reset() method of ActionForm will be called to reset properties to a default state, but I don't remember if it is called before the first time the form is loaded and displayed in the page. I suppose if it is that's an option, but I would only want it to do the initialization on the first call. That sounds just mildly complicated (I would need a boFirstTime member variable flag or something?).
Can anyone help?
What I ended up doing was overriding reset() of ActionForm, and setting the desired property only if it is null or blank. The property I needed to initialize is represented in the class member variable _strMailTo (yeah I know nobody but me uses the underscores for member variables anymore).
It turns out that reset() is also called before the ActionForm properties are used for the first time to populate the fields of the form for the associated Action. In this way the first time the user sees the form the my pre-populated data is there. But if they change it and later land on the form again they see the text they put in the field the last time they submitted the form.
I guess maybe I'm also the only one still using Struts 1 anymore...
public void reset(ActionMapping mapping, HttpServletRequest request) {
if (_strMailTo == null || _strMailTo.equals("")) {
String strRemoteUser = request.getRemoteUser();
_strMailTo = chOps.UtilityUsers.getEmail(strRemoteUser);
}
}

Struts 1 ActionForms - What's the convention for dates in the form?

I am about to start with e project that uses Struts 1.2. There is no plan to move to another framework.
I wanted to know what's the convention when handling a date in the form?
Should I create a Date variable and create a setDate(String date) method where the conversion will occur?
Create a Date variable, a setDate(Date date) and register a special converter somewhere in the chain? (don't know if it's possible)
Create a String variable, a setDate(String date) and let the conversion/validation to the validate method in the ActionForm bean?
Or another approach?
Also, if you have any advice to get up to speed with this framework, I would really appreciate it.
Before I respond to your question, let me start by saying this: People don't understand what ActionForm is or what ActionForm does
The ActionForm represents the data that the user filled in the HTML form. Struts reads in the request parameters and matches them by name with the configured ActionForm for the target Action of the submit. It is data inputted by the user, plain and simple.
Data that comes on the request is always of type java.lang.String. But people might have a form field named "age" which is an int in their Model data. Or maybe instead of "age" they have a "birthDate" which off course is java.util.Date in their Model data. So they think it is a good idea to have the type int and Date placed on the ActionForm and allow Struts to convert the Strings that arrive on request into ints and Dates.
Its a very useful conversion and you as a developer don't have to handle it, Struts does. It's a little bit of framework magic.
But this ain't freaking Harry Potter! Conversion can fail on ints and Dates. Why?
int is a primitive type and as such it must always have a value. Default initialization is with zero. When doing the binding (request parameters to ActionForm object properties) Struts sees an int type on the ActionForm and tries to convert the request String value to int.
If user inserted String "5", the field is set to 5. That's fine!
But what if user inserted "bla"? Will we get an exception thrown in our face? Nope! We get back a value of zero because conversion (silently) failed. Ups!
Dates again are an issue. Why? Because their value arrives on request as String. Their format might be something simple like "12/01/2011" which is completely useless as information. Why? Because Dates represented as Strings must go hand in hand with a Locale in order to be transformed to the correct Date instance it represents.
"12/01/2011" + Locale.US = 01 December 2011
"12/01/2011" + Locale.FRENCH = 12 January 2011
Ups!
Ok, so this was the intro! Now let's go to your question.
Should I create a Date variable and create a setDate(String date) method where the conversion will occur.
At some point you will have to send the Date back to the view and the Struts html tags will normally have to go for a getDate() which returns String. The "12/01/2011" you get on user input might be displayed as "Jan. 12, 2011 00:00:00" if the getter returns Date (Struts will do a toString() on the getter value). So you actually need the Date field with both a setter/getter of type Date and a setter/getter of type String. Use type Date in your Action class and use String for interfacing with the view tags.
Question? How do you get handle on the proper Locale value in your ActionForm?
Create a Date variable, a setDate(Date date) and register a special converter somewhere in the chain (don't know if it's possible)
It is possible. You can create and register a custom converter that might take String representations of dates and convert them to Date. If you use the ISO 8601 format I think you will be safe.
Question? Can you accommodate this into the existing application without breaking code that transforms String to dates in their own way by using all sorts of formats or Locales?
Create a String variable, a setDate(String date) and let the conversion/validation to the validate method in the actionForm bean
This won't work. The validate method is called after the parameter bindings on the ActionForm object. When you reach this point is already to late. Struts did the conversion. If you have a field of type int with value zero there is no way to know if the user actually inserted zero and conversion worked or if the user inserted "bla" and the conversion failed and you got back zero as default initialization value. If zero is a valid value for your application than you are in even bigger trouble.
So what's the convention?
There is no convention. Use the above information and apply common sense given your situation.
Ideally you should have all properties in the ActionForm as Strings so that you loose no information during the binding. But this involves manually converting the properties to the proper type in the Action class, before using them. You have full control (Struts no longer does conversions since source and destination values are of type String) but you also have a lot of boiler plate code to write to do it the proper way which at some point can become annoying.
P.S. Before I end this and go to bed (it's 01:00 AM in my Country :D) I just want to mention one thing that people often don't see. The ActionForm is not part of the model, neither should it interact directly with the model.
If you need the data from the ActionForm to be processed in the model, then map it as a one-to-one relationships with a Model DTO (data transfer object). If you don't, then you create a tight coupling between the Model and the Struts framework because your ActionForm objects are not a POJOs. Your class must extend the ActionForm class from Struts that we've been talking about. People don't do this and send the ActionForm straight to the Model. What is even worse is that they also use the validate method to perform business validations instead of basic validation such as "is required", "is value withing range" etc.
ActionForms are just a communication path between the Action (controller) and the view. Treat it as such.