C# MVC 3: Prevent magic string in property attribute - c#

I found a RequiredIfAttribute on the internet which I modified to RequiredNotIf. The attribute can be used like this.
[RequiredNotIf("LastName", null, ErrorMessage = "You must fill this.")]
public string FirstName { get; set; }
[RequiredNotIf("FirstName", null, ErrorMessage = "You must fill this")]
public string LastName { get; set; }
And the source code to the attribute...
[AttributeUsageAttribute(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = true)]
public class RequiredNotIfAttribute : RequiredAttribute, IClientValidatable
{
private string OtherProperty { get; set; }
private object Condition { get; set; }
public RequiredNotIfAttribute(string otherProperty, object condition)
{
OtherProperty = otherProperty;
Condition = condition;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(OtherProperty);
if (property == null)
{
return new ValidationResult(String.Format("Property {0} not found.", OtherProperty));
}
var propertyValue = property.GetValue(validationContext.ObjectInstance, null);
var conditionIsMet = !Equals(propertyValue, Condition);
return conditionIsMet ? base.IsValid(value, validationContext) : null;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
ValidationType = "requiredif",
};
var depProp = BuildDependentPropertyId(metadata, context as ViewContext);
var targetValue = (Condition ?? "").ToString();
if (Condition != null && Condition is bool)
{
targetValue = targetValue.ToLower();
}
rule.ValidationParameters.Add("otherproperty", depProp);
rule.ValidationParameters.Add("condition", targetValue);
yield return rule;
}
private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext)
{
var depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(OtherProperty);
var thisField = metadata.PropertyName + "_";
if (depProp.StartsWith(thisField))
{
depProp = depProp.Substring(thisField.Length);
}
return depProp;
}
}
The drawback with this - as I see it - is the magic string in the attribute "header". How can I get rid of it?

You can't get rid of it because attributes are metadata and values must be known at compile time. If you want to do more advanced validation without magic strings I would very strongly recommend you FluentValidation.NET. Performing validation with attributes in a declarative manner is very limiting IMHO. Just look at the quantity of source code you have to write for something as standard and easy as RequiredIf or RequiredNotIf. I don't know what the designers of the framework were thinking when they choose Data Annotations for validation. It's just ridiculous. Maybe in the future they will enrich it and allow for more complex scenarios but until then I stick with FV.

Assuming you mean the other property name; you can't. Attributes can only use a limited number of parameter types - Expression is not one of them, so you can't use the lambda trick (not that it would be a good idea anyway). And C# does not have a infoof / memberof operator. So all you have is things like strings.
Well, I guess you could add a non-magic key (i.e. one that doesn't tie directly to a member-name), but it seems massive overkill, for example:
// NOT a recommendation
[RequiredNotIf(1, ...)]
public string Foo {get;set;}
[SomeKey(1)]
public string Bar {get;set;}
This has removed the member-name, but still has a dependency on the key (1) resolving to another attributed member, and is more complex. It is also less clear, i.e. on a non-trivial class you might have to scan up and down to see which other member has the matching tag. I'm not a fan ;p

Related

how to change type validation error messages?

I'm using entity framework code first in an ASP MVC project, and I'd like to change the error message that appears for validation of a numeric type.
I have a property like
public decimal Amount1 { get; set; }
If I enter a non-number in the field, I get the message: The field Amount1 must be a number. How do I change that message?
For other validations, like Required I can just use the ErrorMessage parameter like: [Required(ErrorMessage = "My message...")]
Is there something similar for validating types?
Thank you.
Unfortunately Microsoft didn't expose any interfaces to change the default messages.
But if you are desperate enough to change these non friendly messages, you can do so by creating validation attribute for decimal, creating corresponding validator and finally register it with DataAnnotationsModelValidatorProvider at the application startup. Hope this helps.
UPDATE:
Sample below
Step 1: Create validation attribute
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false)]
public class ValidDecimalAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
if (value == null || value.ToString().Length == 0) {
return ValidationResult.Success;
}
decimal d;
return !decimal.TryParse(value.ToString(), out d) ? new ValidationResult(ErrorMessage) : ValidationResult.Success;
}
}
Step 2: Create validator
public class ValidDecimalValidator : DataAnnotationsModelValidator<ValidDecimal>
{
public ValidDecimalValidator(ModelMetadata metadata, ControllerContext context, ValidDecimal attribute)
: base(metadata, context, attribute)
{
if (!attribute.IsValid(context.HttpContext.Request.Form[metadata.PropertyName]))
{
var propertyName = metadata.PropertyName;
context.Controller.ViewData.ModelState[propertyName].Errors.Clear();
context.Controller.ViewData.ModelState[propertyName].Errors.Add(attribute.ErrorMessage);
}
}
}
Step 3: Register the adapter in Global.asax under Application_Start() method or Main() method
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(ValidDecimal), typeof(ValidDecimalValidator));
Step 4: Finally decorate your property in your model with this attribute
[ValidDecimal(ErrorMessage = "Only decimal numbers allowed")]
public decimal CPEHours { get; set; }
Hope it helps.
I couldn't find a clean solution. If there is something like [Required] you could override it in the same way. Only option I find is to remove and add another error into the model state. Again NOT the best option if you have better alternates, but does the job. This example only works if you have something like must be a number at the end. You can create a filter with this kind of loop:
foreach (var m in ModelState)
{
var errors = m.Value.Errors;
foreach (var error in errors)
{
if (error.ErrorMessage.EndsWith("must be a number"))
{
errors.Remove(error);
ModelState.AddModelError(m.Key, $"This is my own validation");
}
}
}
While it's not possible to change the whole message, you can at least change the string used to reference the field. Use the [Display(Name = "amount field"] attribute, like:
[BindProperty]
[Display(Name = "line length")]
public decimal? LineLength { get; set; }
If the user enters a string into a field like this, they will at least see an error message that reads "The value 'sdf' is not valid for line length."
Not a complete solution, but good enough in many scenarios.

