Wednesday, March 6, 2019

Data Annotation: Compare value against other member

This time again: no rocket sience :)

Data annotation gives some nice possiblity to automate data validation. But I couldn't find an out of the box validation attribute which lets check a member value against another member value.

This is typically required if you have f.ex. from date and to date: The from date shall be smaller (or equal)  to the to date. Or to lower-upper-bound ranges: start point shall be smaller (or equal) to end point.

So I created such a validation attribute from scratch. May be you can use it too - at least you can take it as template to create your own validation attribute classes if you need some jump start.

The validation attribute class itself:

 using System;  
 using System.Collections.Generic;  
 using System.ComponentModel;  
 using System.ComponentModel.DataAnnotations;  
 using System.Globalization;  
 using System.Reflection;  
 using System.Linq;  
   
 namespace AnyNamespace  
 {  
   /// <summary>  
   /// Provides functionality to validate member   
   /// value against other value through comparison  
   /// </summary>  
   [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property,   
     AllowMultiple = false)]  
   public sealed class MustCompareToAttribute : ValidationAttribute  
   {  
     /// <summary>  
     /// The comparison operators to fit validation  
     /// </summary>  
     public enum Operator  
     {  
       /// <summary>  
       /// The 'smaller' operator  
       /// </summary>  
       [Display(Description = "is not smaller than")]   
       // TODO replace with localized resource  
       SmallerThan,  
   
       /// <summary>  
       /// The 'smaller or equal' operator  
       /// </summary>  
       [Display(Description = "is not smaller than or equal to")]   
       // TODO replace with localized resource  
       SmallerOrEqualThan,  
   
       /// <summary>  
       /// The 'greater' operator  
       /// </summary>  
       [Display(Description = "is not greater than")]   
       // TODO replace with localized resource  
       GreaterThan,  
   
       /// <summary>  
       /// The 'greater or equal' operator  
       /// </summary>  
       [Display(Description = "is not greater than or equal to")]   
       // TODO replace with localized resource  
       GreaterOrEqualThan  
     }  
   
     /// <summary>  
     /// The counterpart for value comparison  
     /// </summary>  
     public object ComparisonReference { get; }  
   
     /// <summary>  
     /// The operator used for comparison  
     /// </summary>  
     public Operator ComparisonAgainstReference { get; }  
   
     /// <summary>  
     /// Gets a value that indicates whether the   
     /// attribute requires validation context.  
     /// <c>True</c> if the attribute requires validation context,   
     /// otherwise <c>false</c>.  
     /// </summary>  
     public override bool RequiresValidationContext => true;  
   
   
   
     /// <summary>  
     /// Constructor with fixed comparison value and error message  
     /// </summary>  
     /// <param name="comparisonReferenceMember">  
     /// A member name of the validation context's object instance subject   
     /// to retrieve it's value for comparison  
     /// </param>  
     /// <param name="comparisonAgainstReference">  
     /// The comparison operator which must fullify in order to   
     /// successfull validate member  
     /// </param>  
     /// <param name="errorMessage">  
     /// The error message to associate with a validation control.  
     /// The message will be formatted with following values:  
     /// {0} = Trigger member display name,  
     /// {1} = Trigger member value,  
     /// {2} = Comparison operator missmatch text fragment,  
     /// {3} = Comparsion member display name  
     /// {4} = Comparsion value,  
     /// </param>  
     public MustCompareToAttribute(string comparisonReferenceMember,   
       Operator comparisonAgainstReference, string errorMessage)  
         : base(errorMessage)  
     {  
       ComparisonAgainstReference = comparisonAgainstReference;  
       ComparisonReference = comparisonReferenceMember ??   
         throw new ArgumentNullException(nameof(comparisonReferenceMember));  
     }  
   
   
     /// <summary>  
     /// Constructor with fixed comparison value and error message  
     /// </summary>  
     /// <param name="comparisonReferenceMember">  
     /// A member name of the validation context's object   
     /// instance subject to retrieve it's value for comparison  
     /// </param>  
     /// <param name="comparisonAgainstReference">  
     /// The comparison operator which must fullify in order   
     /// to successfull validate member  
     /// </param>  
     /// <param name="errorMessageAccessor">  
     /// The function that enables access to validation resources.  
     /// The message will be formatted with following values:  
     /// {0} = Trigger member display name,  
     /// {1} = Trigger member value,  
     /// {2} = Comparison operator missmatch text fragment,  
     /// {3} = Comparsion member display name  
     /// {4} = Comparsion value,  
     /// </param>  
     public MustCompareToAttribute(string comparisonReferenceMember,   
       Operator comparisonAgainstReference, Func<string> errorMessageAccessor)   
         : base(errorMessageAccessor)  
     {  
       ComparisonAgainstReference = comparisonAgainstReference;  
       ComparisonReference = comparisonReferenceMember ??   
         throw new ArgumentNullException(nameof(comparisonReferenceMember));  
     }  
   
     /// <summary>  
     /// Constructor with fixed comparison value and error message  
     /// </summary>  
     /// <param name="comparisonReferenceType">  
     /// The type of the counterpart for value comparison  
     /// </param>  
     /// <param name="comparisonReferenceMemberName">  
     /// A member name of the validation context's object instance subject   
     /// to retrieve it's value for comparison  
     /// </param>  
     /// <param name="comparisonAgainstReference">  
     /// The comparison operator which must fullify  
     /// in order to successfull validate member  
     /// </param>  
     public MustCompareToAttribute(string comparisonReferenceMemberName,   
       Operator comparisonAgainstReference) : base((string)null)  
     {  
       ComparisonAgainstReference = comparisonAgainstReference;  
       ComparisonReference = comparisonReferenceMemberName;  
     }  
   
     string ComparisonOperatorText()  
     {  
       Type operatorType = typeof(Operator);  
       return operatorType.GetField(Enum.GetName(operatorType,   
         ComparisonAgainstReference))  
         .GetCustomAttribute<DisplayAttribute>().Description;  
     }  
   
     /// <summary>  
     /// Validates the specified value with respect   
     /// to the current validation attribute.  
     /// </summary>  
     /// <param name="value">  
     /// The value to validate.  
     /// </param>  
     /// <param name="validationContext">  
     /// The context information about the validation operation.  
     /// </param>  
     /// <returns>  
     /// The validation result  
     /// </returns>  
     protected override ValidationResult IsValid(object value,   
       ValidationContext validationContext)  
     {  
       ValidationResult ret = ValidationResult.Success;  
   
       if (value != null)  
       {  
         var comparisonValue = default(object);  
   
         var comparisonMemberDisplayName = default(string);  
         var comparisonMember = default(MemberInfo);  
   
         comparisonMember = validationContext.ObjectType  
           .GetMember((string)ComparisonReference)  
             .Where(x => x.MemberType == MemberTypes.Property  
               || x.MemberType == MemberTypes.Field)  
                 .FirstOrDefault();  
   
         if (comparisonMember == null)  
         {  
           throw new MissingMemberException(validationContext.ObjectType.Name,   
             (string)ComparisonReference);  
         }  
   
         if (comparisonMember is PropertyInfo comparisonPropertyInfo)  
         {  
           comparisonValue = comparisonPropertyInfo  
             .GetValue(validationContext.ObjectInstance);  
         }  
         else if (comparisonMember is FieldInfo comparisonFieldInfo)  
         {  
           comparisonValue = comparisonFieldInfo  
             .GetValue(validationContext.ObjectInstance);  
         }  
         comparisonMemberDisplayName = comparisonMember.Name;  
   
         var attribute = GetCustomAttribute(comparisonMember, typeof(DisplayAttribute));  
         if (attribute is DisplayAttribute displayAttribute)  
         {  
           comparisonMemberDisplayName = displayAttribute.Name;  
         }  
         else  
         {  
           attribute = GetCustomAttribute(comparisonMember, typeof(DisplayNameAttribute));  
           if (attribute is DisplayNameAttribute displayNameAttribute)  
           {  
             comparisonMemberDisplayName = displayNameAttribute.DisplayName;  
           }  
         }  
   
         if (comparisonValue != null)  
         {  
           int? comparison = null;  
   
   
           if (value is IComparable comparableValue)  
           {  
             comparison = comparableValue.CompareTo(comparisonValue);  
           }  
           else  
           {  
             throw new InvalidOperationException("Trigger member value must implement IComparable");   
             // TODO replace with localized resource  
           }  
   
           if (comparison <= 0   
             && ComparisonAgainstReference == Operator.GreaterThan  
               || comparison >= 0   
                 && ComparisonAgainstReference == Operator.SmallerThan  
                   || comparison > 0   
                     && ComparisonAgainstReference == Operator.SmallerOrEqualThan  
                       || comparison < 0   
                         && ComparisonAgainstReference == Operator.GreaterOrEqualThan)  
           {  
             var memberList = new List<string>() { { validationContext.MemberName } };  
   
             memberList.Add(comparisonMember.Name);  
   
             var errorMessageString = ErrorMessageString;  
   
             if (errorMessageString == null)  
             {  
               errorMessageString = "'{0}' ({1}) {2} '{3}' ({4})";   
               // TODO replace with localized resource  
             }  
   
             ret = new ValidationResult(string.Format(CultureInfo.CurrentCulture,  
               errorMessageString,  
                 validationContext.DisplayName,  
                   value.ToString(),  
                     ComparisonOperatorText(),  
                       comparisonMemberDisplayName,  
                         comparisonValue.ToString()),  
                           memberList);  
           }  
         }  
   
       }  
       return ret;  
     }  
   }  
 }  

