Conditional Validation in MVC using IValidatableObject - asp.net-mvc

I have a scenario where I need to validate a view model differently under different contexts. For example, certain fields are required if you are going to, say, Post a message but they are not needed if you just want to save a Draft. Yet, there are still required fields for a Draft. Therefore, I have inputs that require validation based on the scenario you are saving under.
I'm using IValidatableObject on supple input models since the static attributes don't seem to allow this. I see there is the option to pass in data to the Validate method using the ValidationContext.Items property. I can read that in the validation, but after searching through the source code for MVC, it doesn't look like there's a way to actually set that before you would do a TryUpdate, etc, to set the scenario you're validating under.
Am I missing something or is there another method I'm not seeing?
public IEnumerable<System.ComponentModel.DataAnnotations.ValidationResult> Validate(ValidationContext validationContext)
{
ValidationLevel validationLevel;
object validationLevelObject;
if (validationContext.Items.TryGetValue("$" + nameof(ValidationLevel), out validationLevelObject))
{
validationLevel = (ValidationLevel)validationLevelObject;
}
else
{
validationLevel = ValidationLevel.Full;
}
...

In your model/class that implemented the IValidatableObject, try doing something like this:
...
public List<ValidationResult> ValidationResults { get; } = new List<ValidationResult>();
public bool TryValidate(out List<ValidationResult> vResults)
{
vResults = ValidationResults;
var context = new ValidationContext(this);
Validate(context);
var fieldValidations = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(context.ObjectInstance, context, fieldValidations,
validateAllProperties: true);
//Add any attribute validation errors to ValidationResults
if (!isValid)
{
foreach (var validationResult in fieldValidations)
{
ValidationResults.Add(validationResult);
}
}
//Add your custom validations
if (!IsDraft() && Message.IsStringBlank())
{
ValidationResults.Add(new ValidationResult("Message cannot empty");
}
isValid = !ValidationResults.Any();
return isValid;
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (_isClassValidate) return new List<ValidationResult>();
_isClassValidate = true;
return ValidationResults;
}

Related

Validate an Object in ASP.NET MVC without passing it into an Action

In ASP.NET MVC you can validate the model passed to an Action with ModelState.IsValid().
I'd like to validate arbitrary objects rather than the one Model passed in. How can I do that, using the framework's libraries?
public ActionResult IsValidSoFar()
{
// Get a user's autosaved data
var json = await ...
HomeModel model = JsonConvert.Deserialize<HomeModel>(json);
// Validate the model <---- How?
}
public class HomeModel
{
[Required, MaxLength(100)]
public string Name { get; set; }
}
you can use ValidationContext class ... like below
var context = new ValidationContext(modelObject);
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(modelObject, context, results);
if (!isValid)
{
foreach (var validationResult in results)
{
//validation errors
}
}
You can use ValidateModel or TryValidateModel controller methods.
ValidateModel - throws exception if model is not valid.
TryValidateModel - returns bool which indicates if model is valid.
IMPORTANT: If you validate list of models one by one, you probably would like to reset ModelState for each iteration by calling ModelState.Clear();
Please see my question regarding this: Validate list of models programmatically in ASP.NET MVC

.NET MVC Custom validation (without data annotation)

use .NET MVC and code-first EF to implement of requested functionality. Business objects are relatively complex and I use System.ComponentModel.DataAnnotations.IValidatableObject to validate business object.
Now I'm trying to find the way, how to show validation result from business object, using MVC ValidationSummary without using data annotations. For example (very simplified):
Business Object:
public class MyBusinessObject : BaseEntity, IValidatableObject
{
public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
return Validate();
}
public IEnumerable<ValidationResult> Validate()
{
List<ValidationResult> results = new List<ValidationResult>();
if (DealType == DealTypes.NotSet)
{
results.Add(new ValidationResult("BO.DealType.NotSet", new[] { "DealType" }));
}
return results.Count > 0 ? results.AsEnumerable() : null;
}
}
Now in my MVC controller I have something like this:
public class MyController : Controller
{
[HttpPost]
public ActionResult New(MyModel myModel)
{
MyBusinessObject bo = GetBoFromModel(myModel);
IEnumerable<ValidationResult> result = bo.Validate();
if(result == null)
{
//Save bo, using my services layer
//return RedirectResult to success page
}
return View(myModel);
}
}
In view, I have Html.ValidationSummary();.
How I can pass IEnumerable<ValidationResult> to the ValidationSummary?
I tried to find an answer by googling, but all examples I found describes how to show validation summary using data annotations in Model and not in Business object.
Thanks
Add property, say BusinessError, in the model
in the View do the following
#Html.ValidationMessageFor(model => model.BusinessError)
Then in your controller whenever you have error do the following
ModelState.AddModelError("BussinessError", your error)
I would have a look at FluentValidation. It's a framework for validation without data annoations. I've used it with great success in some projects for complex validation, and it is also usable outside of the MVC-project.
Here is the sample code from their page:
using FluentValidation;
public class CustomerValidator: AbstractValidator<Customer> {
public CustomerValidator() {
RuleFor(customer => customer.Surname).NotEmpty();
RuleFor(customer => customer.Forename).NotEmpty().WithMessage("Please specify a first name");
RuleFor(customer => customer.Company).NotNull();
RuleFor(customer => customer.Discount).NotEqual(0).When(customer => customer.HasDiscount);
RuleFor(customer => customer.Address).Length(20, 250);
RuleFor(customer => customer.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");
}
private bool BeAValidPostcode(string postcode) {
// custom postcode validating logic goes here
}
}
Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();
ValidationResult results = validator.Validate(customer);
bool validationSucceeded = results.IsValid;
IList<ValidationFailure> failures = results.Errors;
Entity Framework should throw a DbEntityValidationException if there are validation errors. You can then use the exception to add the errors to the ModelState.
try
{
SaveChanges();
}
catch (DbEntityValidationException ex)
{
AddDbErrorsToModelState(ex);
}
return View(myModel);
protected void AddDbErrorsToModelState(DbEntityValidationException ex)
{
foreach (var validationErrors in ex.EntityValidationErrors)
{
foreach (var validationError in validationErrors.ValidationErrors)
{
ModelState.AddModelError(validationError.PropertyName, validationError.ErrorMessage);
}
}
}
One of the ways to pass the contents of IEnumerate and keep taking advantage of Html.ValidationSummary is by updating ModelState.
You can find a good discussion on how to update the ModelState here.

MVC3 Remote Validation with DataAnnotationsModelValidator adapter

I want to validate various telephone number properties on a DTO property of my model using a custom DataAnnotationsAttribute. I don't want to duplicate the DataAnnotations onto ViewModels, to keep the code DRY, and instead I have registered a custom adapter for client-side validation using DataAnnotationsModelValidatorProvider. This adapter provides ModelClientValidationRemoteRules, normally used by the RemoteAttribute. jQuery unobtrusive validation then calls into my validate action, which validates the individual fields.
This setup isn't really adequate however.
The attribute currently uses the its ContainerType to work out which
validation action to call. The DTO is used on different viewmodels
at different levels of nesting, however, so we don't know exactly what
prefix to use on the action. Depending on the location of the ProfileDto
in the model hierarchy, the action prefix would need to change
The validation action uses Request.Form.Keys to work out which
property which should be validating. I know it is best practice to
stay away from the Request object in Action for the sake of unit
testing etc.
Is there a good way to include the name of the field to validate in postback, so I can have it on my action as an additional parameter instead of using Request.Form?
Is there a way to get the model binder to bind my properties, given that they will posted back with a prefix dependent on the child model's name?
Thanks in advance!
The attribute is as follows:
public class PhoneNumberAttribute : ValidationAttribute
{
public PhoneNumberType RequiredType { get; set; }
public PhoneNumberAttribute()
: base("{0} is not a valid phone number.")
{
}
public override bool IsValid(object value)
{
string s = value as string;
if (s == null)
{
return false;
}
if (!PhoneNumberUtils.IsValidNumber(s, RequiredType))
{
return false;
}
return true
}
public override string FormatErrorMessage(string name)
{
return string.Format(ErrorMessageString, name);
}
}
and the adapter:
public class PhoneNumberAttributeAdapter : DataAnnotationsModelValidator<PhoneNumberAttribute>
{
public PhoneNumberAttributeAdapter(ModelMetadata metadata, ControllerContext context, PhoneNumberAttribute attribute)
: base(metadata, context, attribute)
{
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var errorMessage = Attribute.FormatErrorMessage(Metadata.GetDisplayName());
var routeData = new RouteValueDictionary {
{ "controller", "Validate" },
{ "action", Metadata.ContainerType.Name },
};
var path = RouteTable.Routes.GetVirtualPathForArea(ControllerContext.RequestContext, routeData);
var rule = new ModelClientValidationRemoteRule(
errorMessage,
path.VirtualPath,
"POST",
"*." + Metadata.PropertyName);
return new[] { rule };
}
}
here is the Action:
public ActionResult ProfileDto([Bind(Prefix = "Dto")]ProfileDto model)
{
string fieldToValidate = Request.Form.Keys[0];
if (ModelState.IsValidField(fieldToValidate))
{
return Json(true);
}
var fieldErrors = ModelState[fieldToValidate].Errors;
return Json(fieldErrors.First().ErrorMessage);
}
Take a look at this example here where is show how to get the nested properties even with prefix in the custom jQuery validator.
Secondly, MVC model binder should bind your prefix automatically.

Date comparison in asp.net mvc3 model

I have two fields in my model
CreateDateTo
CreateDateFrom
which renders like this
<b>Start Date</b> #Html.EditorFor(model => model.AdvanceSearch.CreateDatefrom, new { #class = "picker startDate" })
<b>End Date</b> #Html.EditorFor(model => model.AdvanceSearch.CreateDateto, new { #class = "picker endDate" })
I have a validation scenario that enddate should not be greater then start date, currently I am validating it by jquery
$.validator.addMethod("endDate", function (value, element) {
var startDate = $('.startDate').val();
return Date.parse(startDate) <= Date.parse(value);
}, "* End date must be Equal/After start date");
I want to know that is there any way in MVC3 model validation to do this?
I would say that you should not rely solely on Javascript unless you are in control of your client's browser in some sort of intranet application. If the app is public facing - make sure you have both client and server side validation.
Also, a cleaner way of implementing the server side validation inside your model object can be done with a custom validation attribute shown below. Your validation then becomes centralised and you do not have have to explicitly compare the dates in your controller.
public class MustBeGreaterThanAttribute : ValidationAttribute
{
private readonly string _otherProperty;
public MustBeGreaterThanAttribute(string otherProperty, string errorMessage) : base(errorMessage)
{
_otherProperty = otherProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var otherProperty = validationContext.ObjectInstance.GetType().GetProperty(_otherProperty);
var otherValue = otherProperty.GetValue(validationContext.ObjectInstance, null);
var thisDateValue = Convert.ToDateTime(value);
var otherDateValue = Convert.ToDateTime(otherValue);
if (thisDateValue > otherDateValue)
{
return ValidationResult.Success;
}
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
}
This can then be applied to your model like so:
public class MyViewModel
{
[MustBeGreaterThan("End", "Start date must be greater than End date")]
public DateTime Start { get; set; }
public DateTime End { get; set; }
// more properties...
}
You need to create a custom validation against the model. You could put this in the controller after if(Model.IsValid)
if(Model.End<Model.StartDate)
....
But I would stick to javascript.
It works on the clientside and does not hit the server.
Unless you just need the added assurance.

MVC3 Object with IValidatableObject - Validate fires multiple times per TryUpdateModel()

I have an object class generated from a T4, with a partial SafeClass to do validation, which looks like this:
public partial class Address : IValidatableObject
This class has a Validate method like so:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
//ValidationResponse is a custom struct that holds a few properties.
ValidationResponse r = this.IsValidAddress(); //a method that does some checks
if (!r.IsValid)
{
yield return new ValidationResult(r.Message, new string[] { "Address1" });
}
}
In my Controller's HttpPost event, I have the line:
if (!TryUpdateModel(_policy))
return View(_policy);
Note that the Policy object contains a Person object, which contains an Address object (pieces of all 3 are rendered in the same view; may be relevant, I don't know).
When TryUpdateModel() executes, the Address's Validate method gets called 3 times. I verified it's not triggering for other addresses on the policy. I have also verified that the Controller's HttpPost method is only being called once. It's the single execution of TryUpdateModel() that fires off 3 Validates.
Has anybody else run into this sort of thing? Any ides what's going on?
I had encoutered similar issue running this code
if (isPoorSpecimen)
{
errors.Add(new ValidationResult("Your are reporting poor specimen condition, please specify what is the reason"
, new string[] { "SpecimenCondition", "QtyOk", "DocumentedOk", "ColdChainOk", "PackagingOK", "IsProcessable" }));
}
It will show the error message 6 times in Html.ValidationSummary() .
The solution is to highligt a single control per error.
if (isPoorSpecimen)
{
errors.Add(new ValidationResult("Your are reporting poor specimen condition, please specify what is the reason"
, new string[] { "SpecimenCondition" }));
}
It is called 3 times, because the Address instance is validated first as a standalone entity, then as a member of a standalone Person entity, and finally as a member of the Person entity being a member of the Policy entity.
I would suggest the following solutions:
1) Remove IValidatableObject from all the classes but Policy and validate its members manually:
public class Policy : IValidatableObject
{
public Person PersonInstance { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// validate Policy...
// then validate members explicitly
var results = PersonInstance.Validate(validationContext);
}
}
public class Person
{
public Address AddressInstance { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// validate Person...
// then validate members explicitly
var results = AddressInstance.Validate(validationContext);
}
}
public class Address
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// validate Address...
}
}
2) Or add a flag to each class to validate only once, since the instance across the calls is the same:
private bool validated = false;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!validated)
{
validated = true;
// do validation
}
}
Hope this helps.

Resources