How do I enforce e.g. a numeric constraint using DataAnnotations? - c#

I'm trying to utilize model state to validate requests to my WebAPI, but I'm having some trouble with how to control the error messages for some properties. For example, given the following model
public class Stuff {
[Range(0, Double.PositiveInfinity)]
public double? SomeProp { get; set; }
}
(and some infrastructure defined at the bottom of the post) a request with a payload such as { someProp: "1.2" } will model bind correctly - and negative or missing values will give errors - but if I enter something that isn't a valid number (e.g. { someProp: "hello" }) the model state has an error such as
Error converting value "hello" to type 'System.Nullable`1[System.Double]'. Path 'someStuff', line 1, position 41.
(I guess the position is in the JSON object, here cut from a larger request, so don't mind the exact numbers...). For another property, which isn't nullable but instead has a [Required] attribute, the error message about a missing required property is included in the model state dict in addition to the above message.
I do understand why "hello" cannot be bound to a double? (or double), but regardless of constraints I set in attributes, it seems that the value has to be model bound before they are even checked, and if model binding fails I both get that not-so-friendly message above, as well as all other errors that apply when the value is unset. The only constraint I've been able to apply before attempting to bind the value is a Regex, but if that fails I get a similarly user-unfriendly message like
The field SomeProp must match the regular expression '<my regex>'.
Is there a way to work around this, so that I can give the user a better error message when entering something non-numeric into a textbox that should specify a numeric value?
Footnotes: my validation infrastructure
[HttpPost]
[Validate]
public IHttpActionResult Validate(Stuff stuff) {
return Ok();
}
where the Validate attribute is defined like this:
public class ValidationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request
.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}

Related

Different validation contexts in ASP.Net and how to handle this

