C# - if else in Data Annotations - c#

This is about Data Annotations in C#.
I have a class called MyClass. There I will be having two properties.
class MyClass
{
private string _propName;
public string PropName
{
get {return _propName;}
set {_propName=value;}
}
private string _propVal;
public string PropVal
{
get {return _propVal;}
set {_propVal=value;}
}
}
I have a MS SQl table which contains data for propName and relevant propValue.
I need to have validations using DataAnnotations. Each and every _propName need to have different validations.
My question is : How do I add different validations/data annotations to the class structure I have mentioned above ? I was wondering whether or not there is some way to specify if-else conditions in Data Annotations ?
if "this propertyname"
validation 1
else if "that property name"
validation 1
Can someone help ?

You should be able to something like this:
public class MyValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
switch (validationContext.MemberName)
{
//some logic
}
}
}

The problem is with ValidationAttribute your only going to get the value of the one property, not the combination of the properties, what you can do is combine the PropName and PropVal in a tuple, and then validate that.
This little test app shows a method of achieving what you want.
public class MyClass {
[MyValidation]
public Tuple<string, string> PropValue { get; set; }
public List<ValidationResult> TestValidation() {
var validationContext = new ValidationContext(this, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
Validator.TryValidateObject(this, validationContext, results, validateAllProperties: true);
return results;
}
}
public class MyValidationAttribute : ValidationAttribute {
public override bool IsValid(object value) {
var propValue = value as Tuple<string, string>;
if (propValue.Item1 == "Item") {
var setPropValue = propValue.Item2;
//Do you validation
}
return true;
}
}
class Program {
static void Main(string[] args) {
MyClass instance = new MyClass();
instance.PropValue = new Tuple<string, string>("Item", "Value");
var result = instance.TestValidation();
}
}

Related

Enforce an interface via parameter to an attribute [duplicate]

Is there a way to force the compiler to restrict the usage of a custom attribute to be used only on specific property types like int, short, string (all the primitive types)?
similar to the AttributeUsageAttribute's ValidOn-AttributeTargets enumeration.
No, you can't, basically. You can limit it to struct vs class vs interface, that is about it. Plus: you can't add attributes to types outside your code anyway (except for via TypeDescriptor, which isn't the same).
You can run this unit test to check it.
First, declare validation attribute PropertyType:
[AttributeUsage(AttributeTargets.Class)]
// [JetBrains.Annotations.BaseTypeRequired(typeof(Attribute))] uncomment if you use JetBrains.Annotations
public class PropertyTypeAttribute : Attribute
{
public Type[] Types { get; private set; }
public PropertyTypeAttribute(params Type[] types)
{
Types = types;
}
}
Create unit test:
[TestClass]
public class TestPropertyType
{
public static Type GetNullableUnderlying(Type nullableType)
{
return Nullable.GetUnderlyingType(nullableType) ?? nullableType;
}
[TestMethod]
public void Test_PropertyType()
{
var allTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes());
var allPropertyInfos = allTypes.SelectMany(a => a.GetProperties()).ToArray();
foreach (var propertyInfo in allPropertyInfos)
{
var propertyType = GetNullableUnderlying(propertyInfo.PropertyType);
foreach (var attribute in propertyInfo.GetCustomAttributes(true))
{
var attributes = attribute.GetType().GetCustomAttributes(true).OfType<PropertyTypeAttribute>();
foreach (var propertyTypeAttr in attributes)
if (!propertyTypeAttr.Types.Contains(propertyType))
throw new Exception(string.Format(
"Property '{0}.{1}' has invalid type: '{2}'. Allowed types for attribute '{3}': {4}",
propertyInfo.DeclaringType,
propertyInfo.Name,
propertyInfo.PropertyType,
attribute.GetType(),
string.Join(",", propertyTypeAttr.Types.Select(x => "'" + x.ToString() + "'"))));
}
}
}
}
Your attribute, for example allow only decimal property types:
[AttributeUsage(AttributeTargets.Property)]
[PropertyType(typeof(decimal))]
public class PriceAttribute : Attribute
{
}
Example model:
public class TestModel
{
[Price]
public decimal Price1 { get; set; } // ok
[Price]
public double Price2 { get; set; } // error
}
You could write code yourself to enforce correct use of your attribute class, but that's as much as you can do.
The code below will return an error if the attribute was placed on a property/field that is not List of string.
The line if (!(value is List<string> list)) may be a C#6 or 7 feature.
[AttributeUsage(AttributeTargets.Property |
AttributeTargets.Field, AllowMultiple = false)]
public sealed class RequiredStringListAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext context)
{
if (!(value is List<string> list))
return new ValidationResult($"The required attrribute must be of type List<string>");
bool valid = false;
foreach (var item in list)
{
if (!string.IsNullOrWhiteSpace(item))
valid = true;
}
return valid
? ValidationResult.Success
: new ValidationResult($"This field is required"); ;
}
}
The way I am doing this is following:
[AttributeUsage(AttributeTargets.Property)]
public class SomeValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value is not string stringToValidate)
{
throw new AttributeValueIsNotStringException(validationContext.DisplayName, validationContext.ObjectType.Name);
}
// validationContext.DisplayName is name of property, where validation attribut was used.
// validationContext.ObjectType.Name is name of class, in which the property is placed to instantly identify, where is the error.
//Some validation here.
return ValidationResult.Success;
}
}
And exception look like this:
public class AttributeValueIsNotStringException : Exception
{
public AttributeValueIsNotStringException(string propertyName, string className) : base(CreateMessage(propertyName, className))
{
}
private static string CreateMessage(string propertyName, string className)
{
return $"Validation attribute cannot be used for property: \"{propertyName}\" in class: \"{className}\" because it's type is not string. Use it only for string properties.";
}
}

