diff --git a/src/Orleans.Core.Abstractions/IDs/IdSpan.cs b/src/Orleans.Core.Abstractions/IDs/IdSpan.cs
index b919f75f8cb..466d1963d90 100644
--- a/src/Orleans.Core.Abstractions/IDs/IdSpan.cs
+++ b/src/Orleans.Core.Abstractions/IDs/IdSpan.cs
@@ -94,7 +94,20 @@ private IdSpan(SerializationInfo info, StreamingContext context)
public override bool Equals(object? obj) => obj is IdSpan kind && Equals(kind);
///
- public bool Equals(IdSpan obj) => _value == obj._value || _value.AsSpan().SequenceEqual(obj._value);
+ public bool Equals(IdSpan obj)
+ {
+ if (_value == obj._value)
+ {
+ return true;
+ }
+
+ if (_value is null || obj._value is null)
+ {
+ return false;
+ }
+
+ return _value.AsSpan().SequenceEqual(obj._value);
+ }
///
public override int GetHashCode() => _hashCode;
@@ -139,7 +152,20 @@ public void GetObjectData(SerializationInfo info, StreamingContext context)
public static byte[]? UnsafeGetArray(IdSpan id) => id._value;
///
- public int CompareTo(IdSpan other) => _value.AsSpan().SequenceCompareTo(other._value.AsSpan());
+ public int CompareTo(IdSpan other)
+ {
+ if (_value is null)
+ {
+ return other._value is null ? 0 : -1;
+ }
+
+ if (other._value is null)
+ {
+ return 1;
+ }
+
+ return _value.AsSpan().SequenceCompareTo(other._value.AsSpan());
+ }
///
/// Returns a string representation of this instance, decoding the value as UTF8.
diff --git a/test/NonSilo.Tests/IdSpanTests.cs b/test/NonSilo.Tests/IdSpanTests.cs
new file mode 100644
index 00000000000..804617ddb8c
--- /dev/null
+++ b/test/NonSilo.Tests/IdSpanTests.cs
@@ -0,0 +1,123 @@
+using Orleans.Runtime;
+using Xunit;
+
+namespace NonSilo.Tests
+{
+ ///
+ /// Tests for IdSpan, a primitive type for identities representing a sequence of bytes.
+ /// Validates equality, hash code consistency, and proper handling of null vs empty arrays.
+ ///
+ [TestCategory("BVT")]
+ public class IdSpanTests
+ {
+ ///
+ /// Tests that IdSpan.Create(string.Empty) and default(IdSpan) are NOT equal.
+ /// They should have different internal states (empty array vs null) and should not be considered equal.
+ ///
+ [Fact]
+ public void IdSpan_CreateEmptyString_NotEqualToDefault()
+ {
+ IdSpan createdFromEmptyString = IdSpan.Create(string.Empty);
+ IdSpan defaultIdSpan = default;
+
+ Assert.NotEqual(createdFromEmptyString, defaultIdSpan);
+ Assert.False(createdFromEmptyString.Equals(defaultIdSpan));
+ Assert.False(createdFromEmptyString == defaultIdSpan);
+ Assert.True(createdFromEmptyString != defaultIdSpan);
+ }
+
+ ///
+ /// Tests that hash codes are consistent with equality.
+ /// If two IdSpans are equal, they must have the same hash code.
+ ///
+ [Fact]
+ public void IdSpan_HashCode_ConsistentWithEquality()
+ {
+ IdSpan id1 = IdSpan.Create("test");
+ IdSpan id2 = IdSpan.Create("test");
+ IdSpan id3 = IdSpan.Create("different");
+
+ // Equal objects must have equal hash codes
+ Assert.Equal(id1, id2);
+ Assert.Equal(id1.GetHashCode(), id2.GetHashCode());
+
+ // Not equal objects may have different hash codes (not required but expected)
+ Assert.NotEqual(id1, id3);
+ }
+
+ ///
+ /// Tests that default IdSpan has expected properties.
+ ///
+ [Fact]
+ public void IdSpan_Default_HasExpectedProperties()
+ {
+ IdSpan defaultIdSpan = default;
+
+ Assert.True(defaultIdSpan.IsDefault);
+ Assert.Equal(0, defaultIdSpan.GetHashCode());
+ Assert.Equal("", defaultIdSpan.ToString());
+ }
+
+ ///
+ /// Tests that IdSpan created from empty string has expected properties.
+ ///
+ [Fact]
+ public void IdSpan_CreateEmptyString_HasExpectedProperties()
+ {
+ IdSpan emptyStringIdSpan = IdSpan.Create(string.Empty);
+
+ Assert.True(emptyStringIdSpan.IsDefault);
+ Assert.Equal("", emptyStringIdSpan.ToString());
+ // Hash code should be computed from empty byte array, not 0
+ Assert.NotEqual(0, emptyStringIdSpan.GetHashCode());
+ }
+
+ ///
+ /// Tests that IdSpans with same content are equal.
+ ///
+ [Fact]
+ public void IdSpan_SameContent_AreEqual()
+ {
+ IdSpan id1 = IdSpan.Create("test123");
+ IdSpan id2 = IdSpan.Create("test123");
+
+ Assert.Equal(id1, id2);
+ Assert.True(id1 == id2);
+ Assert.False(id1 != id2);
+ Assert.Equal(id1.GetHashCode(), id2.GetHashCode());
+ }
+
+ ///
+ /// Tests that IdSpans with different content are not equal.
+ ///
+ [Fact]
+ public void IdSpan_DifferentContent_AreNotEqual()
+ {
+ IdSpan id1 = IdSpan.Create("test1");
+ IdSpan id2 = IdSpan.Create("test2");
+
+ Assert.NotEqual(id1, id2);
+ Assert.False(id1 == id2);
+ Assert.True(id1 != id2);
+ }
+
+ ///
+ /// Tests CompareTo behavior with null and empty arrays.
+ ///
+ [Fact]
+ public void IdSpan_CompareTo_HandlesNullAndEmpty()
+ {
+ IdSpan defaultIdSpan = default;
+ IdSpan emptyStringIdSpan = IdSpan.Create(string.Empty);
+ IdSpan normalIdSpan = IdSpan.Create("test");
+
+ // Default (null) should compare differently than empty string
+ Assert.NotEqual(0, defaultIdSpan.CompareTo(emptyStringIdSpan));
+ Assert.NotEqual(0, emptyStringIdSpan.CompareTo(defaultIdSpan));
+
+ // Both should be less than a normal span
+ Assert.True(defaultIdSpan.CompareTo(normalIdSpan) < 0);
+ Assert.True(emptyStringIdSpan.CompareTo(normalIdSpan) < 0);
+ }
+ }
+}