I have a problem with custom validation. I have a ViewModel:
public class CityViewModel
{
[ForeignKey(ErrorMessageResourceName = "County")]
public int CountyId { get; set; }
public string PostCode { get; set; }
}
I created a custom validation class named ForeignKey that contains this code:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
public class ForeignKeyAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
PropertyInfo propretyInfo = validationContext.ObjectType.GetProperty(validationContext.MemberName);
ResourceManager manager = Resource.ResourceManager;
if (propretyInfo.PropertyType.IsGenericType && propretyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) {
if (value != null && (int)value == 0) {
return new ValidationResult(manager.GetString(ErrorMessageResourceName));
}
else {
return ValidationResult.Success;
}
}
else if (value == null || (int)value == 0) {
return new ValidationResult(manager.GetString(ErrorMessageResourceName));
}
return ValidationResult.Success;
}
}
This method works perfectly and return the error correctly. The problem is in the action in my controller:
[HttpPost]
public ActionResult Create(DataSourceRequest request, CityViewModel model)
{
try {
if (ModelState.IsValid) {
// Some code
return Json(new[] { model }.ToDataSourceResult(request, ModelState));
}
}
catch (Exception e) {
// Some code
}
return Json(ModelState.ToDataSourceResult());
}
If CountyId is null (really is 0 but during validation before entering in the method Create can be null) my ModelState contains, for CountyId field, this error "The CountyId field is required." instead of my error passed to ForeignKey custom attribute.
If I use this code:
TryValidateModel(model);
Then ModelState contains both errors, so before calling TryValidateModel I should use:
ModelState["CountyId"].Errors.Clear();
How can I say to MVC not to overwrite my error during first validation? I prefer to use simply ModelState.IsValid. Anyone can help me?
Try Excluding the Id in the Create method parameters
[HttpPost]
public ActionResultCreate([Bind(Exclude = "Id")], CityViewModel model)
Related
I have this Dto class for the web api controller in .NET Core 2.2 MVC.
ApplicationDocumentType is an enum
public class DocumentUploadDto
{
[FileValidation]
public IFormFile File { get; set; }
public ApplicationDocumentType DocumentType { get; set; }
public Guid Id { get; set; }
}
and
public enum ApplicationDocumentType
{
BANKSTATEMENT,
NRIC
}
and the below class implements the [FileValidation]
public class FileValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var file = value as IFormFile;
// some code removed for brevity
if (!AllowMimeTypes.Contains(file.ContentType))
{
ErrorMessage = "Invalid file type.";
return new ValidationResult(ErrorMessage);
}
return ValidationResult.Success;
}
}
Now I need to validate based on DocumentType. How do I pass DocumentType into FileValidationAttribute to do some validation?
Currently all DocumentType is having the same validation. But now I need to customize the validation based on DocumentType.
Thanks Richard for the clue, but I just keep getting the first enum value.
public class FileValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var containerType = validationContext.ObjectType;
var documentType = containerType.GetProperty("DocumentType");
var file = value as IFormFile;
if (file == null)
return new ValidationResult("No file found.");
if (documentType != null)
{
var documentTypeValue = documentType.GetValue(validationContext.ObjectInstance, null);
if (documentTypeValue.ToString() == "NRIC"
&& file.ContentType == "application/pdf")
{
ErrorMessage = "Invalid file type. Pdf file type is not allowed for NRIC.";
return new ValidationResult(ErrorMessage);
}
}
// some code removed for brevity purpose.
}
}
If document type is static, you can simply add an attribute to your Attribute.
public class FileValidationAttribute : ValidationAttribute
{
public FileValidationAttribute(params string[] allowMimeTypes)
{
AllowMimeTypes = allowMimeTypes;
}
public string[] AllowMimeTypes { get; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var file = value as IFormFile;
// some code removed for brevity
if (!AllowMimeTypes.Contains(file.ContentType))
{
ErrorMessage = "Invalid file type.";
return new ValidationResult(ErrorMessage);
}
return ValidationResult.Success;
}
}
and then, in Dto, add allowed Mime Types
public class DocumentUploadDto
{
[FileValidation("text/javascript", "text/html")]
public IFormFile File { get; set; }
public ApplicationDocumentType DocumentType { get; set; }
public Guid Id { get; set; }
}
If you need to pass mime type dynamically, then I suggest looking at https://fluentvalidation.net/ which allows you to easily add data to validation context and write more fluent validators.
How to prevent execution of next ValidationAttribute, if the first validation is failed?
For example, if (category "id" <= 0), then do not try to check if it exists.
Because now, when I do "PUT /api/categories/-1", I get this:
{
"id": [
"id must be greater or equal to 0",
"Entity with such Id was not found!"
]
}
Method where I want to prevent further validation:
[HttpPut("{id}")]
public ActionResult UpdateCategory([Min(0)][CategoryExists] int id, [FromBody] Category category)
{
return new OkResult();
//if (category.Id == 0) {
// return new BadRequestObjectResult("Id property is required!");
//}
//_context.Category.Update(category);
//_context.SaveChanges();
}
Min attribute
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
public class MinAttribute : ValidationAttribute
{
private int _minVal;
public MinAttribute(int minVal)
{
_minVal = minVal;
}
public override bool IsValid(object value)
{
if ((int)value >= _minVal)
{
return true;
}
else
{
return false;
}
}
public override string FormatErrorMessage(string name)
{
return $"{name} must be greater or equal to {_minVal}";
}
}
CategoryExists attribute
public class CategoryExistsAttribute : ValidationAttribute
{
public CategoryExistsAttribute()
{
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var context = (TechDbContext)validationContext.GetService(typeof(TechDbContext));
var result = from i in context.Category
where i.Id == (int)value
select i;
Category category = result.SingleOrDefault();
if (category == null)
{
return new ValidationResult("Entity with such Id was not found!");
}
return ValidationResult.Success;
}
public override string FormatErrorMessage(string name)
{
return base.FormatErrorMessage(name);
}
}
Write an extension method for the Category object, which validates all your needs.
Eg
internal static bool Validate(this Category category){
if (category.Id <= 0) {
return false;
}
// Add more validations or throw exceptions if needed.
return true;
}
So from your controller you can do this.
if(category.Validate()){
//proceed to other business logic
}
For more detailed information about data validation read this
I'm trying to get information from a demo database using a RESTful api built in ASP.NET. In order to get information from the database, I need to pass in an object trough the header. However, my API is not receiving those values. My API function is using the [FromHeader] attribute but nothing comes out. I get a 400 status with the following error message: {"AccountEmail":["The AccountEmail field is required."]}
So here is the following code:
I have a model that looks like this:
{
[Serializable]
public class Account
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Required]
public long AccountId { get; protected set; }
[Required]
[EmailAddress]
public string AccountEmail { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Required]
public Guid AccountAccessKey { get; set; }
}
}
The API call looks like
[HttpGet]
[AccountFilter]
public async Task<ActionResult<Account>> GetAccount([FromHeader] Account account) {
try
{
return await _Context.GetAccount(account.AccountEmail);
}
catch { }
return BadRequest(new Error()
{
ErrorTitle = "Unable to get Account",
ErrorMessage = "ValidationError"
});
}
Where GetAccount looks like
public Task<Account> GetAccount(string email)
{
return _context.Accounts.FirstOrDefaultAsync(x => x.AccountEmail == email);
}
The API call looks like this
let account = new Account({
AccountEmail: 'gazefekini#eaglemail.top',
AccountAccessKey:'00000000-0000-0000-0000-000000000000',
AccountId:0
})
fetch(process.env.API + '/api/Account/GetAccount', {
method:'GET',
headers: {
"Accept":"application/json",
"account": "" + account
}
})
I could use FromQuery and get the account trough the AccountId, but I have a Filter that needs the AccountAccessKey and the AccountEmail to do some AWS verification in the Filter, hence why I decided to pass an account header to the api call.
I tried removing the [Required] attribute to the AccountEmail in the model, and then the API call works, but I need to have a [Required] attribute.
Am I missing something? I understand that the Error is coming from that [Required] attribute but I'm not sure why
For your requirement, you need to implement your own model binder.
Custom HeaderComplexModelBinder
public class HeaderComplexModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var headerModel = bindingContext.HttpContext.Request.Headers[bindingContext.OriginalModelName].FirstOrDefault();
var modelType = bindingContext.ModelMetadata.ModelType;
bindingContext.Model = JsonConvert.DeserializeObject(headerModel, modelType);
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
return Task.CompletedTask;
}
}
Custom IModelBinderProvider
public class HeaderComplexModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsComplexType)
{
var x = context.Metadata as Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata;
var headerAttribute = x.Attributes.Attributes.Where(a => a.GetType() == typeof(FromHeaderAttribute)).FirstOrDefault();
if (headerAttribute != null)
{
return new BinderTypeModelBinder(typeof(HeaderComplexModelBinder));
}
else
{
return null;
}
}
else
{
return null;
}
}
}
Configure in Startup.cs
services.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new HeaderComplexModelBinderProvider());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
I have a view model:
public class SelectVendorViewModel : IValidatableObject
{
[Display(Name = "Document Date")]
[RequiredUnless("IsPidDv")]
public DateTime? DocumentDate { get; set; }
[Display(Name = "Document Number")]
[RequiredUnless("IsPidDv")]
public int? DocumentNumber { get; set; }
[Display(Name = "Vendor")]
[RequiredUnless("IsPidDv")]
public Guid? VendorId { get; set; }
public List<SelectListItem> Vendors { get; set; }
[Display(Name="PID/DV")]
public bool IsPidDv { get; set; }
public Guid? SalesReportId { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
return SelectVendorViewModelValidator.ValidateSalesReport(validationContext, this);
}
}
A custom model binder:
internal class SelectVendorViewModelBinder : DefaultModelBinder, IModelBinder<SelectVendorViewModel>
{
private readonly IVendorUnitOfWork _uow;
public SelectVendorViewModelBinder(IVendorUnitOfWork uow)
{
_uow = uow;
}
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = bindingContext.Model as SelectVendorViewModel;
if (model == null || !model.VendorId.HasValue || !model.DocumentDate.HasValue || !model.DocumentNumber.HasValue)
{
return;
}
var salesReport = _uow.SalesReportRepository.GetSalesReport(model.VendorId.Value, model.DocumentNumber.Value,
model.DocumentDate.Value);
if (salesReport != null)
{
model.SalesReportId = salesReport.Id;
}
}
}
And a validator:
internal class SelectVendorViewModelValidator
{
internal static IEnumerable<ValidationResult> ValidateSalesReport(ValidationContext validationContext, SelectVendorViewModel viewModel)
{
if (viewModel.IsPidDv)
{
yield break;
}
if (!viewModel.SalesReportId.HasValue || viewModel.SalesReportId.Value == default(Guid))
{
yield return new ValidationResult("Sales report document does not exist.");
}
}
}
And the controller action that is being posted to:
[HttpPost]
public virtual ActionResult SelectVendor(SelectVendorViewModel selectVendorVM)
{
selectVendorVM.Vendors = GetVendors();
if (!ModelState.IsValid)
{
return View(selectVendorVM);
}
return RedirectToAction(MVC.Licensing.Endorsements.Create(selectVendorVM.SalesReportId));
}
The binder is running correctly, I can step through it in the debugger. But the SelectVendorViewModel.Validate method is never called. The property validation passes, and if I set a breakpoint in the controller action ModelState.IsValid is true. I thought it might be something with the custom RequiredUnless annotation, but even when I remove them the validation doesn't work. I use this same pattern in lots of places in this app but this is the only one that doesn't work. The only difference I could find between this and the others is the RequiredUnless annotation and I was able to rule that out. What am I missing?
EDIT: Here's how I register the model binders:
Custom IModelBinderProvider:
public class GenericModelBinder : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
var genericBinder = typeof(IModelBinder<>).MakeGenericType(modelType);
var binder = DependencyResolver.Current.GetService(genericBinder) as IModelBinder;
return binder;
}
}
In Global.asax Application_Start method:
ModelBinderProviders.BinderProviders.Add(new GenericModelBinder());
And in the Ninject config:
kernel.Bind<IModelBinder<SelectVendorViewModel>>().To<SelectVendorViewModelBinder>();
Oh (cringe) you're not calling the base method of the ModelBinder which in turn calls the Validate method on the model. ;)
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = bindingContext.Model as SelectVendorViewModel;
if (model == null || !model.VendorId.HasValue || !model.DocumentDate.HasValue || !model.DocumentNumber.HasValue)
{
return;
}
var salesReport = _uow.SalesReportRepository.GetSalesReport(model.VendorId.Value, model.DocumentNumber.Value,
model.DocumentDate.Value);
if (salesReport != null)
{
model.SalesReportId = salesReport.Id;
}
// this is important as we overrode but still need base
// functionality to effect a validate
base.OnModelUpdated(controllerContext, bindingContext);
}
I have a custom ValidationAttribute, however I only want to validate this property if a CheckBox is checked.
I've made my class inherit from IValidationObject and am using the Validate method to perform any custom validation, however can I use a custom ValidationAttribute here instead of duplicating the code? And if so, how?
public class MyClass : IValidatableObject
{
public bool IsReminderChecked { get; set; }
public bool EmailAddress { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (IsReminderChecked)
{
// How can I validate the EmailAddress field using
// the Custom Validation Attribute found below?
}
}
}
// Custom Validation Attribute - used in more than one place
public class EmailValidationAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
var email = value as string;
if (string.IsNullOrEmpty(email))
return false;
try
{
var testEmail = new MailAddress(email).Address;
}
catch (FormatException)
{
return false;
}
return true;
}
}
It's possible to validate a property based on the value of another property, but there are a few hoops to jump through to make sure the validation engine works the way you expect. Simon Ince's RequiredIfAttribute has a good approach and it should be easy to modify it into a ValidateEmailIfAttribute just by adding your e-mail validation logic to the IsValid method.
For example, you could have your base validation attribute, just like you do now:
public class ValidateEmailAttribute : ValidationAttribute
{
...
}
and then define the conditional version, using Ince's approach:
public class ValidateEmailIfAttribute : ValidationAttribute, IClientValidatable
{
private ValidateEmailAttribute _innerAttribute = new ValidateEmailAttribute();
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public ValidateEmailIfAttribute(string dependentProperty, object targetValue)
{
this.DependentProperty = dependentProperty;
this.TargetValue = targetValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
// get a reference to the property this validation depends upon
var containerType = validationContext.ObjectInstance.GetType();
var field = containerType.GetProperty(this.DependentProperty);
if (field != null)
{
// get the value of the dependent property
var dependentvalue = field.GetValue(validationContext.ObjectInstance, null);
// compare the value against the target value
if ((dependentvalue == null && this.TargetValue == null) ||
(dependentvalue != null && dependentvalue.Equals(this.TargetValue)))
{
// match => means we should try validating this field
if (!_innerAttribute.IsValid(value))
// validation failed - return an error
return new ValidationResult(this.ErrorMessage, new[] { validationContext.MemberName });
}
}
return ValidationResult.Success;
}
// Client-side validation code omitted for brevity
}
Then you could just have something like:
[ValidateEmailIf("IsReminderChecked", true)]
public bool EmailAddress { get; set; }