Customize data annotations CompareAttribute

I need to compare two properties in a class using .net data annotations. One of the two properties should be filled and the other should be blank. How can I override the behavior of the CompareAttribute ? If it is not possible, what's the alternative solution ?
This class works with one issue:
If Property A is set to something and property B already has a value, then property A becomes invalid as expected. Upon Blanking property B, property A should become valid but it won't until I try to modify property A so I trigger the validation again. Is there a way to connect the two together to trigger the validation on both one either one changes ?
class CustomAttribute : ValidationAttribute
{
private readonly string _other;
public CustomAttribute(string other)
{
_other = other;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(_other);
if (property == null)
{
return new ValidationResult(
string.Format("Unknown property: {0}", _other)
);
}
var otherValue = property.GetValue(validationContext.ObjectInstance, null);
if (!String.IsNullOrEmpty(value.ToString()) && !String.IsNullOrEmpty(otherValue.ToString()))
{
return new ValidationResult("Test");
}
return null;
}
}
For stuff like this I use ExpressiveAnnotations. It has a brilliant RequiredIf attribute:
[RequiredIf("B == null", ErrorMessage = "Either A or B should be filled")]
public string A { get; set; }
[RequiredIf("A == null", ErrorMessage = "Either A or B should be filled")]
public string B { get; set; }
You can extend the CompareAttribute with your own class:
public class CustomCompareAttribute: CompareAttribute {
public CustomCompareAttribute(string otherProperty) : base(otherProperty) {
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
if (OtherProperty == null && value == null) {
return new ValidationResult("Either A or B should be filled");
}
// more checks here ...
}
}

How do I do Sub Object Custom Validation mvc3