Validator ignoring MaxLength attributes

Problem:
I am trying to manually Validate some c# objects, and the Validator is ignoring string length related validations.
Test Case:
extending this example which uses the [Required] attribute, i also wanted to validate that strings were not too long, as follows.
public class Recipe
{
//[Required]
public string Name { get; set; }
[MaxLength(1)] public string difficulty = "a_string_that_is_too_long";
}
public static void Main(string[] args)
{
var recipe = new Recipe();
var context = new ValidationContext(recipe, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(recipe, context, results);
if (!isValid)
{
foreach (var validationResult in results)
{
Console.WriteLine(validationResult.ErrorMessage);
}
} else {
Console.WriteLine("is valid");
}
}
Expected result: an error: "difficulty is too long."
Actual result: 'is valid'
other things tested:
the validator is working, uncommenting the [Required] results in the message "The Name field is required."
using [StringLength] instead (as noted
at https://stackoverflow.com/a/6802739/432976 ) made no difference.
You need to make 2 changes to have the validation work the way you expect:
1. You have to change the difficulty field to a property.
The Validator class only validates properties, so change the difficulty definition to a property like this:
[MaxLength(1)] public string difficulty { get; set; } = "a_string_that_is_too_long";
2. Specify the validateAllProperties: true parameter to the Validator.TryValidateObject call.
The documentation for Validator.TryValidateObject is not very forthcoming about the fact that, unless you use the overload with validateAllProperties: true, only the Required attribute will be checked. So modify the call like this:
var isValid = Validator.TryValidateObject(recipe,
context,
results,
validateAllProperties: true);

Either Or Required Validation

I want to use ComponentModel DataAnnotations validate that at least one of two properties has a value. My model looks like this:
public class FooModel {
public string Bar1 { get; set; }
public int Bar2 { get; set; }
}
Basically, I want to validate FooModel so that either Bar1 or Bar2 is required. In other words, you can enter one, or the other, or both, but you can't just leave them both empty.
I would prefer that this worked both for server-side and unobtrusive client-side validation.
EDIT: This may be a possible duplicate, as this looks similar to what I'm looking to do
You would need to extend the ValidationAttribute class and over ride the IsValid method, and implement the IClientValidatable if you want to pump custom JavaScript to do the validation. something like below.
[AttributeUsage(AttributeTargets.Property)]
public sealed class AtLeastOneOrTwoParamsHasValue : ValidationAttribute, IClientValidatable
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var param1 = validationContext.ObjectInstance.GetType().GetProperty("Param1").GetValue(value, null);
//var param2 = validationContext.ObjectInstance.GetType().GetProperty("Param2").GetValue(value, null);
//DO Compare logic here.
if (!string.IsNullOrEmpty(Convert.ToString(param1)))
{
return ValidationResult.Success;
}
return new ValidationResult("Some Error");
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
//Do custom client side validation hook up
yield return new ModelClientValidationRule
{
ErrorMessage = FormatErrorMessage(metadata.DisplayName),
ValidationType = "validParam"
};
}
}
Usage:
[AtLeastOneOrTwoParamsHasValue(ErrorMessage="Atleast one param must be specified.")]
This worked for me, a simple solution, just using .net without any third party:
https://stackoverflow.com/a/69621414/6742644
Like this:
public class EditModel
{
public string ISBN { get; set; }
public string ISBN13 { get; set; }
[RegularExpression("True|true", ErrorMessage = "At least one field must be given a value")]
public bool Any => ISBN != null || ISBN13 != null;
}
Also good to know is that you can add any attributes to the properties in the model, like MinLength, MaxLength, etc. Just do not add the Required attribute.

