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