DataAnnotation and optional DateTime values - c#

I'm working with a HTML form that accepts 4 dates, two of which are optional. These dates are inserted into a MS SQL database, so I'm boundary checking the DateTime variables, which are passed from the form, against SqlDateTime.MinValue and SqlDateTime.MaxValue. Here's what my model looks like:
[Required]
[DisplayName("Planned Start Date")]
[CustomValidation(typeof(Goal), "ValidateGoalDate")]
public object planned_start_date { get; set; }
[DisplayName("Actual Start Date")]
[CustomValidation(typeof(Goal), "ValidateGoalDate")]
public object start_date { get; set; }
[Required]
[DisplayName("Planned End Date")]
[CustomValidation(typeof(Goal), "ValidateGoalDate")]
public object planned_end_date { get; set; }
[DisplayName("Actual Start Date")]
//[CustomValidation(typeof(Goal), "ValidateGoalDate")]
public object end_date { get; set; }
And my custom validator:
public static ValidationResult ValidateGoalDate(DateTime goalDate) {
//* this does not appear to work ever because the optional field does
//* not ever get validated.
if (goalDate == null || goalDate.Date == null)
return ValidationResult.Success;
if (goalDate.Date < (DateTime)SqlDateTime.MinValue)
return new ValidationResult("Date must be after " + SqlDateTime.MinValue.Value.ToShortDateString());
if (goalDate.Date > (DateTime)SqlDateTime.MaxValue)
return new ValidationResult("Date must be before " + SqlDateTime.MaxValue.Value.ToShortDateString() );
return ValidationResult.Success;
}
The problem occurs whenever you submit the form without the optional values. In my controller, my ModelState.IsValid returns false and I get a validation error message:
Could not convert the value of type 'null' to 'System.DateTime' as expected by method GoalManager.Models.Goal.ValidateGoalDate. Must enter a valid date.
Stepping though the code, I see that the custom validator does not run on the optional fields, but when I remove the DataAnnotation from those optional fields, I return no error. If the user does not insert a date into the field, I want to insert a NULL into the table. How do tell the Validator that I do not want to error check a blank (or null) date, to ignore it, and to insert a null into the database?

The DateTime that your custom validator takes as a param is not nullable in your example... If you make it a nullable DateTime, it should fix your problem.

