Hide the underlying property name in Data Annotation validation error message - c#

I have this asp.net core model:
public class MyModel
{
[ModelBinder(Name = "id")]
[StringLength(36, MinimumLength = 3)]
public string ObjectId { get; set; }
}
I added the ModelBinder attribute in order to rename the "ObjectId" field to "id".
When I trying to submit the model with bad values. For example:
{
"id": "1111111111111111111111111111111111111111111111111111111111111111111111111111"
}
I'm getting back this response from the server:
{
"id":["The field ObjectId must be a string with a minimum length of 3 and a maximum length of 36."]
}
Expected output:
{
"id":["The field id must be a string with a minimum length of 3 and a maximum length of 36."]
}
That's strange because the key ("id") was written in the right case. But in the value ("ObjectId") it was written in wrong.
My client shouldn't be aware of ObjectId. He just know id. How to fix messages like these?
Thanks.

The solution is to use DisplayName attribute:
public class MyModel
{
[ModelBinder(Name = "id")]
[StringLength(36, MinimumLength = 3)]
[DisplayName("id")]
public string Id {get; set;}
}

You can set a custom error message on StringLengthAttribute Class.
public class MyModel
{
[ModelBinder(Name = "id")]
[StringLength(36, MinimumLength = 3, ErrorMessage="The field id must be a string with a minimum length of {1} and a maximum length of {2}.")]
public string ObjectId { get; set; }
}
Excerpt from documentation page:
You can use composite formatting placeholders in the error message: {0} is the name of the property; {1} is the maximum length; and {2} is the minimum length. The placeholders correspond to arguments that are passed to the String.Format method at runtime.

For StringLength, it uses the default property name for constructing the error message which is expected.
If you prefer using the Name arugment from ModelBinder, you could implement your own StringLength arribute like
public class CustomStringLength : StringLengthAttribute
{
public CustomStringLength(int maximumLength)
: base(maximumLength)
{
}
public override string FormatErrorMessage(string name)
{
return base.FormatErrorMessage(name);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var propertyName = validationContext.DisplayName;
var propertyAttribute = validationContext.ObjectType
.GetProperty(propertyName)
.GetCustomAttribute(typeof(ModelBinderAttribute));
if (propertyAttribute is ModelBinderAttribute modelBinderAttribute)
{
validationContext.DisplayName = modelBinderAttribute.Name;
}
//validationContext.DisplayName = "Id";
return base.IsValid(value, validationContext);
}
}
Then use it like
public class MyModel
{
[ModelBinder(Name = "id")]
[CustomStringLength(36, MinimumLength = 3)]
public string ObjectId { get; set; }
}

Related

C# Unable to find other properties in custom validation attribute

