diff --git a/README.md b/README.md index a808e08..7ad4418 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ enumerable lengths - XRechnung routing validation - European VAT ID validation - XML validation +- Hostname or IP address validation - Conditional value requirements - Enumeration value validation - Validation references @@ -81,6 +82,7 @@ The **ObjectValidation-CountryValidator** extension is licensed using the | XRechnung routing validation | `XRechnungRouteAttribute` | | European VAT ID validation | `EuVatIdAttribute` | | XML validation | `XmlAttribute` | +| Hostname or IP address validation | `HostAttribute` | | Conditional value requirement | `RequiredIfAttribute` | | Allowed/denied values | `AllowedValuesAttribute`, `DeniedValuesAttribute` | | Enumeration value | (none - using the type) | @@ -298,6 +300,7 @@ These item validation adapters exist: | `CompareAttribute` | `ItemCompareAttribute` | | `CreditCardAttribute` | `ItemCreditCardAttribute` | | `EmailAddressAttribute` | `ItemEmailAddressAttribute` | +| `HostAttribute` | `ItemHostAttribute` | | `MaxLengthAttribute` | `ItemMaxLengthAttribute` | | `MinLengthAttribute` | `ItemMinLengthAttribute` | | `NoValidationAttribute` | `ItemNoValidationAttribute` | diff --git a/src/ObjectValidation CountryValidator/ObjectValidation CountryValidator.csproj b/src/ObjectValidation CountryValidator/ObjectValidation CountryValidator.csproj index e28aeec..1ecab43 100644 --- a/src/ObjectValidation CountryValidator/ObjectValidation CountryValidator.csproj +++ b/src/ObjectValidation CountryValidator/ObjectValidation CountryValidator.csproj @@ -9,7 +9,7 @@ True ObjectValidation-CountryValidator ObjectValidation-CountryValidator - 2.3.0 + 2.4.0 nd1012 Andreas Zimmermann, wan24.de ObjectValidation @@ -29,7 +29,7 @@ - + diff --git a/src/ObjectValidation Tests/A_Initialization.cs b/src/ObjectValidation Tests/A_Initialization.cs new file mode 100644 index 0000000..220bd55 --- /dev/null +++ b/src/ObjectValidation Tests/A_Initialization.cs @@ -0,0 +1,9 @@ +namespace ObjectValidation_Tests +{ + [TestClass] + public class A_Initialization + { + [AssemblyInitialize] + public static void Init(TestContext tc) => wan24.Tests.TestsInitialization.Init(tc); + } +} diff --git a/src/ObjectValidation Tests/Aba_Tests.cs b/src/ObjectValidation Tests/Aba_Tests.cs index e72b153..b6853be 100644 --- a/src/ObjectValidation Tests/Aba_Tests.cs +++ b/src/ObjectValidation Tests/Aba_Tests.cs @@ -1,9 +1,10 @@ using wan24.ObjectValidation; +using wan24.Tests; namespace ObjectValidation_Tests { [TestClass] - public class Aba_Tests + public class Aba_Tests : TestBase { [TestMethod] public void AbaRtn_Tests() diff --git a/src/ObjectValidation Tests/EuVatId_Tests.cs b/src/ObjectValidation Tests/EuVatId_Tests.cs index 253dd7c..7f1425f 100644 --- a/src/ObjectValidation Tests/EuVatId_Tests.cs +++ b/src/ObjectValidation Tests/EuVatId_Tests.cs @@ -1,9 +1,10 @@ using wan24.ObjectValidation; +using wan24.Tests; namespace ObjectValidation_Tests { [TestClass] - public class EuVatId_Tests + public class EuVatId_Tests : TestBase { [TestMethod] public void VatId_Tests() diff --git a/src/ObjectValidation Tests/InvalidTestObject.cs b/src/ObjectValidation Tests/InvalidTestObject.cs index 59f81e2..9e12dad 100644 --- a/src/ObjectValidation Tests/InvalidTestObject.cs +++ b/src/ObjectValidation Tests/InvalidTestObject.cs @@ -41,6 +41,7 @@ public InvalidTestObject() : base() RequiredIfConditional = true; EnumProperty = TestEnum.Invalid; Enum2Property = (TestEnum)3; + HostProperty = "test 123"; } } } diff --git a/src/ObjectValidation Tests/Luhn_Tests.cs b/src/ObjectValidation Tests/Luhn_Tests.cs index f903f50..fb3fd63 100644 --- a/src/ObjectValidation Tests/Luhn_Tests.cs +++ b/src/ObjectValidation Tests/Luhn_Tests.cs @@ -1,9 +1,10 @@ using wan24.ObjectValidation; +using wan24.Tests; namespace ObjectValidation_Tests { [TestClass] - public class Luhn_Tests + public class Luhn_Tests : TestBase { [TestMethod] public void Luhn_Checksum_Tests() diff --git a/src/ObjectValidation Tests/ObjectValidation Tests.csproj b/src/ObjectValidation Tests/ObjectValidation Tests.csproj index a1f193e..1c28e44 100644 --- a/src/ObjectValidation Tests/ObjectValidation Tests.csproj +++ b/src/ObjectValidation Tests/ObjectValidation Tests.csproj @@ -12,13 +12,14 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/ObjectValidation Tests/ObjectValidation_Tests.cs b/src/ObjectValidation Tests/ObjectValidation_Tests.cs index 7ed3582..3f2bb68 100644 --- a/src/ObjectValidation Tests/ObjectValidation_Tests.cs +++ b/src/ObjectValidation Tests/ObjectValidation_Tests.cs @@ -1,10 +1,11 @@ using System.ComponentModel.DataAnnotations; using wan24.ObjectValidation; +using wan24.Tests; namespace ObjectValidation_Tests { [TestClass] - public class ObjectValidation_Tests + public class ObjectValidation_Tests : TestBase { public ObjectValidation_Tests() { @@ -84,6 +85,7 @@ from name in res.MemberNames Assert.IsTrue(failedMembers.Contains(nameof(ValidTestObject.RequiredProperty))); Assert.IsTrue(failedMembers.Contains(nameof(ValidTestObject.EnumProperty))); Assert.IsTrue(failedMembers.Contains(nameof(ValidTestObject.Enum2Property))); + Assert.IsTrue(failedMembers.Contains(nameof(ValidTestObject.HostProperty))); Assert.AreEqual(54, results.Count); // Validation exception diff --git a/src/ObjectValidation Tests/Swift_Tests.cs b/src/ObjectValidation Tests/Swift_Tests.cs index 12f401a..5ec2f32 100644 --- a/src/ObjectValidation Tests/Swift_Tests.cs +++ b/src/ObjectValidation Tests/Swift_Tests.cs @@ -1,9 +1,10 @@ using wan24.ObjectValidation; +using wan24.Tests; namespace ObjectValidation_Tests { [TestClass] - public class Swift_Tests + public class Swift_Tests : TestBase { [TestMethod] public void Bic_Tests() diff --git a/src/ObjectValidation Tests/ValidTestObject.cs b/src/ObjectValidation Tests/ValidTestObject.cs index 142cb9f..020ef41 100644 --- a/src/ObjectValidation Tests/ValidTestObject.cs +++ b/src/ObjectValidation Tests/ValidTestObject.cs @@ -151,6 +151,9 @@ public class ValidTestObject : IValidatableObject public TestEnum Enum2Property { get; set; } = TestEnum.Valid; + [Host(CheckIfExists = true)] + public string HostProperty { get; set; } = "localhost"; + IEnumerable IValidatableObject.Validate(ValidationContext validationContext) { List results = new(); diff --git a/src/ObjectValidation Tests/XRechnung_Tests.cs b/src/ObjectValidation Tests/XRechnung_Tests.cs index 39fd3c5..0263131 100644 --- a/src/ObjectValidation Tests/XRechnung_Tests.cs +++ b/src/ObjectValidation Tests/XRechnung_Tests.cs @@ -1,9 +1,10 @@ using wan24.ObjectValidation; +using wan24.Tests; namespace ObjectValidation_Tests { [TestClass] - public class XRechnung_Tests + public class XRechnung_Tests : TestBase { [TestMethod] public void XRechnung_Route_Tests() diff --git a/src/ObjectValidation/CountryAttribute.cs b/src/ObjectValidation/CountryAttribute.cs index 44a0e01..510debb 100644 --- a/src/ObjectValidation/CountryAttribute.cs +++ b/src/ObjectValidation/CountryAttribute.cs @@ -5,13 +5,11 @@ namespace wan24.ObjectValidation /// /// Country code validation attribute /// - public class CountryAttribute : ValidationAttributeBase + /// + /// Constructor + /// + public class CountryAttribute() : ValidationAttributeBase() { - /// - /// Constructor - /// - public CountryAttribute() : base() { } - /// protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { diff --git a/src/ObjectValidation/CountryCodes.cs b/src/ObjectValidation/CountryCodes.cs index 3ce1542..2280267 100644 --- a/src/ObjectValidation/CountryCodes.cs +++ b/src/ObjectValidation/CountryCodes.cs @@ -1,4 +1,6 @@ -namespace wan24.ObjectValidation +using System.Collections.Frozen; + +namespace wan24.ObjectValidation { /// /// Country ISO 3166-1 alpha-2 codes diff --git a/src/ObjectValidation/CurrencyCodes.cs b/src/ObjectValidation/CurrencyCodes.cs index 599ce00..82c2b57 100644 --- a/src/ObjectValidation/CurrencyCodes.cs +++ b/src/ObjectValidation/CurrencyCodes.cs @@ -1,4 +1,6 @@ -namespace wan24.ObjectValidation +using System.Collections.Frozen; + +namespace wan24.ObjectValidation { /// /// Currency ISO 4217 codes @@ -298,7 +300,7 @@ public static class CurrencyCodes /// Numeric code /// Name /// Minor unit - public class Currency(string code, string numericCode, string name, int minorUnit = 2) + public record class Currency(string code, string numericCode, string name, int minorUnit = 2) { /// /// Factor @@ -328,14 +330,14 @@ public class Currency(string code, string numericCode, string name, int minorUni /// /// Factor /// - public int Factor => _Factor ??= (int)Math.Pow(10, MinorUnit); + public virtual int Factor => _Factor ??= (int)Math.Pow(10, MinorUnit); /// /// Validate a value /// /// Value /// Valid? - public bool Validate(decimal value) => Factor < 1 ? Math.Round(value) == value : Math.Round(value * Factor) / Factor == value; + public virtual bool Validate(decimal value) => Factor < 1 ? Math.Round(value) == value : Math.Round(value * Factor) / Factor == value; } } } diff --git a/src/ObjectValidation/EuVatId.cs b/src/ObjectValidation/EuVatId.cs index 392b0c8..71842d7 100644 --- a/src/ObjectValidation/EuVatId.cs +++ b/src/ObjectValidation/EuVatId.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Collections.Frozen; +using System.Text.RegularExpressions; namespace wan24.ObjectValidation { @@ -15,7 +16,7 @@ public static partial class EuVatId /// /// VAT ID syntax regular expressions /// - private static readonly Dictionary Syntax = new() + private static readonly FrozenDictionary Syntax = new Dictionary() { {"AT", AT_Generated()}, {"BE", BE_Generated()}, @@ -45,7 +46,7 @@ public static partial class EuVatId {"SI", SI_Generated()}, {"SK", SK_Generated()}, {"XI", XI_Generated()}// North Ireland, since Brexit - }; + }.ToFrozenDictionary(); /// /// Validate a European VAT ID diff --git a/src/ObjectValidation/HostAttribute.cs b/src/ObjectValidation/HostAttribute.cs new file mode 100644 index 0000000..21cf72a --- /dev/null +++ b/src/ObjectValidation/HostAttribute.cs @@ -0,0 +1,86 @@ +using System.ComponentModel.DataAnnotations; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Security.Cryptography; + +namespace wan24.ObjectValidation +{ + /// + /// Host name or IP address validation attribute + /// + /// + /// Constructor + /// + public class HostAttribute() : ValidationAttributeBase() + { + /// + /// If IPv4 addresses are allowed + /// + public bool AllowIPv4 { get; set; } = true; + + /// + /// If IPv6 addresses are allowed + /// + public bool AllowIPv6 { get; set; } = true; + + /// + /// Check if the hostname (DNS lookup) or IP address (ICMP) exists + /// + public bool CheckIfExists { get; set; } + + /// + /// Ping timeout in ms + /// + public int PingTimeout { get; set; } = 300; + + /// + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is null) return null; + if (value is not string str) return this.CreateValidationResult($"Hostname or IP address value as {typeof(string)} expected", validationContext); + UriHostNameType type = Uri.CheckHostName(str); + IPAddress? ip; + switch (type) + { + case UriHostNameType.Dns: + if (!CheckIfExists) return null; + try + { + Dns.GetHostEntry(str); + return null; + } + catch (Exception ex) + { + return this.CreateValidationResult($"Hostname DNS lookup failed: {ex.Message ?? ex.GetType().ToString()}", validationContext); + } + case UriHostNameType.IPv4: + if(!IPAddress.TryParse(str, out ip)) return this.CreateValidationResult($"Host IPv4 address parsing failed", validationContext); + if (ip.AddressFamily != AddressFamily.InterNetwork) return this.CreateValidationResult($"Detected host IPv4 address parsed to IPv6", validationContext); + break; + case UriHostNameType.IPv6: + if (!IPAddress.TryParse(str, out ip)) return this.CreateValidationResult($"Host IPv6 address parsing failed", validationContext); + if (ip.AddressFamily != AddressFamily.InterNetworkV6) return this.CreateValidationResult($"Detected host IPv6 address parsed to IPv4", validationContext); + break; + default: + return this.CreateValidationResult($"Hostname or IP address value invalid ({type})", validationContext); + } + if (!CheckIfExists) return null; + using Ping ping = new(); + try + { + PingReply pong = ping.Send(ip, PingTimeout, RandomNumberGenerator.GetBytes(count: 32), new() + { + DontFragment = true + }); + return pong.Status == IPStatus.Success + ? null + : this.CreateValidationResult($"Ping to {ip} failed: {pong.Status}", validationContext); + } + catch(Exception ex) + { + return this.CreateValidationResult($"Ping to {ip} failed exceptional: {ex.Message ?? ex.GetType().ToString()}", validationContext); + } + } + } +} diff --git a/src/ObjectValidation/IValidationInfo.cs b/src/ObjectValidation/IValidationInfo.cs index c435b42..fcc7bb6 100644 --- a/src/ObjectValidation/IValidationInfo.cs +++ b/src/ObjectValidation/IValidationInfo.cs @@ -8,7 +8,7 @@ public interface IValidationInfo /// /// Seen objects /// - List Seen { get; } + HashSet Seen { get; } /// /// Current validation depth /// diff --git a/src/ObjectValidation/ItemHostAttribute.cs b/src/ObjectValidation/ItemHostAttribute.cs new file mode 100644 index 0000000..3137f4d --- /dev/null +++ b/src/ObjectValidation/ItemHostAttribute.cs @@ -0,0 +1,48 @@ +namespace wan24.ObjectValidation +{ + /// + /// Host name or IP address validation attribute + /// + /// + /// Constructor + /// + /// Validation target + public class ItemHostAttribute(ItemValidationTargets target = ItemValidationTargets.Item) : ItemValidationAttribute(target, new HostAttribute()) + { + /// + /// If IPv4 addresses are allowed + /// + public bool AllowIPv4 + { + get => ((HostAttribute)ValidationAttribute).AllowIPv4; + set => ((HostAttribute)ValidationAttribute).AllowIPv4 = value; + } + + /// + /// If IPv6 addresses are allowed + /// + public bool AllowIPv6 + { + get => ((HostAttribute)ValidationAttribute).AllowIPv6; + set => ((HostAttribute)ValidationAttribute).AllowIPv6 = value; + } + + /// + /// Check if the hostname (DNS lookup) or IP address (ICMP) exists + /// + public bool CheckIfExists + { + get => ((HostAttribute)ValidationAttribute).CheckIfExists; + set => ((HostAttribute)ValidationAttribute).CheckIfExists = value; + } + + /// + /// Ping timeout in ms + /// + public int PingTimeout + { + get => ((HostAttribute)ValidationAttribute).PingTimeout; + set => ((HostAttribute)ValidationAttribute).PingTimeout = value; + } + } +} diff --git a/src/ObjectValidation/ObjectValidation.csproj b/src/ObjectValidation/ObjectValidation.csproj index af765d0..f174134 100644 --- a/src/ObjectValidation/ObjectValidation.csproj +++ b/src/ObjectValidation/ObjectValidation.csproj @@ -22,7 +22,7 @@ LICENSE True README.md - 2.6.0 + 2.7.0 embedded true Debug;Release;Trunk diff --git a/src/ObjectValidation/ObjectValidationEventArgs.cs b/src/ObjectValidation/ObjectValidationEventArgs.cs index 3249348..bc7ece0 100644 --- a/src/ObjectValidation/ObjectValidationEventArgs.cs +++ b/src/ObjectValidation/ObjectValidationEventArgs.cs @@ -22,7 +22,7 @@ public sealed class ObjectValidationEventArgs : CancelEventArgs /// Current result /// Current property public ObjectValidationEventArgs( - List seen, + HashSet seen, object obj, List validationResults, IReadOnlyList allResults, @@ -47,7 +47,7 @@ public ObjectValidationEventArgs( /// /// Seen objects /// - public List Seen { get; } + public HashSet Seen { get; } /// /// Object diff --git a/src/ObjectValidation/README.md b/src/ObjectValidation/README.md index a808e08..7ad4418 100644 --- a/src/ObjectValidation/README.md +++ b/src/ObjectValidation/README.md @@ -24,6 +24,7 @@ enumerable lengths - XRechnung routing validation - European VAT ID validation - XML validation +- Hostname or IP address validation - Conditional value requirements - Enumeration value validation - Validation references @@ -81,6 +82,7 @@ The **ObjectValidation-CountryValidator** extension is licensed using the | XRechnung routing validation | `XRechnungRouteAttribute` | | European VAT ID validation | `EuVatIdAttribute` | | XML validation | `XmlAttribute` | +| Hostname or IP address validation | `HostAttribute` | | Conditional value requirement | `RequiredIfAttribute` | | Allowed/denied values | `AllowedValuesAttribute`, `DeniedValuesAttribute` | | Enumeration value | (none - using the type) | @@ -298,6 +300,7 @@ These item validation adapters exist: | `CompareAttribute` | `ItemCompareAttribute` | | `CreditCardAttribute` | `ItemCreditCardAttribute` | | `EmailAddressAttribute` | `ItemEmailAddressAttribute` | +| `HostAttribute` | `ItemHostAttribute` | | `MaxLengthAttribute` | `ItemMaxLengthAttribute` | | `MinLengthAttribute` | `ItemMinLengthAttribute` | | `NoValidationAttribute` | `ItemNoValidationAttribute` | diff --git a/src/ObjectValidation/ReflectionHelper.cs b/src/ObjectValidation/ReflectionHelper.cs index ec0c129..d081f86 100644 --- a/src/ObjectValidation/ReflectionHelper.cs +++ b/src/ObjectValidation/ReflectionHelper.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Reflection; namespace wan24.ObjectValidation @@ -8,6 +9,11 @@ namespace wan24.ObjectValidation /// public static class ReflectionHelper { + /// + /// Never validate attribute full type (ASP.NET) + /// + internal const string VALIDATENEVER_ATTRIBUTE_TYPE = "Microsoft.AspNetCore.Mvc.ModelBinding.Validation.NeverValidateAttribute"; + /// /// CreateGetterDelegate method /// @@ -19,15 +25,15 @@ public static class ReflectionHelper /// /// Item validation attribute cache /// - private static readonly ConcurrentDictionary ItemValidationAttributeCache; + private static readonly ConcurrentDictionary> ItemValidationAttributeCache; /// /// cache /// - private static readonly ConcurrentDictionary PropertyInfoCache; + private static readonly ConcurrentDictionary> PropertyInfoCache; /// /// cache /// - private static readonly ConcurrentDictionary AttributeCache; + private static readonly ConcurrentDictionary> AttributeCache; /// /// Static constructor @@ -61,10 +67,10 @@ static ReflectionHelper() /// /// Object /// Attributes - public static IItemValidationAttribute[] GetItemValidationAttributes(this ICustomAttributeProvider cap) + public static FrozenSet GetItemValidationAttributes(this ICustomAttributeProvider cap) => ItemValidationAttributeCache.GetOrAdd( cap.GetHashCode(), - (key) => GetCustomAttributesCached(cap).Where(a => a is IItemValidationAttribute).Cast().ToArray() + (key) => GetCustomAttributesCached(cap).Where(a => a is IItemValidationAttribute).Cast().ToFrozenSet() ); /// @@ -72,10 +78,18 @@ public static IItemValidationAttribute[] GetItemValidationAttributes(this ICusto /// /// Type /// Properties - public static PropertyInfo[] GetPropertiesCached(this Type type) + public static FrozenSet GetPropertiesCached(this Type type) => PropertyInfoCache.GetOrAdd( type.GetHashCode(), - (key) => type.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.CanRead && p.GetMethod!.IsPublic && p.GetIndexParameters().Length == 0).ToArray() + (key) => type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where( + p => (p.GetMethod?.IsPublic ?? false) && + !p.PropertyType.IsByRef && + !p.PropertyType.IsByRefLike && + p.GetIndexParameters().Length == 0 && + !p.GetCustomAttributesCached().Any(a => (a is NoValidationAttribute attr && attr.SkipNullValueCheck) || a.GetType().FullName == VALIDATENEVER_ATTRIBUTE_TYPE) + ) + .ToFrozenSet() ); /// @@ -83,10 +97,10 @@ public static PropertyInfo[] GetPropertiesCached(this Type type) /// /// Object /// Attributes - public static Attribute[] GetCustomAttributesCached(this ICustomAttributeProvider cap) + public static FrozenSet GetCustomAttributesCached(this ICustomAttributeProvider cap) => AttributeCache.GetOrAdd( cap.GetHashCode(), - (key) => cap.GetCustomAttributes(inherit: true).Cast().ToArray() + (key) => cap.GetCustomAttributes(inherit: true).Cast().ToFrozenSet() ); /// diff --git a/src/ObjectValidation/ValidatableObjectBase.cs b/src/ObjectValidation/ValidatableObjectBase.cs index 1d6cbf5..256ca06 100644 --- a/src/ObjectValidation/ValidatableObjectBase.cs +++ b/src/ObjectValidation/ValidatableObjectBase.cs @@ -5,13 +5,11 @@ namespace wan24.ObjectValidation /// /// Base class for an object validatable object /// - public abstract class ValidatableObjectBase : IObjectValidatable + /// + /// Constructor + /// + public abstract class ValidatableObjectBase() : IObjectValidatable { - /// - /// Constructor - /// - protected ValidatableObjectBase() { } - /// /// Validate the object /// diff --git a/src/ObjectValidation/ValidatableRecordBase.cs b/src/ObjectValidation/ValidatableRecordBase.cs index 1087f51..37f13a8 100644 --- a/src/ObjectValidation/ValidatableRecordBase.cs +++ b/src/ObjectValidation/ValidatableRecordBase.cs @@ -5,13 +5,11 @@ namespace wan24.ObjectValidation /// /// Base class for an object validatable object /// - public abstract record class ValidatableRecordBase : IObjectValidatable + /// + /// Constructor + /// + public abstract record class ValidatableRecordBase() : IObjectValidatable { - /// - /// Constructor - /// - protected ValidatableRecordBase() { } - /// /// Validate the object /// diff --git a/src/ObjectValidation/ValidatableTypes.cs b/src/ObjectValidation/ValidatableTypes.cs index ac2c6b1..1a1068f 100644 --- a/src/ObjectValidation/ValidatableTypes.cs +++ b/src/ObjectValidation/ValidatableTypes.cs @@ -1,7 +1,4 @@ -using System.Collections.Concurrent; -using System.Reflection; - -namespace wan24.ObjectValidation +namespace wan24.ObjectValidation { /// /// Validatable types @@ -11,19 +8,19 @@ public static class ValidatableTypes /// /// Types which are forced to be validated /// - public static readonly ConcurrentBag ForcedTypes; + public static readonly HashSet ForcedTypes; /// /// Types (also inherited) which are forced to be validated /// - public static readonly ConcurrentBag ForcedTypesInheritable; + public static readonly HashSet ForcedTypesInheritable; /// /// Types which are denied to be validated /// - public static readonly ConcurrentBag DeniedTypes; + public static readonly HashSet DeniedTypes; /// /// Types (also inherited) which are denied to be validated /// - public static readonly ConcurrentBag DeniedTypesInheritable; + public static readonly HashSet DeniedTypesInheritable; /// /// Constructor @@ -32,8 +29,8 @@ static ValidatableTypes() { ForcedTypes = []; ForcedTypesInheritable = []; - DeniedTypes = new(new Type[] { typeof(string), typeof(object), typeof(IQueryable<>) }); - DeniedTypesInheritable = new(new Type[] { typeof(Type), typeof(Stream) }); + DeniedTypes = new([typeof(string), typeof(object), typeof(IQueryable<>)]); + DeniedTypesInheritable = new([typeof(Type), typeof(Stream)]); } /// @@ -49,28 +46,28 @@ public static bool IsTypeValidatable(Type type) { Type? gtd = type.IsGenericType ? type.GetGenericTypeDefinition() : null;// Generic type definition res = ForcedTypes.Contains(type) || // Forced - ForcedTypesInheritable.Any(t=>t.IsAssignableFrom(type)) || + ForcedTypesInheritable.Any(t => t.IsAssignableFrom(type)) || ( // Forced generic type definition gtd is not null && ( ForcedTypes.Contains(gtd) || - ForcedTypesInheritable.Any(t=>t.IsAssignableFrom(gtd)) + ForcedTypesInheritable.Any(t => t.IsAssignableFrom(gtd)) ) ) || ( !DeniedTypes.Contains(type) && // Not denied - !DeniedTypesInheritable.Any(t=>t.IsAssignableFrom(type)) && + !DeniedTypesInheritable.Any(t => t.IsAssignableFrom(type)) && ( // Not denied generic type definition gtd is null || ( !DeniedTypes.Contains(gtd) && - !DeniedTypesInheritable.Any(t=>t.IsAssignableFrom(gtd)) + !DeniedTypesInheritable.Any(t => t.IsAssignableFrom(gtd)) ) ) && !( // Other type restrictions (type.IsValueType && !type.IsEnum) || // Not a non-enum value type type.IsArray || // Not an array - type.GetCustomAttribute(inherit: true) is not null // Not ignored + type.GetCustomAttributesCached().Any(a => a is NoValidationAttribute)// Not ignored ) ); if (res) return res;// Validate, if validatable in this moment diff --git a/src/ObjectValidation/ValidationExtensions.Events.cs b/src/ObjectValidation/ValidationExtensions.Events.cs index 575db9e..b01cd93 100644 --- a/src/ObjectValidation/ValidationExtensions.Events.cs +++ b/src/ObjectValidation/ValidationExtensions.Events.cs @@ -22,7 +22,7 @@ public static partial class ValidationExtensions /// If the event was cancelled, the new result, and if the validation failed during the event internal static (bool Cancelled, bool NewResult, bool Failed) RaiseEvent( ObjectValidation_Delegate? delegates, - List seen, + HashSet seen, object obj, List validationResults, IReadOnlyList allResults, diff --git a/src/ObjectValidation/ValidationExtensions.Internal.cs b/src/ObjectValidation/ValidationExtensions.Internal.cs index 09e7f82..c850124 100644 --- a/src/ObjectValidation/ValidationExtensions.Internal.cs +++ b/src/ObjectValidation/ValidationExtensions.Internal.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Frozen; using System.ComponentModel.DataAnnotations; using System.Data; using System.Diagnostics; @@ -10,15 +11,10 @@ namespace wan24.ObjectValidation // Internals public static partial class ValidationExtensions { - /// - /// Never validate attribute full type (ASP.NET) - /// - private const string VALIDATENEVER_ATTRIBUTE_TYPE = "Microsoft.AspNetCore.Mvc.ModelBinding.Validation.NeverValidateAttribute"; - /// /// Unsigned numeric enum types /// - private static readonly Type[] UnsignedNumericEnumTypes = [typeof(byte), typeof(ushort), typeof(uint), typeof(ulong)]; + private static readonly FrozenSet UnsignedNumericEnumTypes = new Type[] { typeof(byte), typeof(ushort), typeof(uint), typeof(ulong) }.ToFrozenSet(); /// /// Validate an object @@ -39,9 +35,10 @@ internal static bool ValidateObject( string? member, bool throwOnError, IEnumerable? members = null, - IServiceProvider? serviceProvider = null) + IServiceProvider? serviceProvider = null + ) { - // Update array level and recursion informations + // Update array level and recursion information if (info.ArrayLevel != 0) { info = info.GetClone(); @@ -56,9 +53,8 @@ internal static bool ValidateObject( // Skip object that disabled the validation or which has a not supported type if (!ValidatableTypes.IsTypeValidatable(type)) return true; // Avoid an endless recursion - if (info.Seen.Contains(obj)) return true; seenIndex = info.Seen.Count; - info.Seen.Add(obj); + if (!info.Seen.Add(obj)) return true; // Prepare the results List validationResults = [],// Single validation results (will be added to all validation results after a validation) allResults = [];// All validation results (will be added to the given results list, if any) @@ -123,7 +119,7 @@ bool Finalize() if (type.IsEnum) { Type numericType = type.GetEnumUnderlyingType() ?? throw new InvalidProgramException($"Enumeration {type} {contextInfo} without underlying numeric type"); - if (type.GetCustomAttribute() is not null) + if (type.GetCustomAttributesCached().Any(a => a is FlagsAttribute)) { bool err; object number, @@ -131,7 +127,9 @@ bool Finalize() if (UnsignedNumericEnumTypes.Contains(numericType)) { ulong allValues = 0, - numericValue = (ulong)Convert.ChangeType(Convert.ChangeType(obj, numericType), typeof(ulong)); + numericValue = numericType == typeof(ulong) + ? (ulong)Convert.ChangeType(obj, numericType) + : (ulong)Convert.ChangeType(Convert.ChangeType(obj, numericType), typeof(ulong)); number = numericValue; foreach (object v in Enum.GetValues(type)) allValues |= (ulong)Convert.ChangeType(Convert.ChangeType(v, numericType), typeof(ulong)); err = (numericValue & ~allValues) != 0; @@ -140,7 +138,9 @@ bool Finalize() else { long allValues = 0, - numericValue = (long)Convert.ChangeType(Convert.ChangeType(obj, numericType), typeof(long)); + numericValue = numericType == typeof(long) + ? (long)Convert.ChangeType(obj, numericType) + : (long)Convert.ChangeType(Convert.ChangeType(obj, numericType), typeof(long)); number = numericValue; foreach (object v in Enum.GetValues(type)) allValues |= (long)Convert.ChangeType(Convert.ChangeType(v, numericType), typeof(long)); err = (numericValue & ~allValues) != 0; @@ -174,14 +174,12 @@ bool Finalize() NullabilityInfoContext nullabilityContext = new();// Context for nullable validation NullabilityInfo nullabilityInfo;// Nullability info NoValidationAttribute? noValidationAttr;// No validation attribute - foreach (PropertyInfo pi in from pi in type.GetPropertiesCached() - // Included - where (members?.Contains(pi.Name) ?? true) && - // Not excluded - !pi.GetCustomAttributesCached().Any(a => a is NoValidationAttribute) && - !pi.GetCustomAttributesCached().Any(a => a.GetType().FullName == VALIDATENEVER_ATTRIBUTE_TYPE) - orderby pi.Name - select pi) + foreach (PropertyInfo pi in members is null + ? type.GetPropertiesCached() + : from pi in type.GetPropertiesCached() + where members.Contains(pi.Name) + select pi + ) { // Break the loop, if requested if (loopCancelled) @@ -267,20 +265,19 @@ orderby pi.Name res = false; validationResults.Add(new( $"Property {pi.Name} value is NULL, but the property type {pi.PropertyType} isn't nullable (or a non-NULL value is required)", - new string[] { pi.Name } + [pi.Name] )); (_, res, _) = RaiseEvent(OnObjectPropertyValidationFailed, info.Seen, obj, validationResults, allResults, member, throwOnError, members, res); } continue; } - //if (isObjectValidatable) continue;// WTH? // Deep object validation valueType = value.GetType(); noValidationAttr = (NoValidationAttribute?)valueType.GetCustomAttributesCached().FirstOrDefault(a => a is NoValidationAttribute); onlyItemNullValueChecks = noValidationAttr is not null && !noValidationAttr.SkipNullValueCheck; if ( !(noValidationAttr?.SkipNullValueCheck ?? false) && - valueType.GetCustomAttributesCached().Any(a => a.GetType().FullName == VALIDATENEVER_ATTRIBUTE_TYPE) + valueType.GetCustomAttributesCached().Any(a => a.GetType().FullName == ReflectionHelper.VALIDATENEVER_ATTRIBUTE_TYPE) ) { #if DEBUG @@ -372,12 +369,12 @@ orderby pi.Name { if (throwOnError) throw; res = false; - validationResults.Add(new($"{VALIDATION_EXCEPTION_PREFIX}{pi.Name}: {ex}", new string[] { pi.Name })); + validationResults.Add(new($"{VALIDATION_EXCEPTION_PREFIX}{pi.Name}: {ex}", [pi.Name])); } catch (Exception ex) { res = false; - validationResults.Add(new($"{VALIDATION_EXCEPTION_PREFIX}{pi.Name}: {ex}", new string[] { pi.Name })); + validationResults.Add(new($"{VALIDATION_EXCEPTION_PREFIX}{pi.Name}: {ex}", [pi.Name])); } finally { @@ -397,7 +394,7 @@ orderby pi.Name finally { info.CurrentDepth--; - if (seenIndex > 0) info.Seen.RemoveAt(seenIndex); + if (seenIndex > 0) info.Seen.Remove(obj); if (results is not null) foreach (ValidationResult result in results) ObjectValidation.ValidateObject.Logger( @@ -422,7 +419,7 @@ internal static bool AddResults(List? results, List? results, ListIs nullable? internal static bool IsNullable(ICustomAttributeProvider obj, NullabilityInfo? ni = null, bool isItem = false, int arrayLevel = 0) { - Attribute[] attributes = obj.GetCustomAttributesCached(); + FrozenSet attributes = obj.GetCustomAttributesCached(); if (!isItem) { if (attributes.Any(a => a is DisallowNullAttribute)) return false; diff --git a/src/ObjectValidation/ValidationExtensions.Items.cs b/src/ObjectValidation/ValidationExtensions.Items.cs index 2c9e443..da62eae 100644 --- a/src/ObjectValidation/ValidationExtensions.Items.cs +++ b/src/ObjectValidation/ValidationExtensions.Items.cs @@ -89,7 +89,7 @@ internal static bool ValidateDictionary( res = false; validationResults.Add(new( $"Property {GetMemberName(info, pi, count, member, isDict: true)} value is NULL, but the value type {itemType} isn't nullable (or a non-NULL value is required)", - new string[] { GetMemberName(info, pi, count, member, isDict: true) } + [GetMemberName(info, pi, count, member, isDict: true)] )); } else if (valueValidations.Length != 0) @@ -183,7 +183,7 @@ internal static bool ValidateList( res = false; validationResults.Add(new( $"Property {GetMemberName(info, pi, count, member)} value is NULL, but the item type {itemType} isn't nullable (or a non-NULL value is required)", - new string[] { GetMemberName(info, pi, count, member) } + [GetMemberName(info, pi, count, member)] )); } else if (itemValidations.Length != 0) @@ -262,15 +262,11 @@ bool throwOnError Type? keyType,// Dictionary key type itemType;// Item type #pragma warning restore IDE0018 // Declare inline - int seenIndex;// Seen index - if (AsDictionary(value, out keyType, out itemType) is IDictionary dict) - { - seenIndex = info.Seen.Count; - info.Seen.Add(dict); - try + if (!info.Seen.Add(value)) + if (AsDictionary(value, out keyType, out itemType) is IDictionary dict) { nestedInfo.CurrentDepth++; - res &= ValidateDictionary( + return res && ValidateDictionary( nestedInfo, pi, dict, @@ -285,19 +281,10 @@ bool throwOnError member ); } - finally - { - info.Seen.RemoveAt(seenIndex); - } - } - else if (value is not string && value is Array arr && valueType.IsArray && valueType.GetElementType() is not null) - { - seenIndex = info.Seen.Count; - info.Seen.Add(arr); - try + else if (value is not string && value is Array arr && valueType.IsArray && valueType.GetElementType() is not null) { nestedInfo.CurrentDepth++; - res &= ValidateList( + return res && ValidateList( nestedInfo, pi, arr, @@ -311,47 +298,44 @@ bool throwOnError member ); } - finally - { - info.Seen.RemoveAt(seenIndex); - } - } - else if (AsList(value, out itemType) is IList list) - { - seenIndex = info.Seen.Count; - info.Seen.Add(list); - try + else if (AsList(value, out itemType) is IList list) { nestedInfo.CurrentDepth++; - res &= ValidateList(nestedInfo, pi, list, valueType, itemType, nullabilityInfo: null, validationResults, serviceProvider, onlyNullCheck: false, throwOnError, member); + return res && ValidateList( + nestedInfo, + pi, + list, + valueType, + itemType, + nullabilityInfo: null, + validationResults, + serviceProvider, + onlyNullCheck: false, + throwOnError, + member + ); } - finally - { - info.Seen.RemoveAt(seenIndex); - } - } - else if (valueValidatable && value is ICollection col) - { - seenIndex = info.Seen.Count; - info.Seen.Add(col); - try + else if (valueValidatable && value is ICollection col) { nestedInfo.CurrentDepth++; - res &= ValidateList(nestedInfo, pi, col, valueType, itemType, nullabilityInfo: null, validationResults, serviceProvider, onlyNullCheck: false, throwOnError, member); - } - finally - { - info.Seen.RemoveAt(seenIndex); + return res && ValidateList( + nestedInfo, + pi, + col, + valueType, + itemType, + nullabilityInfo: null, + validationResults, + serviceProvider, + onlyNullCheck: false, + throwOnError, + member + ); } - } - else if (valueValidatable && value is IEnumerable enumerable) - { - seenIndex = info.Seen.Count; - info.Seen.Add(enumerable); - try + else if (valueValidatable && value is IEnumerable enumerable) { nestedInfo.CurrentDepth++; - res &= ValidateList( + return res && ValidateList( nestedInfo, pi, enumerable, @@ -365,18 +349,10 @@ bool throwOnError member ); } - finally - { - info.Seen.RemoveAt(seenIndex); - } - } #if DEBUG - else - { - ObjectValidation.ValidateObject.Logger( - $"Can't validate item {member} type {valueType} of property {pi.DeclaringType}.{pi.Name} value (not validatable {pi.PropertyType} value)" - ); - } + ObjectValidation.ValidateObject.Logger( + $"Can't validate item {member} type {valueType} of property {pi.DeclaringType}.{pi.Name} value (not validatable {pi.PropertyType} value)" + ); #endif } return res; @@ -430,11 +406,11 @@ internal static string GetMemberName(ValidationInfo info, PropertyInfo pi, long => info.ArrayLevel switch { 0 => isDict - ? $"{pi.Name}[{(target == ItemValidationTargets.Item ? "value" : "key")}#{item}]" - : $"{pi.Name}[{item}]", + ? $"{pi.Name}[{(target == ItemValidationTargets.Item ? "value" : "key")}#{item}]" + : $"{pi.Name}[{item}]", _ => isDict - ? $"{member ?? throw new ArgumentNullException(nameof(member))}[{(target == ItemValidationTargets.Item ? "value" : "key")}#{item}]" - : $"{member ?? throw new ArgumentNullException(nameof(member))}[{item}]" + ? $"{member ?? throw new ArgumentNullException(nameof(member))}[{(target == ItemValidationTargets.Item ? "value" : "key")}#{item}]" + : $"{member ?? throw new ArgumentNullException(nameof(member))}[{item}]" }; } } diff --git a/src/ObjectValidation/ValidationExtensions.cs b/src/ObjectValidation/ValidationExtensions.cs index 732c07b..19f3511 100644 --- a/src/ObjectValidation/ValidationExtensions.cs +++ b/src/ObjectValidation/ValidationExtensions.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using System.Runtime.CompilerServices; namespace wan24.ObjectValidation { diff --git a/src/ObjectValidation/ValidationInfo.cs b/src/ObjectValidation/ValidationInfo.cs index e73e6ac..00491e7 100644 --- a/src/ObjectValidation/ValidationInfo.cs +++ b/src/ObjectValidation/ValidationInfo.cs @@ -3,21 +3,19 @@ /// /// Validation depth /// - internal sealed class ValidationInfo : IValidationInfo + /// + /// Constructor + /// + /// Seen objects + internal sealed class ValidationInfo(HashSet? seen = null) : IValidationInfo { /// /// Current validation depth /// private int _CurrentDepth = -1; - /// - /// Constructor - /// - /// Seen objects - internal ValidationInfo(List? seen = null) => Seen = seen ?? []; - /// - public List Seen { get; } + public HashSet Seen { get; } = seen ?? []; /// public int CurrentDepth