Here's the implementation of what #Rikon said:
public static ValidationResult ValidateGoalDate(DateTime? goalDate) {

This will (probably) avoid the exception but I guess you have to follow it up with more code inside the method:
public static ValidationResult ValidateGoalDate(DateTime goalDate = new DateTime())
If you still have a problem with the ModelState.IsValid returning false, you can put something like this in the controller:
foreach (var state in ModelState) {
if (state.Key == "start_date") state.Value.Errors.Clear();
}
(I'm sure there are better ways to do this but nevermind, at least this is self-explanatory)
By the way, it is not wise to completely disable validation, as it would enable injection security exploits. For more info on validation, and how you can also disable it on a per-field basis on client-side, read this: http://msdn.microsoft.com/en-us/library/aa479045.aspx#aspplusvalid%5Fclientside

Related

Updating a nullable DateTime field as null results in default DateTime value (0001-01-01 00:00:00.0000000)

I'm using ASP.NET Boilerplate MVC (not Core) template in my project, which uses EF6 as ORM. Database is SQL Server Express.
Here's my entity object (ignoring non-related properties):
public class Asset : AggregateRoot<long>
{
[DataType(DataType.DateTime)]
public DateTime? LastControlTime { get; set; }
}
When I create a new Asset, this field appropriately created as NULL. So, everything works as intented at first. But when I try to update an object with a simple service call, it screws up.
Here's the method in the application service class:
public void ResetLastControlTime (EntityDto<long> input)
{
var asset = Repository.Get(input.Id);
asset.LastControlTime = default(DateTime?);
}
This should reset that field to null. I also tried asset.LastControlTime = null;. But in the end it's written "0001-01-01 00:00:00.0000000" to that field in the database. I have lots of places in code that I control for a null value so now I had to change tons of old files or I must find some way to reset that field to simply NULL.
I checked similar questions here but cannot find an answer. All of them tells about nullable DateTime, which I already have. In SQL server table schema, Data Type is datetime2(7), so I guess that's correct too. Oh and deleting the DataType annotation also didn't change anything.
So what am I missing here? What should I check to find the issue?
I suppose if all else fails, you can simplify most of your code by re-implementing the property:
public class Asset : AggregateRoot<long>
{
public DateTime? _LastControlTime;
[DataType(DataType.DateTime)]
public DateTime? LastControlTime {
get {
return _LastControlTime;
}
set {
if (value == DateTime.MinValue) {
_LastControlTime = null;
} else {
_LastControlTime = value;
}
}
}
It doesn't really cut to the heart of the problem, but will let you progress without having to change all of your == null and .HasValue throughout the entire program.

how to change type validation error messages?

I'm using entity framework code first in an ASP MVC project, and I'd like to change the error message that appears for validation of a numeric type.
I have a property like
public decimal Amount1 { get; set; }
If I enter a non-number in the field, I get the message: The field Amount1 must be a number. How do I change that message?
For other validations, like Required I can just use the ErrorMessage parameter like: [Required(ErrorMessage = "My message...")]
Is there something similar for validating types?
Thank you.
Unfortunately Microsoft didn't expose any interfaces to change the default messages.
But if you are desperate enough to change these non friendly messages, you can do so by creating validation attribute for decimal, creating corresponding validator and finally register it with DataAnnotationsModelValidatorProvider at the application startup. Hope this helps.
UPDATE:
Sample below
Step 1: Create validation attribute
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false)]
public class ValidDecimalAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
if (value == null || value.ToString().Length == 0) {
return ValidationResult.Success;
}
decimal d;
return !decimal.TryParse(value.ToString(), out d) ? new ValidationResult(ErrorMessage) : ValidationResult.Success;
}
}
Step 2: Create validator
public class ValidDecimalValidator : DataAnnotationsModelValidator<ValidDecimal>
{
public ValidDecimalValidator(ModelMetadata metadata, ControllerContext context, ValidDecimal attribute)
: base(metadata, context, attribute)
{
if (!attribute.IsValid(context.HttpContext.Request.Form[metadata.PropertyName]))
{
var propertyName = metadata.PropertyName;
context.Controller.ViewData.ModelState[propertyName].Errors.Clear();
context.Controller.ViewData.ModelState[propertyName].Errors.Add(attribute.ErrorMessage);
}
}
}
Step 3: Register the adapter in Global.asax under Application_Start() method or Main() method
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(ValidDecimal), typeof(ValidDecimalValidator));
Step 4: Finally decorate your property in your model with this attribute
[ValidDecimal(ErrorMessage = "Only decimal numbers allowed")]
public decimal CPEHours { get; set; }
Hope it helps.
I couldn't find a clean solution. If there is something like [Required] you could override it in the same way. Only option I find is to remove and add another error into the model state. Again NOT the best option if you have better alternates, but does the job. This example only works if you have something like must be a number at the end. You can create a filter with this kind of loop:
foreach (var m in ModelState)
{
var errors = m.Value.Errors;
foreach (var error in errors)
{
if (error.ErrorMessage.EndsWith("must be a number"))
{
errors.Remove(error);
ModelState.AddModelError(m.Key, $"This is my own validation");
}
}
}
While it's not possible to change the whole message, you can at least change the string used to reference the field. Use the [Display(Name = "amount field"] attribute, like:
[BindProperty]
[Display(Name = "line length")]
public decimal? LineLength { get; set; }
If the user enters a string into a field like this, they will at least see an error message that reads "The value 'sdf' is not valid for line length."
Not a complete solution, but good enough in many scenarios.

What does ModelState.IsValid do?