Validation of Guid

I have a strongly-typed view which has a DropDownListFor attribute on it.
Each item in the dropdown list is represented by a GUID.
What I'm after is a way to validate if a user selects an item from the dropdown list. At present i don't see anyway of doing this using Data Annotations.
Is there anyway of achieving this using Data Annotations so client and server side validation would work.
I'm guessing i need to make a custom method to do this but was wondering if anything already existed.
Actually, you can't use Required attribute with GUIDs (without the method I mention below) because they inherit from struct, and as such their default value is actually an instance of Guid.Empty, which will satisfy the requirements of the Required attribute. Now that being said, it is possible to get what you want you just need to make your property nullable, take this for example...
public class Person
{
[Required] //Only works because the Guid is nullable
public Guid? PersonId { get; set;}
public string FirstName { get; set;}
public string LastName { get; set;}
}
By marking the GUID nullable (using the ?, or Nullable if you prefer the long way) you let it stay as null when binding against what the browser sent. In your case, just make sure the value of the default option of the dropdown uses an empty string as it's value.
EDIT: The only caveat to this method is you end up having to use something like Person.GetValueOfDefault() everywhere and potentially testing for Guid.Empty. I got tired of doing this and ended up creating my own validation attribute to help simplify validating Guids (and any other types that have default values I want to treat as invalid such as int, DateTime, etc). However I don't have client side validation to go along with this yet, so validation only happens on the server. This can be combined with [Required] (designed to not duplicate functionality of [Required]) if you're ok with using nullable types. This would mean you still have to use GetValueOrDefault(), but at least then you don't have to test for Guid.Empty anymore. The Gist link has some XMLDocs with examples, I left them out here for brevity. I'm currently using it with ASP.NET Core.
EDIT: Updated to fix a bug with Nullable<>, and a bug with treating null as invalid. Added supporting classes to handle client side validation. See Gist for full code.
Gist: RequireNonDefaultAttribute
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class RequireNonDefaultAttribute : ValidationAttribute
{
public RequireNonDefaultAttribute()
: base("The {0} field requires a non-default value.")
{
}
public override bool IsValid(object value)
{
if (value is null)
return true; //You can flip this if you want. I wanted leave the responsability of null to RequiredAttribute
var type = value.GetType();
return !Equals(value, Activator.CreateInstance(Nullable.GetUnderlyingType(type) ?? type));
}
}
Edited Answer
Upon re-reading your question, it sounds like you just want to know if a value is selected. If that's the case then just apply the RequiredAttribute to the Guid property and make it nullable on the model
public class GuidModel
{
[Required]
public Guid? Guid { get; set; }
public IEnumerable<Guid> Guids { get; set; }
}
then in the strongly typed View (with #model GuidModel)
#Html.ValidationMessageFor(m => m.Guid)
#Html.DropDownListFor(
m => m.Guid,
Model.Guids.Select(g => new SelectListItem {Text = g.ToString(), Value = g.ToString()}),
"-- Select Guid --")
Add the client validation JavaScript script references for client-side validation.
The controller looks like
public class GuidsController : Controller
{
public GuidRepository GuidRepo { get; private set; }
public GuidsController(GuidRepository guidRepo)
{
GuidRepo = guidRepo;
}
[HttpGet]
public ActionResult Edit(int id)
{
var guid = GuidRepo.GetForId(id);
var guids - GuidRepo.All();
return View(new GuidModel { Guid = guid, Guids = guids });
}
[HttpPost]
public ActionResult Edit(GuidModel model)
{
if (!ModelState.IsValid)
{
model.Guids = GuidRepo.All();
return View(model);
}
/* update db */
return RedirectToAction("Edit");
}
}
This will ensure that the Guid property is required for a model-bound GuidModel.
Original Answer
I don't believe that there is a ready made Data Annotation Validation attribute that is capable of doing this. I wrote a blog post about one way to achieve this; the post is using an IoC container but you could take the hard coded dependency if you're wanting to get something working.
Something like
public class ValidGuidAttribute : ValidationAttribute
{
private const string DefaultErrorMessage = "'{0}' does not contain a valid guid";
public ValidGuidAttribute() : base(DefaultErrorMessage)
{
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var input = Convert.ToString(value, CultureInfo.CurrentCulture);
// let the Required attribute take care of this validation
if (string.IsNullOrWhiteSpace(input))
{
return null;
}
// get all of your guids (assume a repo is being used)
var guids = new GuidRepository().AllGuids();
Guid guid;
if (!Guid.TryParse(input, out guid))
{
// not a validstring representation of a guid
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
// is the passed guid one we know about?
return guids.Any(g => g == guid) ?
new ValidationResult(FormatErrorMessage(validationContext.DisplayName)) : null;
}
}
and then on the model you send into the controller action
public class GuidModel
{
[ValidGuid]
public Guid guid { get; set; }
}
This gives you server side validation. You could write client side validation to do this as well, perhaps using RemoteAttribute but I don't see a lot of value in this case as the only people that are going to see this client side validation are people that are messing with values in the DOM; it would be of no benefit to your normal user.
I know this is an old question now, but if anyone else is interested I managed to get around this by creating an [IsNotEmpty] annotation (making the Guid nullable wasn't an option in my case).
This uses reflection to work out whether there's an implementation of Empty on the property, and if so compares it.
public class IsNotEmptyAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
if (value == null) return false;
var valueType = value.GetType();
var emptyField = valueType.GetField("Empty");
if (emptyField == null) return true;
var emptyValue = emptyField.GetValue(null);
return !value.Equals(emptyValue);
}
}
Regex actually does work (if you use the right one!)
[Required]
[RegularExpression("^((?!00000000-0000-0000-0000-000000000000).)*$", ErrorMessage = "Cannot use default Guid")]
public Guid Id { get; set; }
Non Empty Guid Validator
prevents 00000000-0000-0000-0000-000000000000
Attribute:
using System.ComponentModel.DataAnnotations;
[AttributeUsage(AttributeTargets.Property)]
internal class NonEmptyGuidAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if ((value is Guid) && Guid.Empty == (Guid)value)
{
return new ValidationResult("Guid cannot be empty.");
}
return null;
}
}
Model:
using System.ComponentModel.DataAnnotations;
public class Material
{
[Required]
[NonEmptyGuid]
public Guid Guid { get; set; }
}
If the custom validation doesn't require a high reuse in your system (i.e. without the need for a custom validation attribute), there's another way to add custom validation to a ViewModel / Posted data model, viz by using IValidatableObject.
Each error can be bound to one or more model properties, so this approach still works with e.g. Unobtrusive validation in MVC Razor.
Here's how to check a Guid for default (C# 7.1):
public class MyModel : IValidatableObject // Implement IValidatableObject
{
[Required]
public string Name {get; set;}
public Guid SomeGuid {get; set;}
... other properties here
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (SomeGuid == default)
{
yield return new ValidationResult(
"SomeGuid must be provided",
new[] { nameof(SomeGuid) });
}
}
}
More on IValidatableObject here
You can validate the Guid if it contains default values - "00000000-0000-0000-0000-000000000000".
if (model.Id == Guid.Empty)
{
// TODO: handle the error or do something else
}
You can create a custom validator for that.
using System;
using System.ComponentModel.DataAnnotations;
namespace {{Your_App_Name}}.Pages
{
public class NotEmptyGuidAttribute: ValidationAttribute
{
protected override ValidationResult IsValid(object guidValue, ValidationContext validationContext)
{
var emptyGuid = new Guid();
var guid = new Guid(guidValue.ToString());
if (guid != emptyGuid){
return null;
}
return new ValidationResult(ErrorMessage, new[] {validationContext.MemberName});
}
}
}
You can use it like this
[EmptyGuidValidator(ErrorMessage = "Role is required.")]
public Guid MyGuid{ get; set; }
This worked for me.

