Search code examples
c#attributesdata-annotations

How to prevent the repetition of the same error message text when validating using DataAnnotations in C#?


I’m developing an application where I use DataAnnotations to validate the values passed to the class properties. As I have several different classes with many properties that have similar validation rules, I would like to define for the ErrorMessage argument a method that returns a formatted string. The problem is that when I use this method in the validation attribute I get the following error:

“An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type PrototypeValidationData”

Reading this post I understood that the attribute doesn’t support a method as argument, but I would like to know if there is any way of preventing to repeat the same error message in all the properties definitions. Is there any alternative?

Follows an example of what I’m trying to do:

    public class Foo
    {
        public string Id { get; set; }

        [Range(double.Epsilon, double.MaxValue, ErrorMessage = ErrorMessages.MustBeGreaterThanZero(Bar.ToString(), "Foo", Id, "Bar"))]
        public double Bar { get; set; }
    }

    public static class ErrorMessages
    {
        public static string MustBeGreaterThanZero(string str,
                                                   string senderClass,
                                                   string senderId,
                                                   string senderField)
        {
            return String.Format("The value {0} assigned to the field {1} in {2} ID {3} must be greater than ZERO.",
                                 str, senderField, senderClass, senderId);
        }

Solution

  • You can create custom Validation attibute inherited from RangeAttribute.

    public class RangeCustomAttribute : System.ComponentModel.DataAnnotations.RangeAttribute
    {
        public RangeCustomAttribute(double minimum, 
                                    double maximum,
                                    string senderClass,
                                    string senderId,
                                    string senderField) : base(minimum, maximum)
        {
            SenderClass = senderClass;
            SenderId = senderId;
            SenderField = senderField;
        }
    
        public string SenderClass { get; }
        public string SenderId { get; }
        public string SenderField { get; }
    
        protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
        {
            var result = base.IsValid(value, validationContext);
    
            if (result != ValidationResult.Success)
            {
                var instance = validationContext.ObjectInstance;
                Type type = instance.GetType();
    
                var idValue = type.GetProperty(SenderId)?.GetValue(instance, null);
    
                var err = $"The value {value} assigned to the field {SenderField} in {SenderClass} ID {idValue} must be greater than ZERO.";
    
                result = new ValidationResult(err, new List<string> { validationContext.DisplayName });
            }
    
            return result;
        }
    }
    

    By the way SenderClass and SenderField can be read from reflection, so you perhaps this parameter is redurant. Like this:

    public class RangeCustom2Attribute : System.ComponentModel.DataAnnotations.RangeAttribute
    {
        public RangeCustom2Attribute(double minimum,
                                    double maximum,
                                    string senderId) : base(minimum, maximum)
        {
            SenderId = senderId;
        }
    
        public string SenderId { get; }
    
        protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
        {
            var result = base.IsValid(value, validationContext);
    
            if (result != ValidationResult.Success)
            {
                var instance = validationContext.ObjectInstance;
                Type type = instance.GetType();
    
                var senderClass = type.Name;
    
                var senderField = validationContext.MemberName;
    
                var idValue = type.GetProperty(SenderId)?.GetValue(instance, null);
    
                var err = $"The value {value} assigned to the field {senderField} in {senderClass} ID {idValue} must be greater than ZERO.";
    
                result = new ValidationResult(err, new List<string> { validationContext.DisplayName });
            }
    
            return result;
        }
    }
    

    Then you can use them similar way:

    public class TestClass
    {
        public int Id { get; set; }
    
        [RangeCustom(0, 1, "TestClass", "Id", "Val")]
        public double Val { get; set; }
    
        [RangeCustom2(0, 1, "Id")]
        public double Val2 { get; set; }
    }