The usage:

The unit tests (in case you want to adjust the code and make sure all is still working well) :

 using System;  
 using System.Collections.Generic;  
 using System.ComponentModel.DataAnnotations;  
 using Microsoft.VisualStudio.TestTools.UnitTesting;  
   
 namespace AnyNamespace  
 {  
   
   [TestClass]  
   public class MustCompareToAttributeTests  
   {  
     #region Mock models  
   
     class MockModelSmallerThan<T1, T2>  
     {  
       [MustCompareTo(nameof(Value2), MustCompareToAttribute.Operator.SmallerThan)]  
       public T1 Value1 { get; set; }  
   
       public T2 Value2 { get; set; }  
     }  
   
     class MockModelSmallerOrEqualThan<T1, T2>  
     {  
       [MustCompareTo(nameof(Value2), MustCompareToAttribute.Operator.SmallerOrEqualThan)]  
       public T1 Value1 { get; set; }  
   
       public T2 Value2 { get; set; }  
     }  
   
     class MockModelGreaterThan<T1, T2>  
     {  
       [MustCompareTo(nameof(Value2), MustCompareToAttribute.Operator.GreaterThan)]  
       public T1 Value1 { get; set; }  
   
       public T2 Value2 { get; set; }  
     }  
   
     class MockModelGreaterOrEqualThan<T1, T2>  
     {  
       [MustCompareTo(nameof(Value2), MustCompareToAttribute.Operator.GreaterOrEqualThan)]  
       public T1 Value1 { get; set; }  
   
       public T2 Value2 { get; set; }  
     }  
   
     class MockModelWrongMemberDeclaration<T1, T2>  
     {  
       [MustCompareTo("Typo", MustCompareToAttribute.Operator.GreaterOrEqualThan)]  
       public T1 Value1 { get; set; }  
   
       public T2 Value2 { get; set; }  
     }  
   
     #endregion  
   
     #region Wrong usage  
   
     [TestMethod]  
     public void NotCompatible()  
     {  
       var model = new MockModelSmallerThan<int, string>()  
       {  
          Value1 = 1,  
          Value2 = "a"  
       };  
       // cannot compare integer with string, ArgumentException expected  
       Assert.ThrowsException<ArgumentException>(() => Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     [TestMethod]  
     public void WrongMemberDeclaration()  
     {  
       var model = new MockModelWrongMemberDeclaration<int, string>()  
       {  
         Value1 = 1,  
         Value2 = "a"  
       };  
       // cannot retrieve value from member since member declaration is wrong, MissingMemberException expected  
       Assert.ThrowsException<MissingMemberException>(() => Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
   
     [TestMethod]  
     public void TriggerMemberNotComparable()  
     {  
       var model = new MockModelSmallerThan<MockModelSmallerThan<int,int>, MockModelSmallerThan<int, int>>()  
       {  
         Value1 = new MockModelSmallerThan<int, int>(),  
         Value2 = new MockModelSmallerThan<int, int>()  
       };  
       // type MockModelSmallerThan<T1, T2> does not implement IComparable  
       Assert.ThrowsException<InvalidOperationException>(() => Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
   
     #endregion  
   
     #region Smaller than  
   
     [TestMethod]  
     public void SmallerThanNullableInt()  
     {  
       var model = new MockModelSmallerThan<int?, int?>();  
   
       model.Value1 = null;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 1;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = null;  
       model.Value2 = 2;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 3;  
       model.Value2 = 3;  
       // Value1 is not smaller than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 5;  
       model.Value2 = 4;  
       // Value1 is not smaller than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 6;  
       model.Value2 = 7;  
       // Value1 is smaller than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     [TestMethod]  
     public void SmallerThanMixedInt()  
     {  
       var model = new MockModelSmallerThan<int, int?>();  
   
       model.Value1 = 100;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 101;  
       model.Value2 = 101;  
       // Value1 is not smaller than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 103;  
       model.Value2 = 102;  
       // Value1 is not smaller than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 104;  
       model.Value2 = 105;  
       // Value1 is smaller than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     [TestMethod]  
     public void SmallerThanStr()  
     {  
       var model = new MockModelSmallerThan<string, string>();  
   
       model.Value1 = null;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "a";  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = null;  
       model.Value2 = "a";  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "b";  
       model.Value2 = "b";  
       // Value1 is not smaller than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "d";  
       model.Value2 = "c";  
       // Value1 is not smaller than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "e";  
       model.Value2 = "f";  
       // Value1 is smaller than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     #endregion  
   
     #region Smaller or equal than  
   
     [TestMethod]  
     public void SmallerOrEqualThanNullableInt()  
     {  
       var model = new MockModelSmallerOrEqualThan<int?, int?>();  
   
       model.Value1 = null;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 8;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = null;  
       model.Value2 = 9;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 10;  
       model.Value2 = 10;  
       // Value1 is not smaller or equal than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 12;  
       model.Value2 = 11;  
       // Value1 is not smaller or equal than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 13;  
       model.Value2 = 14;  
       // Value1 is smaller or equal than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     [TestMethod]  
     public void SmallerOrEqualThanMixedInt()  
     {  
       var model = new MockModelSmallerOrEqualThan<int?, int>();  
   
       model.Value1 = null;  
       model.Value2 = 106;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 107;  
       model.Value2 = 108;  
       // Value1 is not smaller or equal than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 110;  
       model.Value2 = 109;  
       // Value1 is not smaller or equal than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 111;  
       model.Value2 = 112;  
       // Value1 is smaller or equal than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     [TestMethod]  
     public void SmallerOrEqualThanStr()  
     {  
       var model = new MockModelSmallerOrEqualThan<string, string>();  
   
       model.Value1 = null;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "g";  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = null;  
       model.Value2 = "h";  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "i";  
       model.Value2 = "i";  
       // Value1 is not smaller or equal than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "l";  
       model.Value2 = "k";  
       // Value1 is not smaller or equal than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "m";  
       model.Value2 = "n";  
       // Value1 is smaller or equal than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     #endregion  
   
     #region Smaller or equal than  
   
     [TestMethod]  
     public void GreaterThanNullableInt()  
     {  
       var model = new MockModelGreaterThan<int?, int?>();  
   
       model.Value1 = null;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 15;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = null;  
       model.Value2 = 16;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 17;  
       model.Value2 = 17;  
       // Value1 is not greater than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 19;  
       model.Value2 = 18;  
       // Value1 is greater than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 20;  
       model.Value2 = 21;  
       // Value1 is not greater than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     [TestMethod]  
     public void GreaterThanMixedInt()  
     {  
       var model = new MockModelGreaterThan<int, int?>();  
   
       model.Value1 = 113;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 114;  
       model.Value2 = 114;  
       // Value1 is not greater than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 116;  
       model.Value2 = 115;  
       // Value1 is greater than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 117;  
       model.Value2 = 118;  
       // Value1 is not greater than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     [TestMethod]  
     public void GreaterThanStr()  
     {  
       var model = new MockModelGreaterThan<string, string>();  
   
       model.Value1 = null;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "o";  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = null;  
       model.Value2 = "p";  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "q";  
       model.Value2 = "q";  
       // Value1 is not greater than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "s";  
       model.Value2 = "r";  
       // Value1 is greater than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "t";  
       model.Value2 = "u";  
       // Value1 is not greater than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
   
     #endregion  
   
     #region Greater or equal than  
   
     [TestMethod]  
     public void GreaterOrEqualThanNullableInt()  
     {  
       var model = new MockModelGreaterOrEqualThan<int?, int?>();  
   
       model.Value1 = null;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 22;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = null;  
       model.Value2 = 23;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 24;  
       model.Value2 = 24;  
       // Value1 is greater or equal than Value2, so NOT OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 26;  
       model.Value2 = 25;  
       // Value1 is greater or equal than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 27;  
       model.Value2 = 28;  
       // Value1 is not greater or equal than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     [TestMethod]  
     public void GreaterOrEqualThanMixedInt()  
     {  
       var model = new MockModelGreaterOrEqualThan<int, int?>();  
   
       model.Value1 = 119;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 120;  
       model.Value2 = 120;  
       // Value1 is greater or equal than Value2, so NOT OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 122;  
       model.Value2 = 121;  
       // Value1 is greater or equal than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = 123;  
       model.Value2 = 124;  
       // Value1 is not greater or equal than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     [TestMethod]  
     public void GreaterOrEqualThanStr()  
     {  
       var model = new MockModelGreaterOrEqualThan<string, string>();  
   
       model.Value1 = null;  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "v";  
       model.Value2 = null;  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = null;  
       model.Value2 = "w";  
       // null values are not compared, so the validation is OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "x";  
       model.Value2 = "x";  
       // Value1 is greater or equal than Value2, so NOT OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "z";  
       model.Value2 = "y";  
       // Value1 is greater or equal than Value2, so OK  
       Assert.IsTrue(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
   
       model.Value1 = "A";  
       model.Value2 = "B";  
       // Value1 is not greater or equal than Value2, so NOT OK  
       Assert.IsFalse(Validator.TryValidateObject(model, new ValidationContext(model), new List<ValidationResult>(), true));  
     }  
   
     #endregion  
   }  
 }  
   
   

Sorry for the missing c# formatting style - this time I wasn't able to copy the code nicely. Last time used Microsoft Word and it worked quite well, but this time I didn't figured out, what is wrong. May be I should restart my PC, since it's running for a while ...

Saturday, December 15, 2018

Number formattings for user prompt

You probably know about formatting numbers for floating types. But do you whish sometimes an easy way to format them with keeping the precision but loosing useless trailing zero value digits? I got that whish too, so I built some helper methods.
The minimum precision digits will be kept (as defined in user profile, usually 2 digits), but higher precision will also be kept, but without useless zeros at the end of formatted result.

Example:
Your number is a decimal with the value 25.222456000 you want to show 25.222456
Your number is a decimial with the value 25.7 you want to show 25.70


The implementation is for the decimal type, but you can easely convert or duplicate it for any other floating type.


using System;
using System.Globalization;

namespace AnyNamespace
{
    /// <summary>
    /// Provides functionality to <c>decimal</c> data type
    /// </summary>
    public static class DecimalExtender
    {
        /// <summary>
        /// Formats a decimal according to current user profile settings
        /// (often called country region settings),
        /// but has option to keep precision though
        /// </summary>
        /// <param name="input">
        /// The value to format
        /// </param>
        /// <param name="format">
        /// The value type:
        /// <c>n</c> is for number,
        /// <c>p</c> is for percent,
        /// <c>c</c> is for currency,
        /// for more infomation about format constants
        /// see <see href="https://docs.microsoft.com/de-de/dotnet/standard/base-types/standard-numeric-format-strings?view=netframework-4.7.2"/>
        /// </param>
        /// <param name="decimalDigitPropertyName">
        /// The property on <c>NumberFormatInfo</c> type
        /// to use to retrieve minimal number of digits for precision,
        /// for more information see <see cref="NumberFormatInfo"/>
        /// </param>
        /// <param name="keepPercision">
        /// Keeps precision over minimal number of digits for precision
        /// </param>
        /// <returns>
        /// The formatted number as string
        /// </returns>
        static string Format(decimal input, string format,
            string decimalDigitPropertyName, bool keepPercision)
        {
            var cultureFormat = (NumberFormatInfo)CultureInfo.CurrentCulture
                .NumberFormat.Clone();

            /* if PercentDecimalDigits is used,
             * we need to take care of x100 multiplication
             * when detecting decimal palces ...
             */
            var decimalPlaces = (decimalDigitPropertyName ==
                nameof(NumberFormatInfo.PercentDecimalDigits)
                    ? (input * 100)
                        : input) % 1;

            int requiredDecimalPlacesLength = 0;

            if (decimalPlaces != 0)
            {
                var decimalString = Math.Abs(decimalPlaces)
                    .ToString(CultureInfo.InvariantCulture);

                while (decimalString.Length > 2
                    && decimalString.Substring(decimalString.Length - 1) ==
                        0.ToString(CultureInfo.InvariantCulture))
                {
                    decimalString = decimalString
                        .Substring(0, decimalString.Length - 1);
                }

                requiredDecimalPlacesLength = decimalString.Length - 2;
            }

            if (keepPercision
                && requiredDecimalPlacesLength
                    > (int)cultureFormat.GetType()
                        .GetProperty(decimalDigitPropertyName)
                            .GetValue(cultureFormat))
            {
                // extend the precision to exact after point lenght
                cultureFormat.GetType().GetProperty(decimalDigitPropertyName)
                    .SetValue(cultureFormat, requiredDecimalPlacesLength);
            }

            string ret = input.ToString(format, cultureFormat);

            return ret;
        }

        /// <summary>
        /// Formats a number value, but keeps decimal places if required
        /// </summary>
        /// <param name="input">
        /// The figure to format
        /// </param>
        /// <param name="keepPercision">
        /// Keeps precision over minimal number of digits for precision
        /// </param>
        /// <returns>
        /// The formated number
        /// </returns>
        /// <remarks>
        /// The method uses the <c>n</c> constant for number format formation,
        /// for more information see
        /// <see href="https://docs.microsoft.com/de-de/dotnet/standard/base-types/standard-numeric-format-strings?view=netframework-4.7.2"/>
        /// </remarks>
        public static string FormatNumber(this decimal input, bool keepPrecision)
        {
            return Format(input, "n",
                nameof(NumberFormatInfo.NumberDecimalDigits), keepPrecision);
        }

        /// <summary>
        /// Formats a percent value, but keeps decimal places if required
        /// </summary>
        /// <param name="input">
        /// The figure to format
        /// </param>
        /// <param name="keepPercision">
        /// Keeps precision over minimal number of digits for precision
        /// </param>
        /// <returns>
        /// The formated percent
        /// </returns>
        /// <remarks>
        /// The method uses the <c>p</c> constant for percent format formation,
        /// for more information see
        /// <see href="https://docs.microsoft.com/de-de/dotnet/standard/base-types/standard-numeric-format-strings?view=netframework-4.7.2"/>
        /// </remarks>
        public static string FormatPercent(this decimal input,
            bool keepPrecision)
        {
            return Format(input, "p",
                nameof(NumberFormatInfo.PercentDecimalDigits), keepPrecision);
        }

        /// <summary>
        /// Formats a currency value, but keeps decimal places if required
        /// </summary>
        /// <param name="input">
        /// The figure to format
        /// </param>
        /// <param name="keepPercision">
        /// Keeps precision over minimal number of digits for precision
        /// </param>
        /// <returns>
        /// The formated currency
        /// </returns>
        /// The method uses the <c>c</c> constant for currency format formation,
        /// for more information see
        /// <see href="https://docs.microsoft.com/de-de/dotnet/standard/base-types/standard-numeric-format-strings?view=netframework-4.7.2"/>
        /// </remarks>
        public static string FormatCurrency(this decimal input,
            bool keepPrecision)
        {
            return Format(input, "c",
                nameof(NumberFormatInfo.CurrencyDecimalDigits), keepPrecision);
        }

        /// <summary>
        /// Formats a number value, but keeps decimal places if required
        /// </summary>
        /// <param name="input">
        /// The figure to format
        /// </param>
        /// <param name="keepPercision">
        /// Keeps precision over minimal number of digits for precision
        /// </param>
        /// <returns>
        /// The formated number
        /// </returns>
        public static string FormatNumber(this decimal? input,
            bool keepPrecision)
        {
            return FormatNumber((decimal)input, keepPrecision);
        }

        /// <summary>
        /// Formats a percent value, but keeps decimal places if required
        /// </summary>
        /// <param name="input">
        /// The figure to format
        /// </param>
        /// <param name="keepPercision">
        /// Keeps precision over minimal number of digits for precision
        /// </param>
        /// <returns>
        /// The formated percent
        /// </returns>
        public static string FormatPercent(this decimal? input,
            bool keepPrecision)
        {
            return FormatPercent((decimal)input, keepPrecision);
        }

        /// <summary>
        /// Formats a currency value, but keeps decimal places if required
        /// </summary>
        /// <param name="input">
        /// The figure to format
        /// </param>
        /// <param name="keepPercision">
        /// Keeps precision over minimal number of digits for precision
        /// </param>
        /// <returns>
        /// The formated currency
        /// </returns>
        public static string FormatCurrency(this decimal? input,
            bool keepPrecision)
        {
            return FormatCurrency((decimal)input, keepPrecision);
        }
    }
}


And that's how you use it:


using System;
using AnyNamespace;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // example when you're using en-us default culture settings

            Console.WriteLine((2.5500m).FormatNumber(false));
            // => to 2.55

            Console.WriteLine((2.55546400m).FormatNumber(false));
            // => to 2.56

            Console.WriteLine((2.5500m).FormatNumber(true));
            // => to 2.55

            Console.WriteLine((2.55546400m).FormatNumber(true));
            // => to 2.555464

            Console.WriteLine((2.5500m).FormatCurrency(false));
            // => to $2.55

            Console.WriteLine((2.55546400m).FormatCurrency(false));
            // => to $2.56

            Console.WriteLine((2.5500m).FormatCurrency(true));
            // => to $2.55

            Console.WriteLine((2.55546400m).FormatCurrency(true));
            // => to $2.555464

            Console.WriteLine((2.5500m).FormatPercent(false));
            // => to 255.00%

            Console.WriteLine((2.55546400m).FormatPercent(false));
            // => to 255.55%

            Console.WriteLine((2.5500m).FormatPercent(true));
            // => to 255.00%

            Console.WriteLine((2.55546400m).FormatPercent(true));
            // => to 255.5464%


            Console.ReadKey();

        }
    }
}