Monojit Sarkar
Monojit Sarkar

Reputation: 2451

ASP.Net MVC: How to add validation to any model property on the fly

i am curios to know how could i add validation to any property of my model at run time without touching the code. so looking for guide line how to achieve what i am after in asp.net mvc.

sample model property

[Required(ErrorMessage = "The field {0} is required")]
[Display(Name = "Company")]
public string Name { get; set; }

the above code is a sample where validation has been added when coding but i do not want to attach validation at coding time rather i want to code in such a way as a result at the run time i will be able to inject validation to any model property.

i search a google and found a sample but do not understand how to use it to achieve my task. here is a sample code what i found but not clear how to use it for my purpose.

in Model:

[Domainabcd(20, "Bar", "Baz", ErrorMessage = "The combined minimum length of the Foo, Bar and Baz properties should be longer than 20")]
public string strAccessionId { get; set; }

in DomainabcdAttribute.cs:

public class DomainabcdAttribute : ValidationAttribute, IClientValidatable
 //Domain is the Attribute name 
//if you do not inherit IClientValidatable ,the server validation still will work ,but the Client validation will not work.

    {
        public DomainAttribute(int minLength, params string[] propertyNames) 
// this is where to get parameters from you pass 
//such as : 20, "Bar", "Baz",
        {
            this.PropertyNames = propertyNames;
            this.MinLength = minLength;
        }

        public string[] PropertyNames { get; private set; }
        public int MinLength { get; private set; }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
           ///this is where to decide if to display error ,or not.
            //var properties = this.PropertyNames.Select(validationContext.ObjectType.GetProperty);
            //var values = properties.Select(p => p.GetValue(validationContext.ObjectInstance, null)).OfType<string>();
            //var totalLength = values.Sum(x => x.Length) + Convert.ToString(value).Length;
            //if (totalLength < this.MinLength)
            //{
               //this is the place to return the validation result
             //so you may just use this line code:   return new ValidationResult(validationContext.DisplayName)
                return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
            //}
            //return null; //no error massage

       }
        //this is for Client to display error message 
        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var modelClientValidationRule = new ModelClientValidationRule
            {
                ValidationType = "requiredif",
                ErrorMessage = ErrorMessage //Added ,pass the error message 

            };

            modelClientValidationRule.ValidationParameters.Add("param", this.PropertyNames); //Added
            return new List<ModelClientValidationRule> { modelClientValidationRule };

        }
        // public override string FormatErrorMessage(string name) //to custom format for error message
        //{
        //    string[] values = this.Values.Select(value => string.Format("'{0}'", value)).ToArray();
        //     //return string.Format("{0}{1}", name, string.Join(",", values));
        //     return string.Format(base.ErrorMessageString, name, string.Join(",", values));
        // }
    }

my objective as follows

1) suppose first day i will add validation to name property at run time like name should be required

2) after few days i will add another validation to name property on the fly suppose name should not have special character like @ , : # etc.

3) again after few days i will add another validation to name property on the fly suppose name's length should not be less than 10 character.

hope i am clear what i am trying to achieve. so please anyone who did it just help me with sample code.

thanks

Upvotes: 1

Views: 3077

Answers (2)

MonarchL
MonarchL

Reputation: 131

Sure you can. E.g. you could validate your object using regular expressions and store them in a web.config.

Sample (this code is just a quick example and should not be used on production):

Modify your web.config to store validator's rules. Add this to <configuration></configuration> section:

<configSections>
    <sectionGroup name="CustomConfig">
    <section name="UniversalValidatorConfig" type="System.Configuration.NameValueSectionHandler" />
    </sectionGroup>
</configSections>

<CustomConfig>
    <UniversalValidatorConfig>
        <add key="EMail" value="^[a-zA-Z0-9_.-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$"/>
        <add key="CurrencyIsoCode" value="^([a-zA-Z]{3})$" />
    </UniversalValidatorConfig>
</CustomConfig>

Here we have added validation for EMail and CurrencyIsoCode properties. To apply validations you should create attribute like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;

using System.Configuration;
using System.Collections.Specialized;
using System.Text.RegularExpressions;

namespace WebApplication1.Attributes
{
    [AttributeUsage(AttributeTargets.Class)]
    public class UniversalValidatorAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(Object value, ValidationContext validationContext)
        {
            if (value == null)
                return ValidationResult.Success;

            var validatorConfig = (NameValueCollection)ConfigurationManager.GetSection("CustomConfig/UniversalValidatorConfig");
            var validationRules = validatorConfig.AllKeys
                .Select(key => new { PropertyName = key, ValidationRule = validatorConfig.GetValues(key).FirstOrDefault() })
                .ToList();

            var errorMessages = new List<string>(validationRules.Count);