I have an object (header) that has a list of sub-ojects (details) that I want to do custom validation on prior to accepting data. I've tried ModelState.IsValid and TryValidateModel, but it doesn't seem to fire the Validate method on the sub-objects (only the header object).
So on submission I see the validation fire for the header, but not the sub-ojects. Then if I do a TryValidateModel I again see (breakpoint) the validation method get called on the header, but not on the sub-objects.
The annotated validation (must be number, etc...) seems to be working on the sub-objects, just not the custom logic added via the IValidatableObject interface. Any help would be greatly appreciated.
I make an attribute ( [ValidateObject] ) and it will validate the attribute you put on your class, like you would think it suppose to do.
public class Personne
{
[ValidateObject]
public Address Address { get; set; }
//[...]
}
(Address is a custom class.)
It can be used for validating:
Object properties on a model. (like above)
Collection of sub object.
[ValidateObject]
public List<Address> Address { get; set; }
It support multi level of dept, if "Address" had a propertie of type "ZipCode" with the attribute [ValidateObject] it would be validate tho.
Code:
public class ValidateObjectAttribute : ValidationAttribute
{
public ValidateObjectAttribute()
{
}
private ValidationContext ValidationContext { get; set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
this.ValidationContext = validationContext;
var results = new List<ValidationResult>();
try
{
var isIterable = this.IsIterable(value);
if (isIterable)
{
int currentItemPosition = -1;
foreach (var objectToValidate in value as IEnumerable<object>)
{
currentItemPosition++;
var resultTemp = ValidationsForObject(objectToValidate, true, currentItemPosition);
if (resultTemp != null)
results.AddRange(resultTemp);
}
if (results.Count <= 0)
results = null;
}
else
results = ValidationsForObject(value);
if (results != null)
{
//Build a validation result
List<string> memberNames = new List<string>();
results.ForEach(r => memberNames.AddRange(r.MemberNames));
var compositeResultsReturn = new CompositeValidationResult($"Validation for {validationContext.DisplayName} failed!", memberNames.AsEnumerable());
results.ForEach(r => compositeResultsReturn.AddResult(r));
return compositeResultsReturn;
}
}
catch (Exception) { }
return ValidationResult.Success;
}
private List<ValidationResult> ValidationsForObject (object objectToValidate, bool IsIterable = false, int position = -1)
{
var results = new List<ValidationResult>();
var contextTemp = new ValidationContext(objectToValidate, null, null);
var resultsForThisItem = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(objectToValidate, contextTemp, resultsForThisItem, true);
if (isValid)
return null;
foreach (var validationResult in resultsForThisItem)
{
List<string> propNames = new List<string>();// add prefix to properties
foreach (var nameOfProp in validationResult.MemberNames)
{
if (IsIterable)
propNames.Add($"{this.ValidationContext.MemberName}[{position}].{nameOfProp}");
else
propNames.Add($"{this.ValidationContext.MemberName}.{nameOfProp}");
}
var customFormatValidation = new ValidationResult(validationResult.ErrorMessage, propNames);
results.Add(customFormatValidation);
}
return results;
}
private bool IsIterable(object value)
{
////COULD WRITE THIS, but its complicated to debug...
//if (value.GetType().GetInterfaces().Any(
//i => i.IsGenericType &&
//i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
//{
// // foreach...
//}
Type valueType = value.GetType();
var interfaces = valueType.GetInterfaces();
bool isIterable = false;
foreach (var i in interfaces)
{
var isGeneric = i.IsGenericType;
bool isEnumerable = i.GetGenericTypeDefinition() == typeof(IEnumerable<>);
isIterable = isGeneric && isEnumerable;
if (isIterable)
break;
}
return isIterable;
}
}
public class CompositeValidationResult : ValidationResult
{
private readonly List<ValidationResult> _results = new List<ValidationResult>();
public IEnumerable<ValidationResult> Results
{
get
{
return _results;
}
}
public CompositeValidationResult(string errorMessage) : base(errorMessage)
{
}
public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames) : base(errorMessage, memberNames)
{
}
protected CompositeValidationResult(ValidationResult validationResult) : base(validationResult)
{
}
public void AddResult(ValidationResult validationResult)
{
_results.Add(validationResult);
}
}
This will work if you model is correctly binded :)
You might want to add the required attribute, making sure the the object is not null itself.
[Required]
Hope it help!
I wonder if you root object has errors preventing child validation.
See Recursive validation using annotations and IValidatableObject
This URL mentions that scenario and also code to force validation on the child from the root
As per the posting triggering off the validation from the root object
public IEnumerable Validate(ValidationContext validationContext)
{
var context = new ValidationContext(this.Details, validationContext.ServiceContainer, validationContext.Items);
var results = new List();
Validator.TryValidateObject(this.Details, context, results);
return results;
}
The TryValidateObject didn't appear to fire custom validation, only data annotations? I went down the path of adding validation of the details to the header validation by doing the following:
foreach (var detail in this.Details)
{
var validationResults = detail.Validate(validationContext);
foreach (var validationResult in validationResults)
{
yield return validationResult;
}
}
This worked in terms of validation, but the UI didn't display the error messages. Even though I have ValidationMessagesFor on the UI.
Validation not working solved here: MVC3 Master-Details Validation not Displaying

CustomValidationAttribute specifed method not being called

I'm using the System.ComponentModel.DataAnnotations.CustomValidationAttribute to validate one of my POCO classes and when I try to unit test it, it's not even calling the validation method.
public class Foo
{
[Required]
public string SomethingRequired { get; set }
[CustomValidation(typeof(Foo), "ValidateBar")]
public int? Bar { get; set; }
public string Fark { get; set; }
public static ValidationResult ValidateBar(int? v, ValidationContext context) {
var foo = context.ObjectInstance as Foo;
if(!v.HasValue && String.IsNullOrWhiteSpace(foo.Fark)) {
return new ValidationResult("Either Bar or Fark must have something in them.");
}
return ValidationResult.Success;
}
}
but when I try to validate it:
var foo = new Foo {
SomethingRequired = "okay"
};
var validationContext = new ValidationContext(foo, null, null);
var validationResults = new List<ValidationResult>();
bool isvalid = Validator.TryValidateObject(foo, validationContext, validationResults);
Assert.IsFalse(isvalid); //FAIL!!! It's valid when it shouldn't be!
It never even steps into the custom validation method. What gives?
Try using the overload that takes a bool that specifies if all properties should be validated. Pass true for the last parameter.
public static bool TryValidateObject(
Object instance,
ValidationContext validationContext,
ICollection<ValidationResult> validationResults,
bool validateAllProperties
)
If you pass false or omit the validateAllProperties, only the RequiredAttribute will be checked.
Here is the MSDN documentation.