DataAnnotation with custom ResourceProvider

I have created a custom ResourceProvider to pull localization information from a database. I now want to use DataAnnotation to add validation to the model.
DataAnnotation has ErrorMessageResourceType and ErrorMessageResourceName properties but ErrorMessageResourceType only accepts System.Type (i.e. a compiled resource file)
Is there any way to get DataAnnotation to use the custom ResourceProvider?
I realize this is an old question, but wanted to add a bit. I found myself in the same situation and there doesn't appear to be any documentation/blogumentation on this topic. Nevertheless, I figured out a way to use a custom resource provider, with one caveat. The caveat is that I'm in an MVC application so I still have HttpContext.GetLocalResourceObject() available. This is the method that asp.net uses to localize items. The absence of the resource object doesn't stop you from writing our own solution, even if its a direct query of the DB tables. Nevertheless, I thought it was worth pointing out.
While I'm not terribly happy with the following solution, it seems to work. For each validation attribute I want to use I inherit from said attribute and overload the IsValid(). The decoration looks like this:
[RequiredLocalized(ErrorMessageResourceType= typeof(ClassBeginValidated), ErrorMessageResourceName="Errors.GenderRequired")]
public string FirstName { get; set; }
The new attribute looks like this:
public sealed class RequiredLocalized : RequiredAttribute {
public override bool IsValid(object value) {
if ( ! (ErrorMessageResourceType == null || String.IsNullOrWhiteSpace(ErrorMessageResourceName) ) ) {
this.ErrorMessage = MVC_HtmlHelpers.Localize(this.ErrorMessageResourceType, this.ErrorMessageResourceName);
this.ErrorMessageResourceType = null;
this.ErrorMessageResourceName = null;
}
return base.IsValid(value);
}
}
Notes
You need to decorate your code with the derived attribute, not the standard one
I'm using ErrorMessageResourceType to pass the type of the class being validated. By that I mean if I'm in a customer class and validating the FirstName property I would pass typeof(customer). I'm doing this because in my database backend I'm using the full class name (namespace + classname) as a key (the same way a page URL is used in asp.net).
MVC_HtmlHelpers.Localize is just a simple wrapper for my custom resource provider
The (semi-stolen) helper code looks like this ....
public static string Localize (System.Type theType, string resourceKey) {
return Localize (theType, resourceKey, null);
}
public static string Localize (System.Type theType, string resourceKey, params object[] args) {
string resource = (HttpContext.GetLocalResourceObject(theType.FullName, resourceKey) ?? string.Empty).ToString();
return mergeTokens(resource, args);
}
private static string mergeTokens(string resource, object[] args) {
if (resource != null && args != null && args.Length > 0) {
return string.Format(resource, args);
} else {
return resource;
}
}
I have used fluent validation to achieve this. It saves me lots of time. This is what my Globalized validator looks like. It does mean that you don't use data anotations, but sometimes data anotations get a bit big and messy.
Here is an example:
(Errors.Required, Labels.Email and Errors.AlreadyRegistered are in my blobal resources folder.)
public class CreateEmployerValidator : AbstractValidator<CreateEmployerModel> {
public RegisterUserValidator() {
RuleFor(m => m.Email)
.NotEmpty()
.WithMessage(String.Format(Errors.Required, new object[] { Labels.Email }))
.EmailAddress()
.WithMessage(String.Format(Errors.Invalid, new object[] { Labels.Email }))
.Must(this.BeUniqueEmail)
.WithMessage(String.Format(Errors.AlreadyRegistered, new object[] { Labels.Email }));
}
public bool BeUniqueEmail(this IValidator validator, string email ) {
//Database request to check if email already there?
...
}
}
Like I said, it is a move away form data annotations, only because I already have too many annotations on my methods already!
I'll add my findings since I had to fight with this. Maybe it will help someone.
When you derive from RequiredAttribute, it seems to break client side validation. So to fix this I implemented IClientValidatable and implemented the GetClientValidationRules method. Resources.GetResources is static helper method I have that wraps around HttpContext.GetGlobalResourceObject.
The custom required attribute:
public class LocalizedRequiredAttribute : RequiredAttribute, IClientValidatable
{
public LocalizedRequiredAttribute(string resourceName)
{
this.ErrorMessage = Resources.GetResource(resourceName);
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage,
ValidationType= "required"
};
}
}
Usage:
[LocalizedRequired("SomeResourceName")]
public string SomeProperty { get; set; }
And my Resources helper if anyone is interested:
public class Resources
{
public static string GetResource(string resourceName)
{
string text = resourceName;
if (System.Web.HttpContext.Current != null)
{
var context = new HttpContextWrapper(System.Web.HttpContext.Current);
var globalResourceObject = context.GetGlobalResourceObject(null, resourceName);
if (globalResourceObject != null)
text = globalResourceObject.ToString();
}
return text;
}
}

Categories

Resources