Trying to make my own Validation Attribute that uses a property name to find another property.
Currently, I am having problems finding the other property. It seems I am unable to find this property (or in fact any properties).
The check for property == null is always coming up as true.
Any ideas why I wouldn't be able to find properties?
This is the custom filter I have made
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var property = validationContext.ObjectInstance.GetType().GetProperty(PropertyName);
if (property == null)
{
return new ValidationResult(string.Format(
"Unknown property {0}",
new[] { PropertyName }
));
}
var propertyValue = property.GetValue(validationContext.ObjectInstance);
// Just for testing purposes.
return new ValidationResult(ErrorMessage);
}
This is the model I am using behind my razor view.
public class OrganisationDetailsModel : PageModel
{
private readonly FormStateContext _context;
public OrganisationDetailsModel(FormStateContext context)
{
_context = context;
}
[BindProperty]
[RegularExpression(pattern: "(yes|no)")]
[Required(ErrorMessage = "Please select if you are registered on companies house")]
public string CompanyHouseToggle { get; set; }
[BindProperty]
[StringLength(60, MinimumLength = 3)]
[RequiredIf("CompanyHouseToggle")]
public string CompanyNumber { get; set; }
[BindProperty]
[StringLength(60, MinimumLength = 3)]
[Required(ErrorMessage = "Enter your organisation name")]
public string OrganisationName { get; set; }
[BindProperty]
[RegularExpression(pattern: "(GB)?([0-9]{9}([0-9]{3})?|[A-Z]{2}[0-9]{3})", ErrorMessage = "This VAT number is not recognised")]
[Required(ErrorMessage = "Enter your vat number")]
public string VatNumber { get; set; }
public void OnGet()
{
}
public IActionResult OnPost()
{
if (!ModelState.IsValid)
{
return Page();
}
return RedirectToPage("ApplicantDetails");
}
I appreciate the fact that the custom validation attribute doesn't really do anything at the moment but that is becuase I have become stuck on this issue.
Thanks for any help.
Let's use the following code snippet to help explain what is going on here:
protected override ValidationResult IsValid(object value, ValidationContext ctx)
{
var typeFullName = ctx.ObjectInstance.GetType().FullName;
...
}
In this example, you might expect typeFullName to be XXX.OrganisationDetailsModel, but it isn't: it's actually System.String (the type of the property you’re trying to validate). System.String clearly does not have a property named e.g. CompanyHouseToggle and so GetProperty rightly returns null.
I've not seen many cases where [BindProperty] has been used more than once on a PageModel. It's certainly possible, but it appears that each property is treated as being individual and that the PageModel itself is not being validated.
In order to work around this, you can just turn your individual properties into a complex type and use that instead. The docs and examples use an inline class for this, inside of the PageModel class. Here's an example of an updated OrganisationDetailsModel class:
public class OrganisationDetailsModel : PageModel
{
...
[BindProperty]
public InputModel Input { get; set; }
public void OnGet() { }
public IActionResult OnPost()
{
if (!ModelState.IsValid)
return Page();
return RedirectToPage("ApplicantDetails");
}
public class InputModel
{
[RegularExpression(pattern: "(yes|no)")]
[Required(ErrorMessage = "Please select if you are registered on companies house")]
public string CompanyHouseToggle { get; set; }
[StringLength(60, MinimumLength = 3)]
[RequiredIf("CompanyHouseToggle")]
public string CompanyNumber { get; set; }
...
}
}
This includes the following changes:
Creation of an InputModel class to hold all properties.
Removal of all other properties that have now moved into InputModel.
Addition of an Input property of type InputModel, that gets bound using [BindProperty].
Removed [BindProperty] from the original properties that have now been moved.
The last step is to replace any usage of e.g. CompanyNumber with Input.CompanyNumber in the PageModel's corresponding .cshtml and to ensure you use the Input. prefix when accessing properties within the PageModel class itself.
By the doc. getProperties() only return publics.
https://learn.microsoft.com/en-us/dotnet/api/system.type.getproperties?view=netframework-4.7.2
So if want to get non-public properties. Find on belows.
Get protected property value of base class using reflections
protected override ValidationResult IsValid(object value, ValidationContext context) {
var property = context.ObjectType.getProperty(context.MemberName);
// TODO
return ValidationResult.Success;
}

Custom API ValidationAttribute for class property

I have a class Student that contains the list of property 'TextPair' as shown below:
public class Student
{
public List<TextPair> Hobbies { get; set; }
public List<TextPair> Languages { get; set; }
public List<TextPair> Majors { get; set; }
}
public class TextPair
{
[StringLength(2, ErrorMessage = "The value length is invalid")]
public string Value { get; set; }
public string Text { get; set; }
}
Here, I validate the value for maximum length 2 using the StringLength AttributeValidator and decorate in the property 'Value' inside TextPair model.
The problem for me is that the length is always fixed and length is always mandatory.
In my use case, I want the different flavor of Value in different part of the application (or, different property of same type) to support different lengths.
I was looking for something like below where I could pass the validation in my class where I declare my property 'TextPair'
[i.e. I don't want to make the validation mandatory always and also not hard-code the value 2]
public class Student
{
//Any length of the value is accepted for hobbies
public List<TextPair> Hobbies{ get; set; }
[ValuesLength(Length = 2, ErrorMessage = "Language code length must be 2 characters max")]
public List<TextPair> Languages { get; set; }
[ValuesLength(Length = 128, ErrorMessage = "The major should be within 128 characters length")]
public List<TextPair> Majors{ get; set; }
}
Is there any efficient way to approach this solution?
Maybe try subclassing StringLengthAttribute to create your desired ValuesLength attribute. Please note that this code is not tested and is just suggestion to final implementation.
public class ValuesLength : StringLengthAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var isValid = true;
var pair = value as TextPair;
if (pair != null && pair.Value != null)
{
var pairValue = pair.Value;
isValid = pairValue.Length < MaximumLength && pairValue.Length > MinimumLength;
}
return IsValid ? ValidationResult.Success : new ValidationResult(ErrorMessage);
}
}
One of the solution approached is as follows:
public class Student
{
//Any length of the value is accepted for hobbies
public List<TextPair> Hobbies{ get; set; }
[ValuesLength(MaximumLength = 2, ErrorMessage = "Language code length must be 2 characters max")]
public List<TextPair> Languages { get; set; }
[ValuesLength(MaximumLength = 128, ErrorMessage = "The major should be within 128 characters length")]
public List<TextPair> Majors{ get; set; }
}
My Custom Attribute validation is checking the list and verifying if anyone of the element values are exceeding the provided length as:
public class ValuesLengthAttribute : ValidationAttribute
{
public int MaximumLength { get; set; }
public override Boolean IsValid(object value)
{
Boolean isValid = true;
var list = value as List<TextPair>;
if (list != null && list.Count>0)
foreach (var item in list)
{
if (item.Value.Length > MaximumLength)
isValid = false;
}
return isValid;
}
}