            foreach (var validationRule in validationRules)
            {
                var property = value.GetType().GetProperty(validationRule.PropertyName);
                if (property == null) continue;

                var propValue = property.GetValue(value)?.ToString();
                var regEx = new Regex(validationRule.ValidationRule, (RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline) | RegexOptions.IgnoreCase);
                var isValid = regEx.IsMatch(propValue);

                if (!isValid)
                    errorMessages.Add(FormatErrorMessage(validationRule.PropertyName));
            }


            if (errorMessages.Any())
                return new ValidationResult(string.Join("\r\n", errorMessages));

            return ValidationResult.Success;
        }
    }
}

Create a class that will be used as input parameter for your action and mark that class with UniversalValidatorAttribute:

using WebApplication1.Attributes;

namespace WebApplication1.ActionParameters
{
    [UniversalValidator]
    public class IndexParameters
    {
        public string EMail { get; set; }
        public string CurrencyIsoCode { get; set; }
    }
}

Now we can use parameters in some action. E.g. modify Home/Index action to consume IndexParameters:

public ActionResult Index(IndexParameters parameters)
{
    if (!ModelState.IsValid)
    {
        foreach(var error in ModelState.Where(ms => ms.Value.Errors.Any()).SelectMany(ms => ms.Value.Errors))
            Debug.WriteLine(error.ErrorMessage);
    }

    return View();
}

To test Validator we can use such URLs (first one will contain two validation errors, second one is valid):

http://localhost:9316/Home/Index?email=admini.net&currencyIsoCode=U http://localhost:9316/Home/[email protected]&currencyIsoCode=USD

That will allow you change validation rules on-the-fly in web.config


Example with custom rules:

web.config:

<CustomValidatorConfig>
  <add key="FirstName" value="LENGTHGREATER(10)" />
  <add key="BirthDate" value="DATEBETWEEN(01.01.2017, 01.02.2017)" />
</CustomValidatorConfig>

Validator attribute:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using System.ComponentModel.DataAnnotations;
using System.Configuration;
using System.Collections.Specialized;
using System.Text.RegularExpressions;

namespace WebApplication1.Attributes
{
    public class CustomValidatorAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(Object value, ValidationContext validationContext)
        {
            if (value == null)
                return ValidationResult.Success;

            var validatorConfig = (NameValueCollection)ConfigurationManager.GetSection("CustomConfig/CustomValidatorConfig");
            var validationRules = validatorConfig.AllKeys
                .Select(key => new { PropertyName = key, ValidationRule = validatorConfig.GetValues(key).FirstOrDefault() })
                .ToList();

            var errorMessages = new List<string>(validationRules.Count);

            foreach (var validationRule in validationRules)
            {
                var property = value.GetType().GetProperty(validationRule.PropertyName);
                if (property == null) continue;

                var propValue = property.GetValue(value);
                var isValid = CustomValidator.IsValid(propValue, validationRule.ValidationRule);

                if (!isValid)
                    errorMessages.Add(FormatErrorMessage(validationRule.PropertyName));
            }


            if (errorMessages.Any())
                return new ValidationResult(string.Join("\r\n", errorMessages));

            return ValidationResult.Success;
        }
    }
}

Attribute applied to model

[CustomValidator]
public class TestValidatorParameters
{
    public string FirstName { get; set; }
    public DateTime BirthDate { get; set; }
}

Action that use Attribute:

public ActionResult TestValidator(TestValidatorParameters parameters)
{
    if (!ModelState.IsValid)
    {
        foreach (var error in ModelState.Where(ms => ms.Value.Errors.Any()).SelectMany(ms => ms.Value.Errors))
            Debug.WriteLine(error.ErrorMessage);

        return Json("Validation failed", JsonRequestBehavior.AllowGet);
    }

    return Json("Validation passed", JsonRequestBehavior.AllowGet);
}

URLs to test (first - validation is ok, second - validation is failed):

http://localhost:9316/Home/TestValidator?FirstName=KowalskiPinguin&BirthDate=01.01.2017 http://localhost:9316/Home/TestValidator?FirstName=Kowalski&BirthDate=01.01.2016

Upvotes: 1

Ionut Ungureanu
Ionut Ungureanu

Reputation: 1030

One possible solution will be to implement on your model IValidatableObject interface (https://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.ivalidatableobject(v=vs.110).aspx) and in the Validate method create your validation messages using Fluent Validation (https://github.com/JeremySkinner/FluentValidation) or similar mechanism/framework. That will still require compilation/distribution of the changes in order to see the effect. In order to avoid that, you could implement an interpreter pattern to create, in the Validate, the validation rules to execute at run-time.

Upvotes: 1

Related Questions