When I do a create method i bind my object in the parameter and then I check if ModelState is valid so I add to the database:
But when I need to change something before I add to the database (before I change it the ModelState couldn't be valid so I have to do it)
why the model state still non valid.
What does this function check exactly?
This is my example:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "EncaissementID,libelle,DateEncaissement,Montant,ProjetID,Description")] Encaissement encaissement) {
encaissement.Montant = Convert.ToDecimal(encaissement.Montant);
ViewBag.montant = encaissement.Montant;
if (ModelState.IsValid) {
db.Encaissements.Add(encaissement);
db.SaveChanges();
return RedirectToAction("Index", "Encaissement");
};
ViewBag.ProjetID = new SelectList(db.Projets, "ProjetId", "nomP");
return View(encaissement);
}
ModelState.IsValid indicates if it was possible to bind the incoming values from the request to the model correctly and whether any explicitly specified validation rules were broken during the model binding process.
In your example, the model that is being bound is of class type Encaissement. Validation rules are those specified on the model by the use of attributes, logic and errors added within the IValidatableObject's Validate() method - or simply within the code of the action method.
The IsValid property will be true if the values were able to bind correctly to the model AND no validation rules were broken in the process.
Here's an example of how a validation attribute and IValidatableObject might be implemented on your model class:
public class Encaissement : IValidatableObject
{
// A required attribute, validates that this value was submitted
[Required(ErrorMessage = "The Encaissment ID must be submitted")]
public int EncaissementID { get; set; }
public DateTime? DateEncaissement { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
// Validate the DateEncaissment
if (!this.DateEncaissement.HasValue)
{
results.Add(new ValidationResult("The DateEncaissement must be set", new string[] { "DateEncaissement" });
}
return results;
}
}
Here's an example of how the same validation rule may be applied within the action method of your example:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "EncaissementID,libelle,DateEncaissement,Montant,ProjetID,Description")] Encaissement encaissement) {
// Perform validation
if (!encaissement.DateEncaissement.HasValue)
{
this.ModelState.AddModelError("DateEncaissement", "The DateEncaissement must be set");
}
encaissement.Montant = Convert.ToDecimal(encaissement.Montant);
ViewBag.montant = encaissement.Montant;
if (ModelState.IsValid) {
db.Encaissements.Add(encaissement);
db.SaveChanges();
return RedirectToAction("Index", "Encaissement");
};
ViewBag.ProjetID = new SelectList(db.Projets, "ProjetId", "nomP");
return View(encaissement);
}
It's worth bearing in mind that the value types of the properties of your model will also be validated. For example, you can't assign a string value to an int property. If you do, it won't be bound and the error will be added to your ModelState too.
In your example, the EncaissementID value could not have a value of "Hello" posted to it, this would cause a model validation error to be added and IsValid will be false.
It is for any of the above reasons (and possibly more) that the IsValid bool value of the model state will be false.
ModelState.IsValid will basically tell you if there is any issues with your data posted to the server, based on the data annotations added to the properties of your model.
If, for instance, you have a [Required(ErrorMessage = "Please fill")], and that property is empty when you post your form to the server, ModelState will be invalid.
The ModelBinder also checks some basic stuff for you. If, for instance, you have a BirthDate datepicker, and the property that this picker is binding to, is not a nullable DateTime type, your ModelState will also be invalid if you have left the date empty.
Here, and here are some useful posts to read.
You can find a great write-up on ModelState and its uses here.
Specifically, the IsValid property is a quick way to check if there are any field validation errors in ModelState.Errors. If you're not sure what's causing your Model to be invalid by the time it POST's to your controller method, you can inspect the ModelState["Property"].Errors property, which should yield at least one form validation error.
Edit: Updated with proper dictionary syntax from #ChrisPratt
This is not meant to be the best answer, but I find my errors by stepping through the ModelState Values to find the one with the error in Visual Studio's debugger:
My guess is that everyone with a question about why their ModelState is not valid could benefit from placing a breakpoint in the code, inspecting the values, and finding the one (or more) that is invalid.
This is not the best way to run a production website, but this is how a developer finds out what is wrong with the code.

Required number parameter defaulting to 0 when not included in JSON

