< Summary - Core.Tests

Information
Class: Common.Core.Classes.ModelDataError
Assembly: Common.Core
File(s): D:\a\NuGetPackages\NuGetPackages\src\Common\Core\Classes\ModelDataError.cs
Tag: 3_8508158812
Line coverage
100%
Covered lines: 117
Uncovered lines: 0
Coverable lines: 117
Total lines: 303
Line coverage: 100%
Branch coverage
90%
Covered branches: 36
Total branches: 40
Branch coverage: 90%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_HasErrors()100%11100%
GetErrors(...)83.33%66100%
GetAllErrors()100%11100%
.ctor()100%11100%
.ctor(...)100%11100%
ValidateAllProperties()100%22100%
ValidateProperty(...)50%22100%
ClearErrors(...)100%66100%
OnErrorsChanged(...)50%22100%
AddValidationResults(...)100%88100%
AddError(...)100%22100%
GetProperties(...)75%44100%
GetDisplayNameForProperty(...)100%22100%
GetDisplayNames()100%66100%

File(s)

D:\a\NuGetPackages\NuGetPackages\src\Common\Core\Classes\ModelDataError.cs

#LineLine coverage
 1using System.Collections;
 2using System.ComponentModel;
 3using System.ComponentModel.DataAnnotations;
 4using System.Reflection;
 5using System.Runtime.CompilerServices;
 6
 7namespace Common.Core.Classes;
 8
 9/// <summary>Base class for models that required the INotifyDataErrorInfo and
 10/// INotifyPropertyChanged interfaces.</summary>
 11/// <remarks>It's base class is <see cref="ModelBase"/> so that functionality is included.<br/>
 12/// It can be used as a base class, or as an instance variable.</remarks>
 13/// <example>
 14/// In a class using it as a base class, the protected constructor will be used:
 15/// <code language="C#">
 16/// using System.ComponentModel.DataAnnotations;
 17/// using Common.Core.Classes;
 18///
 19/// public class TestModel : ModelDataError
 20/// {
 21///     public bool IsValid => !HasErrors;
 22///
 23///     private string? _name;
 24///     [Display( Name = "Full Name" )]
 25///     [Required( ErrorMessage = "Name cannot be empty." )]
 26///     public string Name
 27///     {
 28///         get => ( _name is not null ) ? _name : string.Empty;
 29///         set
 30///         {
 31///             if( value.Equals( _name ) ) return;
 32///
 33///             name = value;
 34///             ValidateProperty( value );
 35///             OnPropertyChanged();
 36///         }
 37///     }
 38///
 39///     public TestModel()
 40///     { }
 41/// }
 42/// </code>
 43/// In a class using it as an instance variable, the public constructor must be used.
 44/// <br/>The INotifyDataErrorInfo interface must be implemented and the event handler needs to
 45/// subscribe to the core error version so that the ErrorsChanged event is seen as being used:
 46/// <code language="C#">
 47/// using System.Collections;
 48/// using System.ComponentModel;
 49/// using System.ComponentModel.DataAnnotations;
 50/// using Common.Core.Classes;
 51///
 52/// public class TestModel : INotifyDataErrorInfo
 53/// {
 54///     public bool IsValid => !_validator.HasErrors;
 55///
 56///     private string? _name;
 57///     [Display( Name = "Full Name" )]
 58///     [Required( ErrorMessage = "Name cannot be empty." )]
 59///     public string Name
 60///     {
 61///         get => ( _name is not null ) ? _name : string.Empty;
 62///         set
 63///         {
 64///             if( value.Equals( _name ) ) return;
 65///             name = value;
 66///             _validator.ValidateProperty( value );
 67///             OnPropertyChanged();
 68///         }
 69///     }
 70///
 71///     private readonly ModelDataError _validator;
 72///
 73///     public TestModel()
 74///     {
 75///         // Do this before anything else
 76///         _validator = new ModelDataError( this );
 77///         _validator.ErrorsChanged += Core_ErrorsChanged;
 78///     }
 79///
 80///     public bool HasErrors => _validator.HasErrors;
 81///
 82///     public IEnumerable GetErrors( string? propertyName )
 83///     {
 84///         return _validator.GetErrors( propertyName );
 85///     }
 86///
 87///     public event EventHandler&lt;DataErrorsChangedEventArgs&gt;? ErrorsChanged;
 88///
 89///     private void Core_ErrorsChanged( object? sender, DataErrorsChangedEventArgs e )
 90///     {
 91///         ErrorsChanged?.Invoke( this, e );
 92///     }
 93/// }
 94/// </code>
 95/// </example>
 96public class ModelDataError : ModelBase, INotifyDataErrorInfo
 97{
 98  #region INotifyDataErrorInfo Implementation
 99
 100  /// <summary>Gets a value that indicates whether the entity has validation errors.</summary>
 5101  public bool HasErrors => _errors.Any( propErrors => propErrors.Value.Count > 0 );
 102
 103  /// <summary>Gets the validation errors for a specified property or for the entire entity.</summary>
 104  /// <param name="propertyName">The name of the property to retrieve validation errors for; or <see langword="null"/>
 105  /// or Empty, to retrieve entity-level errors.</param>
 106  /// <returns>The validation errors for the property or entity.</returns>
 107  public IEnumerable GetErrors( string? propertyName )
 3108  {
 109    // Get entity-level errors when the target property is null or empty
 3110    if( string.IsNullOrEmpty( propertyName ) )
 1111    {
 112      // Local function to gather all the entity-level errors
 113      [MethodImpl( MethodImplOptions.NoInlining )]
 114      IEnumerable<ValidationResult> GetAllErrors()
 1115      {
 1116        return _errors.Values.SelectMany( static errors => errors );
 1117      }
 118
 1119      return GetAllErrors();
 120    }
 121
 122    // Property-level errors, if any
 2123    if( propertyName is not null && _errors.TryGetValue( propertyName, out List<ValidationResult>? value ) )
 1124    {
 1125      return value;
 126    }
 127
 128    // Property not found
 1129    return Array.Empty<ValidationResult>();
 3130  }
 131
 132  /// <summary>Occurs when the validation errors have changed for a property or for the entire entity.</summary>
 133  public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
 134
 135  #endregion
 136
 137  #region Constructors and Variables
 138
 5139  private readonly Dictionary<string, List<ValidationResult>> _errors = [];
 5140  private readonly ConditionalWeakTable<Type, Dictionary<string, string>> _displayNamesMap = [];
 141  private readonly PropertyInfo[] _properties;
 142  private readonly ValidationContext _context;
 143
 144  /// <summary>Initializes a new instance of the ModelDataError class.</summary>
 1145  protected ModelDataError()
 1146  {
 1147    _properties = GetProperties( GetType() );
 1148    _context = new ValidationContext( this );
 1149  }
 150
 151  /// <summary>Initializes a new instance of the ModelDataError class.</summary>
 152  /// <param name="instance">Object being validated.</param>
 4153  public ModelDataError( object instance )
 4154  {
 4155    _properties = GetProperties( instance.GetType() );
 4156    _context = new ValidationContext( instance );
 4157  }
 158
 159  #endregion
 160
 161  #region Public Methods
 162
 163  /// <summary>Validates all the properties in the current instance.</summary>
 164  public void ValidateAllProperties()
 3165  {
 21166    foreach( var propertyInfo in _properties )
 6167    {
 6168      ValidateProperty( propertyInfo.GetValue( _context.ObjectInstance ), propertyInfo.Name );
 6169    }
 3170  }
 171
 172  /// <summary>Validates a property with a specified name and a given input value.</summary>
 173  /// <param name="value">The value to test for the specified property.</param>
 174  /// <param name="propertyName">The name of the property to validate.</param>
 175  /// <returns><see langword="true"/> if the value is valid, <see langword="false"/> if any errors are found.</returns>
 176  public bool ValidateProperty( object? value, [CallerMemberName] string propertyName = "" )
 7177  {
 7178    if( string.IsNullOrEmpty( propertyName ) ) return true;
 179
 7180    _context.MemberName = propertyName;
 7181    _context.DisplayName = GetDisplayNameForProperty( propertyName );
 182
 7183    var results = new List<ValidationResult>();
 184    try
 7185    {
 7186      Validator.TryValidateProperty( value, _context, results );
 6187    }
 1188    catch( Exception ex )
 1189    {
 190      // Handle System.InvalidCastException exception
 191      // Unable to cast object of type 'System.DateTime' to type 'System.String'.
 1192      results.Add( new ValidationResult( ex.Message ) );
 1193    }
 7194    ClearErrors( propertyName );
 7195    AddValidationResults( results );
 196
 7197    return results.Count == 0;
 7198  }
 199
 200  /// <summary>Clears the validation errors for a specified property or for the entire entity.</summary>
 201  /// <param name="propertyName">The name of the property to clear validation errors for.<br/>
 202  /// If a <see langword="null"/> or empty name is used, all entity-level errors will be cleared.
 203  /// </param>
 204  public void ClearErrors( string? propertyName = null )
 10205  {
 10206    if( !string.IsNullOrEmpty( propertyName ) )
 9207    {
 9208      if( _errors.Remove( propertyName ) )
 2209      {
 2210        OnErrorsChanged( propertyName );
 2211      }
 9212    }
 213    else
 1214    {
 7215      foreach( KeyValuePair<string, List<ValidationResult>> property in _errors )
 2216      {
 2217        ClearErrors( property.Key );
 2218      }
 1219    }
 10220  }
 221
 222  #endregion
 223
 224  #region Private Methods
 225
 226  private void OnErrorsChanged( string propertyName )
 6227  {
 6228    ErrorsChanged?.Invoke( this, new DataErrorsChangedEventArgs( propertyName ) );
 6229  }
 230
 231  private void AddValidationResults( List<ValidationResult> results )
 7232  {
 11233    if( results.Count == 0 ) { return; }
 234
 235    // Group validation results by property names
 5236    var resultsByPropName = from res in results
 9237                from mname in res.MemberNames
 8238                group res by mname into g
 9239                select g;
 240
 23241    foreach( var property in resultsByPropName )
 4242    {
 8243      results = property.Select( res => res ).ToList();
 4244      if( results.Count > 0 )
 4245      {
 20246        foreach( var result in results )
 4247        {
 4248          AddError( property.Key, result );
 4249        }
 4250      }
 4251    }
 7252  }
 253
 254  private void AddError( string propertyName, ValidationResult error )
 4255  {
 4256    if( !_errors.TryGetValue( propertyName, out List<ValidationResult>? value ) )
 4257    {
 4258      value = ( [] );
 4259      _errors[propertyName] = value;
 4260    }
 261
 4262    value.Add( error );
 4263    OnErrorsChanged( propertyName );
 4264  }
 265
 266  private static PropertyInfo[] GetProperties<T>( T type ) where T : Type
 5267  {
 268    // Get all properties with data annotations
 5269    PropertyInfo[] validationProperties = (
 5270      from propInfo in type.GetProperties( BindingFlags.Instance | BindingFlags.Public )
 10271      where propInfo.GetIndexParameters().Length == 0 &&
 10272           propInfo.GetCustomAttributes<ValidationAttribute>( true ).Any()
 5273      select propInfo ).ToArray();
 274
 5275    return validationProperties;
 5276  }
 277
 278  private string GetDisplayNameForProperty( string propertyName )
 7279  {
 280    static Dictionary<string, string> GetDisplayNames( Type type )
 4281    {
 4282      Dictionary<string, string> displayNames = [];
 283
 28284      foreach( PropertyInfo property in type.GetProperties( BindingFlags.Instance | BindingFlags.Public ) )
 8285      {
 8286        if( property.GetCustomAttribute<DisplayAttribute>() is DisplayAttribute attribute &&
 8287          attribute.GetName() is string displayName )
 3288        {
 3289          displayNames.Add( property.Name, displayName );
 3290        }
 8291      }
 292
 4293      return displayNames;
 4294    }
 295
 7296    _ = _displayNamesMap.GetValue( _context.ObjectInstance.GetType(),
 11297        static t => GetDisplayNames( t ) ).TryGetValue( propertyName, out string? displayName );
 298
 7299    return displayName ?? propertyName;
 7300  }
 301
 302  #endregion
 303}