I have two endpoints in my ASP.net backend. One that takes an object with optional null parameters i.e. not required and also without any range checking so that this object can be stored in the database and a second that takes an ID of an object in the database. In this second endpoint I want to get the object out of the database and perform further validation. I.e. the user needs to have set all the optional values to be valid in range values for this second endpoint to work.
Basically I have two different endpoints that have the same data object but different validation needs.
eg
public class AnObject
{
[Required]
public Guid Id {get;set;}
public double? Foo {get;set;}
public double? Bar {get;set;}
}
with endpoint like this
[HttpPost]
public IActionResult Endpoint1([FromBody]AnObject anObject)
{
// if anObject is not valid (id is missing or incorrect types) ASP.net returns a BadRequest
// if ok add anObject to DB with id as key
}
and then a second object like this that has the same fields but non nullable and Required with Range checking.
public class AMoreStringentObject
{
[Required]
public Guid Id {get;set;}
[Required][Range(1,10)]
public double Foo {get;set;}
[Required][Range(1,10)]
public double Bar {get;set;}
}
and endpoint
[HttpPost]
public IActionResult Endpoint2(string id)
{
// get the AnObject from the DB with id matching parameter id
// now want to validate this using the AMoreStringentObject
}
in the second end point I would like get the AnObject from the database and perform validation using the DataAnnotations library. The second object and the first are not the same (nullable type on first) so I can't simply automap them. So i thought what I could do is serialize the AnObject to json and not write out things that are null, easy enough. So I want to then call the validation that must occur somewhere in the asp.net magic that would throw out error messages like "Foo is required". But how do I do that? i.e. validate the json string using the AMoreStringentObject so that the Required attributes throw a BadRequest with the required error. OR if there is a better way to go about this I'm all ears.
Additionally, I'm aware that I could do this with settings in the json deserializer but you get an unhelpful exception when you do this. I'd really like to get the BadRequest response that the Required field creates as this is a much more helpful message to the user. eg the json exception has a json position which is no use to the user because it is the backend that has serialized it.
I would recommend looking into FluentValidation which can be integrated into ASP.NET Core pipeline and will allow you to build reusable validations.
But if you want to use the build into the framework validation - you can look into Validator.TryValidateObject with manually created ValidationContext:
AMoreStringentObject toValidate = ...;
var vc = new ValidationContext(toValidate, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(u, vc, results, true);

How to return a key for the whole model in FluentValidation

Imagine, you have this model
public Class SomeModel
{
public string someString1 { get; set; }
public string someString2 { get; set; }
}
Now you want to validate this model. In FluentValidation you write this.RuleFor(m => m.{property} for each property on the model/class and when a validation error occurs, you get the key of the property that failed validation and a message.
Now, what i am wondering about, is how you can set a key for the whole model when it returns an error from validation, which validates two or more properties. (e.g. this.RuleFor(m => m).TestIfPropsAreEqual(); ) What is the correct way to approach this problem? I don't really want to write the method on a property, because that wouldn't be correct, because you are validating the whole model not a specific property.
EDIT: Forgot to mention that when validating the whole model, the returned key is empty.
https://fluentvalidation.net/start#overriding-the-property-name is the answer. I can give the a name to class or new name to a property.

How to selectively validate some data annotation attribute?

There are some properties in my view model that are optional when saving, but required when submitting. In a word, we allow partial saving, but the whole form is submitted, we do want to make sure all required fields have values.
The only approaches I can think of at this moment are:
Manipulate the ModelState errors collection.
The view model has all [Required] attributes in place. If the request is partial save, the ModelState.IsValid becomes false when entering the controller action. Then I run through all ModelState (which is an ICollection<KeyValuePair<string, ModelState>>) errors and remove all errors raised by [Required] properties.
But if the request is to submit the whole form, I will not interfere with the ModelState and the [Required] attributes take effect.
Use different view models for partial save and submit
This one is even more ugly. One view model will contain all the [Required] attributes, used by an action method for submitting. But for partial save, I post the form data to a different action which use a same view model without all the [Required] attributes.
Obviously, I would end up with a lot of duplicate code / view models.
The ideal solution
I have been thinking if I can create a custom data annotation attribute [SubmitRequired] for those required properties. And somehow make the validation ignores it when partial saving but not when submitting.
Still couldn't have a clear clue. Anyone can help? Thanks.
This is one approach I use in projects.
Create a ValidationService<T> containing the business logic that will check that your model is in a valid state to be submitted with a IsValidForSubmission method.
Add an IsSubmitting property to the view model which you check before calling the IsValidForSubmission method.
Only use the built in validation attributes for checking for invalid data i.e. field lengths etc.
Create some custom attributes within a different namespace that would validate in certain scenarios i.e. [RequiredIfSubmitting] and then use reflection within your service to iterate over the attributes on each property and call their IsValid method manually (skipping any that are not within your namespace).
This will populate and return a Dictionary<string, string> which can be used to populate ModelState back to the UI:
var validationErrors = _validationService.IsValidForSubmission(model);
if (validationErrors.Count > 0)
{
foreach (var error in validationErrors)
{
ModelState.AddModelError(error.Key, error.Value);
}
}
I think there is more precise solution for your problem. Lets say you're submitting to one method, I mean to say you are calling same method for Partial and Full submit. Then you should do like below:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult YourMethod(ModelName model)
{
if(partialSave) // Check here whether it's a partial or full submit
{
ModelState.Remove("PropertyName");
ModelState.Remove("PropertyName2");
ModelState.Remove("PropertyName3");
}
if (ModelState.IsValid)
{
}
}
This should solve your problem. Let me know if you face any trouble.
Edit:
As #SBirthare commented that its not feasible to add or remove properties when model get updated, I found below solution which should work for [Required] attribute.
ModelState.Where(x => x.Value.Errors.Count > 0).Select(d => d.Key).ToList().ForEach(g => ModelState.Remove(g));
Above code will get all keys which would have error and remove them from model state. You need to place this line inside if condition to make sure it runs in partial form submit. I have also checked that error will come for [Required] attribute only (Somehow model binder giving high priority to this attribute even you place it after/below any other attribute). So you don't need to worry about model updates anymore.
My approach is to add conditional checking annotation attribute, which is learned from foolproof.
Make SaveMode part of the view model.
Mark the properties nullable so that the values of which are optional when SaveMode is not Finalize.
But add a custom annotation attribute [FinalizeRequired]:
[FinalizeRequired]
public int? SomeProperty { get; set; }
[FinalizeRequiredCollection]
public List<Item> Items { get; set; }
Here is the code for the Attribute:
[AttributeUsage(AttributeTargets.Property)]
public abstract class FinalizeValidationAttribute : ValidationAttribute
{
public const string DependentProperty = "SaveMode";
protected abstract bool IsNotNull(object value);
protected static SaveModeEnum GetSaveMode(ValidationContext validationContext)
{
var saveModeProperty = validationContext.ObjectType.GetProperty(DependentProperty);
if (saveModeProperty == null) return SaveModeEnum.Save;
return (SaveModeEnum) saveModeProperty.GetValue(validationContext.ObjectInstance);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var saveMode = GetSaveMode(validationContext);
if (saveMode != SaveModeEnum.SaveFinalize) return ValidationResult.Success;
return (IsNotNull(value))
? ValidationResult.Success
: new ValidationResult(string.Format("{0} is required when finalizing", validationContext.DisplayName));
}
}
For primitive data types, check value!=null:
[AttributeUsage(AttributeTargets.Property)]
public class FinalizeRequiredAttribute : FinalizeValidationAttribute
{
protected override bool IsNotNull(object value)
{
return value != null;
}
}
For IEnumerable collections,
[AttributeUsage(AttributeTargets.Property)]
public class FinalizeRequiredCollectionAttribute : FinalizeValidationAttribute
{
protected override bool IsNotNull(object value)
{
var enumerable = value as IEnumerable;
return (enumerable != null && enumerable.GetEnumerator().MoveNext());
}
}
This approach best achieves the separation of concerns by removing validation logic out of controller. Data Annotation attributes should handle that kind of work, which controller just need a check of !ModelState.IsValid. This is especially useful in my application, because I would not be able to refactor into a base controller if ModelState check is different in each controller.

Understanding validity in a model where a property has more than one validation attribute

I'm working on an MVC app, and a bit of model code I've written has unfolded somewhat like this:
public class SomeModel
{
public int? CodeA { get; set; }
public int? CodeB { get; set; }
[RequiredIf("CodeA", 3, ErrorMessage = "(required for [Something]!)")]
[RequiredIf("CodeB", 99, ErrorMessage = "(required for [Other]!)")]
public string Foo { get; set; }
// SNIP: Unimportant details
}
Note: The RequiredIf() implementation I am using is found here.
I have decorated property Foo, which a user has the ability to edit in certain circumstances, with two RequiredIf() attributes. There are two different cases in which it is required to be filled out. In all other circumstances, the front end will parse the user's input and populate it for them 'behind' the scenes.
Question: If only one case (e.g. CodeA = 3, CodeB = 4) is satisified, and the user fails to enter anything thus causing a negative validation, will the model still be marked as Invalid and a ErrorMessage logged for it? Or, since the Code B condition is satisfied, will that override the validation performed if CodeA is in a state that it is required (and not entered)?
Another way of asking: are validations additive, or is there an implicit limit to the results of only one validation at a time?
Validation is negative. For validation to pass, ALL validators must confirm the field is valid. So for your Foo, if the CodeA validator passes and the CodeB validator fails, validation will fail. Modelstate will contain a single error for that field. If both fail, modelstate will contain two errors for that field.

How can I remove MVC2's validation messages?

I am using ASP.NET C# MVC2 and I have the following field within a model with the following data annotation validation attributes:
[DisplayName("My Custom Field")]
[Range(long.MinValue, long.MaxValue, ErrorMessage = "The stated My Custom Field value is invalid!")]
public long? MyCustomField{ get; set; }
In the form this field should allow the user to leave it blank and display a validation message should the user attempt to enter a value that cannot be expressed as a number. From a validation point of view, this is working as intended and displaying the following error messages:
The stated My Custom Field value is invalid!
The field My Custom Field must be a number.
The first validation message is the custom validation message that I wrote and the second validation message is the one the MVC2 automatically generates. I need to get rid of the second one since its redundant. How do I do this? In my view I have the following markup
<% Html.EnableClientValidation(); %>
<% using (Html.BeginForm())
{ %>
<%:Html.ValidationSummary(false)%>
<% Html.ValidateFor(m => m.MyCustomField); %>
This issue you have here is because the property being bound is a numeric, and model binding is automatically handling the fact that the string could not be converted to a number. It's not the RangeAttribute's doing.
You might instead consider having a new property as a string and deriving your own RangeAttribute which works at the string level, parsing the number first.
Then you have your existing property wrap that string:
[DisplayName("My Custom Field")]
[MyCustomRangeAttribute(/* blah */)] //<-- the new range attribute you write
public string MyCustomFieldString
{
get; set;
}
public int? MyCustomField
{
get
{
if(string.IsNullOrWhiteSpace(MyCustomField))
return null;
int result;
if(int.TryParse(MyCustomField, out result))
return result;
return null;
}
set
{
MyCustomFieldString = value != null ? value.Value.ToString() : null;
}
}
Your code can continue to work on the int? property quite happily, but - all model binding is done on the string property.
You will also ideally add [Bind(Exclude"MyCustomField")] to the model type - to ensure that MVC doesn't try and bind the int? field. Or you can just make it internal. If it's in the web project and you only need to reference it in the web project.
You could also consider the really hacky approach - and finding that error in your controller method via ModelState.Errors and removing it before you return your view result...

Categories