Can I use a Model property as part of a Range validation attribute

In my model I have an object that has the following property.
[Range(typeof(int), "2014", "2024", ErrorMessage = "{0} can only be beteween {1} and {2}")]
public int FiscalYear { get; set; }
The lower and upper range values are 2014 and 2024 respectively. However, rather than use these fixed values, I'd like them to be based on another property in the model.
So, for example, if I had a property, CurrentFiscalYear, my hypothetical Range attribute would look like this.
[Range(typeof(int), CurrentFiscalYear, CurrentFiscalYear + 10, ErrorMessage = "{0} can only be beteween {1} and {2}")]
public int FiscalYear { get; set; }
Is something like this possible? Or must the lower and upper values be provided at compile time?
No, this isn't possible. Attribute parameter values just be "compile-time constant" values. In other words, the actual value of the parameter must be known when you compile the program.
From MSDN - Attributes tutorial:
Attribute parameters are restricted to constant values of the following types:
Simple types (bool, byte, char, short, int, long, float, and double)
string
System.Type
enums
object (The argument to an attribute parameter of type object must be a constant value of one of the above types.)
One-dimensional arrays of any of the above types
This is documentation for .NET 1.1, but has not changed.
Workaround
This isn't tested at all but you can create a custom ValidationAttribute which takes the range and also model property names who's values to add to the range values when testing for validity. You can create an internal standard RangeAttribute to do the work for you and even keep client validation working by implementing IClientValidatable:
public sealed class ShiftedRangeAttribute : ValidationAttribute
{
public string MinShiftProperty { get; private set; }
public string MaxShiftProperty { get; private set; }
public double Minimum { get; private set; }
public double Maximum { get; private set; }
public ShiftedRangeAttribute(double minimum, double maximum, string minShiftProperty, string maxShiftProperty)
{
this.Minimum = minimum;
this.Maximum = maximum;
this.MinShiftProperty = minShiftProperty;
this.MaxShiftProperty = maxShiftProperty;
}
public ShiftedRangeAttribute(int minimum, int maximum, string minShiftProperty, string maxShiftProperty)
{
this.Minimum = minimum;
this.Maximum = maximum;
this.MinShiftProperty = minShiftProperty;
this.MaxShiftProperty = maxShiftProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
RangeAttribute attr = this.CreateRangeAttribute(validationContext.ObjectInstance);
return attr.GetValidationResult(value, validationContext);
}
internal RangeAttribute CreateRangeAttribute(object model)
{
double min = this.Minimum;
if (this.MinShiftProperty != null)
{
min += Convert.ToDouble(model.GetType().GetProperty(this.MinShiftProperty).GetValue(model));
}
double max = this.Maximum;
if (this.MaxShiftProperty != null)
{
max += Convert.ToDouble(model.GetType().GetProperty(this.MaxShiftProperty).GetValue(model));
}
return new RangeAttribute(min, max);
}
}
If you want it to work with client validation, you will also to create a DataAnnotationsModelValidator and register it in your global.asax Application_Start() to ensure the client validation HTML attributes are output. Again you can cheat and use the built-in RangeAttributeAdapter to help you because in Javascript it is ultimately just a range validator:
public class ShiftedRangeAttributeAdapter : DataAnnotationsModelValidator<ShiftedRangeAttribute>
{
public ShiftedRangeAttributeAdapter(ModelMetadata metadata, ControllerContext context, ShiftedRangeAttribute attribute)
: base(metadata, context, attribute)
{
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
RangeAttribute attr = this.Attribute.CreateRangeAttribute(this.Metadata.Container);
return new RangeAttributeAdapter(this.Metadata, this.ControllerContext, attr).GetClientValidationRules();
}
}
...
DataAnnotationsModelValidatorProvider.RegisterAdapter(
typeof(ShiftedRangeAttribute), typeof(ShiftedRangeAttributeAdapter));
Note that the client validation code only works if the class containing the properties is the top-level model class, which is stored in Metadata.Container. You cannot access the "parent" of the current property. You would need to do more work to create a custom jQuery validator to handle this properly.
You can then use it as so:
[ShiftedRange(0, 10, "CurrentFiscalYear", "CurrentFiscalYear", ErrorMessage = "{0} can only be beteween {1} and {2}")]
public int FiscalYear { get; set; }
EDIT: fixed some bugs after testing
This can be done by writing a custom ValidationAttribute, implementation could be done something like this:
public sealed class FiscalYearAttribute : ValidationAttribute
{
public string CurrentFiscalYear { get; set; }
public override bool IsValid(object value)
{
var currentFiscalYearString = HttpContext.Current.Request[CurrentFiscalYear];
var currentFiscalYear = int.Parse(currentFiscalYearString);
var fiscalYear = (int) value;
return fiscalYear >= currentFiscalYear && fiscalYear <= currentFiscalYear + 10;
}
public override string FormatErrorMessage(string name)
{
return name + " error description here.";
}
}
Usage:
[Required]
[Display(Name = "CurrentFiscalYear")]
public int CurrentFiscalYear { get; set; }
[Display(Name = "FiscalYear")]
[FiscalYear(CurrentFiscalYear = "CurrentFiscalYear")]
public int FiscalYear { get; set; }