I have a model where I am using DataAnnotations to perform validation, such as
public class OrderDTO
{
[Required]
public int Id { get; set; }
[Required]
public Decimal Amount { get; set; }
}
Then I am checking the ModelState in each request to make sure that the JSON is valid.
However, I am having trouble for number properties such as Amount above. Even though it is set as [Required], if it's not included in the JSON it will skip the ModelState validation because it is automatically defaulted to 0 instead of null, so the model will seem valid even though it isn't.
An easy way to 'fix' this is to set all the number properties as nullable (int?, Decimal?). If I do this, the defaulting to 0 doesn't happen, but I don't like this as a definitive solution as I need to change my model.
Is there a way to set the properties to null if they are not part of the JSON?
Because Decimal is a non-nullable type so you cannot do that.
You need Decimal? to bind null value.
You have to use a nullable type. Since a non-nullable value, as you know, cannot be null then it will use 0 as a default value and therefore appear to have a value and always pass the validation.
As you have said it has to be null for the validation to work and therefore be nullable. Another option could be to write your own validation attribute but this could then cause a problem as you would most likely be saying if is null or 0 then not valid, a big issue when you want to have 0 as an accepted value because you then need another way of deciding when 0 is and isn't valid.
Example for custom validation, not specific to this case.
Web API custom validation to check string against list of approved values
A further option could be to add another property that is nullable and provides the value to the non-nullable property. Again, this could cause issues with the 0 value. Here is an example with the Id property, your json will now need to send NullableId rather than Id.
public class OrderDTO
{
//Nullable property for json and validation
[Required]
public int? NullableId {
get {
return Id == 0 ? null : Id; //This will always return null if Id is 0, this can be a problem
}
set {
Id = value ?? 0; //This means Id is 0 when this is null, another problem
}
}
//This can be used as before at any level between API and the database
public int Id { get; set; }
}
As you say another option is to change the model to nullable values through the whole stack.
Finally you could look at having an external model coming into the api with nullable properties and then map it to the current model, either manually or using something like AutoMapper.
I agree with others that Decimal being a non Nullable type cannot be assigned with a null value. Moreover, Required attribute checks for only null, empty string and whitespaces. So for your specific requirement you can use CustomValidationAttribute and you can create a custom Validation Type to do the "0" checking on Decimal properties.
There is no way for an int or Decimal to be null. That is why the nullables where created.
You have several options [Edit: I just realized that you are asking for Web-API specifically and in this case I believe the custom binder option would be more complex from the code I posted.]:
Make the fields nullable in your DTO
Create a ViewModel with nullable types, add the required validation attributes on the view model and map this ViewModel to your DTO (maybe using automapper or something similar).
Manually validate the request (bad and error prone thing to do)
public ActionResult MyAction(OrderDTO order)
{
// Validate your fields against your possible sources (Request.Form,QueryString, etc)
if(HttpContext.Current.Request.Form["Ammount"] == null)
{
throw new YourCustomExceptionForValidationErrors("Ammount was not sent");
}
// Do your stuff
}
Create a custom binder and do the validation there:
public class OrderModelBinder : DefaultModelBinder
{
protected override bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor, object value)
{
if ((propertyDescriptor.PropertyType == typeof(DateTime) && value == null) ||
(propertyDescriptor.PropertyType == typeof(int) && value == null) ||
(propertyDescriptor.PropertyType == typeof(decimal) && value == null) ||
(propertyDescriptor.PropertyType == typeof(bool) && value == null))
{
var modelName = string.IsNullOrEmpty(bindingContext.ModelName) ? "" : bindingContext.ModelName + ".";
var name = modelName + propertyDescriptor.Name;
bindingContext.ModelState.AddModelError(name, General.RequiredField);
}
return base.OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, value);
}
}
And register your binder to your model using one of the techniques described in the following answer: https://stackoverflow.com/a/13749124/149885
For example:
[ModelBinder(typeof(OrderBinder))]
public class OrderDTO
{
[Required]
public int Id { get; set; }
[Required]
public Decimal Amount { get; set; }
}

Exception with invalid dates using data annotations

Have a Json API model with a date property defined as:
[Required]
[DataType(DataType.Date, ErrorMessage = "Invalid expiry date")]
public DateTime ExpiryDate { get; set; }
when posting an incorrect value for the ExpiryDate, examples:
"ExpiryDate":"2020-02-31T00:00:00",
"ExpiryDate":"2020-01-99T00:00:00",
"ExpiryDate":"abc",
the ModelState.Values.Errors[0].ErrorMessage is empty. Instead there is Model exception that I can not return to the API consumer, looks ugly.
ModelState.Values.Errors[0].Exception = {"Could not convert string to DateTime: 2020-02-31T00:00:00. Path 'CardDetails.ExpiryDate', line 13, position 39."}
My question are: how can I make the data annotation generate an error instead of an exception? Why is not the current data annotation giving an error, is not the job of [DataType(DataType.Date...] to do that?
The main issue here is that a DateTime value in JSON (at least according to the JSON parser) has a specific format, and not following that format is a parsing error, which is what's currently happening.
I think you'll have to take the value in as a string and do a conversion and validation on top of it. There's a couple of options. One is a custom ValidationAttribute like #erikscandola mentioned. Another is to implmenent IValidatableObject interface on your model.
You could also convert the model property to a string and simply do a check in the controller action:
DateTime expiryDate;
if (!DateTime.TryParse(model.ExpiryDate, out expiryDate))
{
ModelState.AddModelError("", string.Format(
"The given ExpiryDate '{0}' was not valid", model.ExpiryDate));
}
The approach depends upon how much reuse you need of the validation logic.
You should create a custom data annotation:
public class RequiredDateTimeAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
// Here your code to check date in value
}
}

Categories

Resources