Allow a custom Attribute only on specific type

Is there a way to force the compiler to restrict the usage of a custom attribute to be used only on specific property types like int, short, string (all the primitive types)?
similar to the AttributeUsageAttribute's ValidOn-AttributeTargets enumeration.
No, you can't, basically. You can limit it to struct vs class vs interface, that is about it. Plus: you can't add attributes to types outside your code anyway (except for via TypeDescriptor, which isn't the same).
You can run this unit test to check it.
First, declare validation attribute PropertyType:
[AttributeUsage(AttributeTargets.Class)]
// [JetBrains.Annotations.BaseTypeRequired(typeof(Attribute))] uncomment if you use JetBrains.Annotations
public class PropertyTypeAttribute : Attribute
{
public Type[] Types { get; private set; }
public PropertyTypeAttribute(params Type[] types)
{
Types = types;
}
}
Create unit test:
[TestClass]
public class TestPropertyType
{
public static Type GetNullableUnderlying(Type nullableType)
{
return Nullable.GetUnderlyingType(nullableType) ?? nullableType;
}
[TestMethod]
public void Test_PropertyType()
{
var allTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes());
var allPropertyInfos = allTypes.SelectMany(a => a.GetProperties()).ToArray();
foreach (var propertyInfo in allPropertyInfos)
{
var propertyType = GetNullableUnderlying(propertyInfo.PropertyType);
foreach (var attribute in propertyInfo.GetCustomAttributes(true))
{
var attributes = attribute.GetType().GetCustomAttributes(true).OfType<PropertyTypeAttribute>();
foreach (var propertyTypeAttr in attributes)
if (!propertyTypeAttr.Types.Contains(propertyType))
throw new Exception(string.Format(
"Property '{0}.{1}' has invalid type: '{2}'. Allowed types for attribute '{3}': {4}",
propertyInfo.DeclaringType,
propertyInfo.Name,
propertyInfo.PropertyType,
attribute.GetType(),
string.Join(",", propertyTypeAttr.Types.Select(x => "'" + x.ToString() + "'"))));
}
}
}
}
Your attribute, for example allow only decimal property types:
[AttributeUsage(AttributeTargets.Property)]
[PropertyType(typeof(decimal))]
public class PriceAttribute : Attribute
{
}
Example model:
public class TestModel
{
[Price]
public decimal Price1 { get; set; } // ok
[Price]
public double Price2 { get; set; } // error
}
You could write code yourself to enforce correct use of your attribute class, but that's as much as you can do.
The code below will return an error if the attribute was placed on a property/field that is not List of string.
The line if (!(value is List<string> list)) may be a C#6 or 7 feature.
[AttributeUsage(AttributeTargets.Property |
AttributeTargets.Field, AllowMultiple = false)]
public sealed class RequiredStringListAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext context)
{
if (!(value is List<string> list))
return new ValidationResult($"The required attrribute must be of type List<string>");
bool valid = false;
foreach (var item in list)
{
if (!string.IsNullOrWhiteSpace(item))
valid = true;
}
return valid
? ValidationResult.Success
: new ValidationResult($"This field is required"); ;
}
}
The way I am doing this is following:
[AttributeUsage(AttributeTargets.Property)]
public class SomeValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value is not string stringToValidate)
{
throw new AttributeValueIsNotStringException(validationContext.DisplayName, validationContext.ObjectType.Name);
}
// validationContext.DisplayName is name of property, where validation attribut was used.
// validationContext.ObjectType.Name is name of class, in which the property is placed to instantly identify, where is the error.
//Some validation here.
return ValidationResult.Success;
}
}
And exception look like this:
public class AttributeValueIsNotStringException : Exception
{
public AttributeValueIsNotStringException(string propertyName, string className) : base(CreateMessage(propertyName, className))
{
}
private static string CreateMessage(string propertyName, string className)
{
return $"Validation attribute cannot be used for property: \"{propertyName}\" in class: \"{className}\" because it's type is not string. Use it only for string properties.";
}
}

Categories