Using parameter for StringLength attribute

The code which is generally generated for a ASP.NET MVC 3 Membership, escpecially the property NewPassword of the class ChangePasswordModel looks roughly like:
[Required]
[StringLength(100, MinimumLength=6)]
[DataType(DataType.Password)]
[Display("Name = CurrentPassword")]
public string NewPassword { get; set; }
In order to to fill some information with external parameters I am using RecourceType:
(In this case I am changing OldPassword and fill the attribute Display with some additional Data from a Resource
[Required]
[DataType(DataType.Password)]
[Display(ResourceType = typeof(Account), Name = "ChangePasswordCurrent")]
public string OldPassword { get; set; }
Back to NewPassword. How can I substitute the MinimumLenght with Membership.MinRequiredPasswordLength? : My attempt:
[Required]
[StringLength(100, MinimumLength=Membership.MinRequiredPasswordLength)]
[DataType(DataType.Password)]
[Display(ResourceType=typeof(Account), Name= "ChangePasswordNew")]
public string NewPassword { get; set; }
This produces the error:
An attribute argument must be a constant expression, typeof expression
or array creation expression of an attribute parameter type
(http://msdn.microsoft.com/de-de/library/09ze6t76%28v=vs.90%29.aspx)
Validation attributes must be compiled constants (like your error message states). You could create you own ValidationAttribute that handles this minimum length for you.
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class ValidatePasswordLengthAttribute : ValidationAttribute
{
private const string DefaultErrorMessage = "'{0}' must be at least {1} characters long.";
private readonly int _minCharacters = Membership.Provider.MinRequiredPasswordLength;
public ValidatePasswordLengthAttribute() : base(DefaultErrorMessage)
{
}
public override string FormatErrorMessage(string name)
{
return string.Format(CultureInfo.CurrentUICulture, ErrorMessageString, name, _minCharacters);
}
public override bool IsValid(object value)
{
var valueAsString = value.ToString();
return (valueAsString != null) && (valueAsString.Length >= _minCharacters);
}
}
Then your view model could look like this (you could even get more fancy and add the max length part of your DataAnnotations in the ValidatePasswordLength attribute to remove that line)
[Required]
[StringLength(100)]
[DataType(DataType.Password)]
[Display(ResourceType=typeof(Account), Name= "ChangePasswordNew")]
[ValidatePasswordLength]
public string NewPassword { get; set; }

ObjectValidator unit testing

I'm having problems with [ObjectValidator]. So, i have:
public class UserBO
{
public int ID
{
get;
set;
}
[NotNullValidator(MessageTemplate = "Can't be null!")]
[RegexValidator(#"[a-z]|[A-Z]|[0-9]*", MessageTemplate = "Must be valid!", Ruleset = "validate_username")]
[StringLengthValidator(5, RangeBoundaryType.Inclusive, 25, RangeBoundaryType.Inclusive, Ruleset = "validate_username")]
public string username
{
get;
set;
}
and another class:
public class PersonBO
{
public int ID
{
get;
set;
}
[NotNullValidator(MessageTemplate="Can't be null!")]
[ObjectValidator(MessageTemplate = "Must be valid!", Ruleset="validate_obj_user")]
public UserBO User
{
get;
set;
}
...
Now can you tell me why the following test passes?
[TestMethod()]
public void PersonBOConstructorTest()
{
PersonBO target = new PersonBO()
{
User = new UserBO
{
ID = 4,
username = "asd"
}
};
ValidationResults vr = Validation.Validate<PersonBO>(target, "validate_obj_user");
Assert.IsTrue(vr.IsValid);
}
This should not be valid, because: User attribute (of UserBO type) contains username "asd" (3 characters), and i defined for it a StringLengthValidator (between 5 and 25 characters).. so why the test passes? that object is not valid
I can't understand.
Thanks.
You have to specify a ruleset in your ObjectValidator if you want rules from a set other than the default set applied.
[ObjectValidator("validate_username", MessageTemplate = "Must be valid!", Ruleset = "validate_obj_user")]
The above should work in this specific case. Alternatively, you could remove the ruleset parameter from the string length validator to leave it in the default set.

Categories