diff --git a/Directory.Packages.props b/Directory.Packages.props
index 041434a47..194441b93 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -10,13 +10,19 @@
-
-
-
-
+
+
+
+
+
-
+
@@ -42,4 +48,4 @@
-
+
\ No newline at end of file
diff --git a/src/OpenFeature/Model/Value.cs b/src/OpenFeature/Model/Value.cs
index f09a24667..41b15246b 100644
--- a/src/OpenFeature/Model/Value.cs
+++ b/src/OpenFeature/Model/Value.cs
@@ -6,7 +6,7 @@ namespace OpenFeature.Model;
/// Values serve as a return type for provider objects. Providers may deal in JSON, protobuf, XML or some other data-interchange format.
/// This intermediate representation provides a good medium of exchange.
///
-public sealed class Value
+public sealed class Value : IEquatable
{
private readonly object? _innerValue;
@@ -184,4 +184,196 @@ public Value(Object value)
///
/// Value as DateTime
public DateTime? AsDateTime => this.IsDateTime ? (DateTime?)this._innerValue : null;
+
+ ///
+ /// Determines whether the specified is equal to the current .
+ ///
+ /// The to compare with the current .
+ /// true if the specified is equal to the current ; otherwise, false.
+ public bool Equals(Value? other)
+ {
+ if (other is null) return false;
+ if (ReferenceEquals(this, other)) return true;
+
+ // Both are null
+ if (this.IsNull && other.IsNull) return true;
+
+ // One is null, the other is not
+ if (this.IsNull != other.IsNull) return false;
+
+ // Different types
+ if (this.GetValueType() != other.GetValueType()) return false;
+
+ // Compare based on type
+ return this.GetValueType() switch
+ {
+ ValueType.Boolean => this.AsBoolean == other.AsBoolean,
+ ValueType.Number => this.AsDouble == other.AsDouble,
+ ValueType.String => this.AsString == other.AsString,
+ ValueType.DateTime => this.AsDateTime == other.AsDateTime,
+ ValueType.Structure => this.StructureEquals(other),
+ ValueType.List => this.ListEquals(other),
+ _ => false
+ };
+ }
+
+ ///
+ /// Determines whether the specified object is equal to the current .
+ ///
+ /// The object to compare with the current .
+ /// true if the specified object is equal to the current ; otherwise, false.
+ public override bool Equals(object? obj) => this.Equals(obj as Value);
+
+ ///
+ /// Returns the hash code for this .
+ ///
+ /// A hash code for the current .
+ public override int GetHashCode()
+ {
+ if (this.IsNull) return 0;
+
+ return this.GetValueType() switch
+ {
+ ValueType.Boolean => this.AsBoolean!.GetHashCode(),
+ ValueType.Number => this.AsDouble!.GetHashCode(),
+ ValueType.String => this.AsString!.GetHashCode(),
+ ValueType.DateTime => this.AsDateTime!.GetHashCode(),
+ ValueType.Structure => this.GetStructureHashCode(),
+ ValueType.List => this.GetListHashCode(),
+ _ => 0
+ };
+ }
+
+ ///
+ /// Determines whether two instances are equal.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ /// true if the values are equal; otherwise, false.
+ public static bool operator ==(Value? left, Value? right)
+ {
+ if (left is null && right is null) return true;
+ if (left is null || right is null) return false;
+ return left.Equals(right);
+ }
+
+ ///
+ /// Determines whether two instances are not equal.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ /// true if the values are not equal; otherwise, false.
+ public static bool operator !=(Value? left, Value? right) => !(left == right);
+
+ ///
+ /// Gets the type of the current value.
+ ///
+ /// The of the current value.
+ private ValueType GetValueType()
+ {
+ if (this.IsNull) return ValueType.Null;
+ if (this.IsBoolean) return ValueType.Boolean;
+ if (this.IsNumber) return ValueType.Number;
+ if (this.IsString) return ValueType.String;
+ if (this.IsDateTime) return ValueType.DateTime;
+ if (this.IsStructure) return ValueType.Structure;
+ if (this.IsList) return ValueType.List;
+ return ValueType.Unknown;
+ }
+
+ ///
+ /// Compares two Structure values for equality.
+ ///
+ /// The other to compare.
+ /// true if the structures are equal; otherwise, false.
+ private bool StructureEquals(Value other)
+ {
+ var thisStructure = this.AsStructure!;
+ var otherStructure = other.AsStructure!;
+
+ if (thisStructure.Count != otherStructure.Count) return false;
+
+ foreach (var kvp in thisStructure)
+ {
+ if (!otherStructure.TryGetValue(kvp.Key, out var otherValue) || !kvp.Value.Equals(otherValue))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Compares two List values for equality.
+ ///
+ /// The other to compare.
+ /// true if the lists are equal; otherwise, false.
+ private bool ListEquals(Value other)
+ {
+ var thisList = this.AsList!;
+ var otherList = other.AsList!;
+
+ if (thisList.Count != otherList.Count) return false;
+
+ for (int i = 0; i < thisList.Count; i++)
+ {
+ if (!thisList[i].Equals(otherList[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Gets the hash code for a Structure value.
+ ///
+ /// The hash code of the structure.
+ private int GetStructureHashCode()
+ {
+ var structure = this.AsStructure!;
+ var hash = new HashCode();
+
+ foreach (var kvp in structure)
+ {
+ hash.Add(kvp.Key);
+ hash.Add(kvp.Value);
+ }
+
+ return hash.ToHashCode();
+ }
+
+ ///
+ /// Gets the hash code for a List value.
+ ///
+ /// The hash code of the list.
+ private int GetListHashCode()
+ {
+ var list = this.AsList!;
+ var hash = new HashCode();
+
+ foreach (var item in list)
+ {
+ hash.Add(item);
+ }
+
+ return hash.ToHashCode();
+ }
+
+ ///
+ /// Represents the different types that a can contain.
+ ///
+ private enum ValueType
+ {
+ Null,
+ Boolean,
+ Number,
+ String,
+ DateTime,
+ Structure,
+ List,
+ Unknown
+ }
}
diff --git a/src/OpenFeature/OpenFeature.csproj b/src/OpenFeature/OpenFeature.csproj
index 3d81a99eb..732c92f3b 100644
--- a/src/OpenFeature/OpenFeature.csproj
+++ b/src/OpenFeature/OpenFeature.csproj
@@ -8,6 +8,7 @@
+
diff --git a/test/OpenFeature.Tests/ValueTests.cs b/test/OpenFeature.Tests/ValueTests.cs
index da76f29ad..9f94b5eaf 100644
--- a/test/OpenFeature.Tests/ValueTests.cs
+++ b/test/OpenFeature.Tests/ValueTests.cs
@@ -230,4 +230,561 @@ public void AsDateTime_WhenCalledWithNonDateTimeInnerValue_ReturnsNull()
// Assert
Assert.Null(actualValue);
}
+
+ #region Equality Tests
+
+ [Fact]
+ public void Equals_WithNullValue_ReturnsFalse()
+ {
+ // Arrange
+ var value = new Value("test");
+
+ // Act & Assert
+ Assert.False(value.Equals(null));
+ }
+
+ [Fact]
+ public void Equals_WithSameReference_ReturnsTrue()
+ {
+ // Arrange
+ var value = new Value("test");
+
+ // Act & Assert
+ Assert.True(value.Equals(value));
+ }
+
+ [Fact]
+ public void Equals_WithBothNull_ReturnsTrue()
+ {
+ // Arrange
+ var value1 = new Value();
+ var value2 = new Value();
+
+ // Act & Assert
+ Assert.True(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithOneNullOneNotNull_ReturnsFalse()
+ {
+ // Arrange
+ var nullValue = new Value();
+ var stringValue = new Value("test");
+
+ // Act & Assert
+ Assert.False(nullValue.Equals(stringValue));
+ Assert.False(stringValue.Equals(nullValue));
+ }
+
+ [Fact]
+ public void Equals_WithDifferentTypes_ReturnsFalse()
+ {
+ // Arrange
+ var stringValue = new Value("test");
+ var intValue = new Value(42);
+ var boolValue = new Value(true);
+
+ // Act & Assert
+ Assert.False(stringValue.Equals(intValue));
+ Assert.False(stringValue.Equals(boolValue));
+ Assert.False(intValue.Equals(boolValue));
+ }
+
+ [Fact]
+ public void Equals_WithSameStringValues_ReturnsTrue()
+ {
+ // Arrange
+ var value1 = new Value("test");
+ var value2 = new Value("test");
+
+ // Act & Assert
+ Assert.True(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithDifferentStringValues_ReturnsFalse()
+ {
+ // Arrange
+ var value1 = new Value("test1");
+ var value2 = new Value("test2");
+
+ // Act & Assert
+ Assert.False(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithSameBooleanValues_ReturnsTrue()
+ {
+ // Arrange
+ var value1 = new Value(true);
+ var value2 = new Value(true);
+ var value3 = new Value(false);
+ var value4 = new Value(false);
+
+ // Act & Assert
+ Assert.True(value1.Equals(value2));
+ Assert.True(value3.Equals(value4));
+ }
+
+ [Fact]
+ public void Equals_WithDifferentBooleanValues_ReturnsFalse()
+ {
+ // Arrange
+ var value1 = new Value(true);
+ var value2 = new Value(false);
+
+ // Act & Assert
+ Assert.False(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithSameNumberValues_ReturnsTrue()
+ {
+ // Arrange
+ var value1 = new Value(42.5);
+ var value2 = new Value(42.5);
+ var intValue1 = new Value(42);
+ var intValue2 = new Value(42);
+
+ // Act & Assert
+ Assert.True(value1.Equals(value2));
+ Assert.True(intValue1.Equals(intValue2));
+ }
+
+ [Fact]
+ public void Equals_WithDifferentNumberValues_ReturnsFalse()
+ {
+ // Arrange
+ var value1 = new Value(42.5);
+ var value2 = new Value(42.6);
+
+ // Act & Assert
+ Assert.False(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithSameDateTimeValues_ReturnsTrue()
+ {
+ // Arrange
+ var dateTime = DateTime.Now;
+ var value1 = new Value(dateTime);
+ var value2 = new Value(dateTime);
+
+ // Act & Assert
+ Assert.True(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithDifferentDateTimeValues_ReturnsFalse()
+ {
+ // Arrange
+ var value1 = new Value(DateTime.Now);
+ var value2 = new Value(DateTime.Now.AddDays(1));
+
+ // Act & Assert
+ Assert.False(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithSameStructureValues_ReturnsTrue()
+ {
+ // Arrange
+ var structure1 = Structure.Builder()
+ .Set("key1", new Value("value1"))
+ .Set("key2", new Value(42))
+ .Build();
+ var structure2 = Structure.Builder()
+ .Set("key1", new Value("value1"))
+ .Set("key2", new Value(42))
+ .Build();
+ var value1 = new Value(structure1);
+ var value2 = new Value(structure2);
+
+ // Act & Assert
+ Assert.True(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithDifferentStructureValues_ReturnsFalse()
+ {
+ // Arrange
+ var structure1 = Structure.Builder()
+ .Set("key1", new Value("value1"))
+ .Build();
+ var structure2 = Structure.Builder()
+ .Set("key1", new Value("value2"))
+ .Build();
+ var value1 = new Value(structure1);
+ var value2 = new Value(structure2);
+
+ // Act & Assert
+ Assert.False(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithStructuresDifferentKeyCounts_ReturnsFalse()
+ {
+ // Arrange
+ var structure1 = Structure.Builder()
+ .Set("key1", new Value("value1"))
+ .Build();
+ var structure2 = Structure.Builder()
+ .Set("key1", new Value("value1"))
+ .Set("key2", new Value("value2"))
+ .Build();
+ var value1 = new Value(structure1);
+ var value2 = new Value(structure2);
+
+ // Act & Assert
+ Assert.False(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithSameListValues_ReturnsTrue()
+ {
+ // Arrange
+ var list1 = new List { new("test"), new(42), new(true) };
+ var list2 = new List { new("test"), new(42), new(true) };
+ var value1 = new Value(list1);
+ var value2 = new Value(list2);
+
+ // Act & Assert
+ Assert.True(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithDifferentListValues_ReturnsFalse()
+ {
+ // Arrange
+ var list1 = new List { new("test1"), new(42) };
+ var list2 = new List { new("test2"), new(42) };
+ var value1 = new Value(list1);
+ var value2 = new Value(list2);
+
+ // Act & Assert
+ Assert.False(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithListsDifferentLengths_ReturnsFalse()
+ {
+ // Arrange
+ var list1 = new List { new("test") };
+ var list2 = new List { new("test"), new(42) };
+ var value1 = new Value(list1);
+ var value2 = new Value(list2);
+
+ // Act & Assert
+ Assert.False(value1.Equals(value2));
+ }
+
+ [Fact]
+ public void Equals_WithObject_CallsTypedEquals()
+ {
+ // Arrange
+ var value1 = new Value("test");
+ var value2 = new Value("test");
+ object obj = value2;
+
+ // Act & Assert
+ Assert.True(value1.Equals(obj));
+ }
+
+ [Fact]
+ public void Equals_WithNonValueObject_ReturnsFalse()
+ {
+ // Arrange
+ var value = new Value("test");
+ object obj = "test";
+
+ // Act & Assert
+ Assert.False(value.Equals(obj));
+ }
+
+ #endregion
+
+ #region Operator Tests
+
+ [Fact]
+ public void OperatorEquals_WithBothNull_ReturnsTrue()
+ {
+ // Arrange
+ Value? value1 = null;
+ Value? value2 = null;
+
+ // Act & Assert
+ Assert.True(value1 == value2);
+ }
+
+ [Fact]
+ public void OperatorEquals_WithOneNull_ReturnsFalse()
+ {
+ // Arrange
+ Value? value1 = null;
+ Value value2 = new Value("test");
+
+ // Act & Assert
+ Assert.False(value1 == value2);
+ Assert.False(value2 == value1);
+ }
+
+ [Fact]
+ public void OperatorEquals_WithEqualValues_ReturnsTrue()
+ {
+ // Arrange
+ var value1 = new Value("test");
+ var value2 = new Value("test");
+
+ // Act & Assert
+ Assert.True(value1 == value2);
+ }
+
+ [Fact]
+ public void OperatorEquals_WithDifferentValues_ReturnsFalse()
+ {
+ // Arrange
+ var value1 = new Value("test1");
+ var value2 = new Value("test2");
+
+ // Act & Assert
+ Assert.False(value1 == value2);
+ }
+
+ [Fact]
+ public void OperatorNotEquals_WithEqualValues_ReturnsFalse()
+ {
+ // Arrange
+ var value1 = new Value("test");
+ var value2 = new Value("test");
+
+ // Act & Assert
+ Assert.False(value1 != value2);
+ }
+
+ [Fact]
+ public void OperatorNotEquals_WithDifferentValues_ReturnsTrue()
+ {
+ // Arrange
+ var value1 = new Value("test1");
+ var value2 = new Value("test2");
+
+ // Act & Assert
+ Assert.True(value1 != value2);
+ }
+
+ #endregion
+
+ #region GetHashCode Tests
+
+ [Fact]
+ public void GetHashCode_WithNullValue_ReturnsZero()
+ {
+ // Arrange
+ var value = new Value();
+
+ // Act
+ var hashCode = value.GetHashCode();
+
+ // Assert
+ Assert.Equal(0, hashCode);
+ }
+
+ [Fact]
+ public void GetHashCode_WithEqualValues_ReturnsSameHashCode()
+ {
+ // Arrange
+ var value1 = new Value("test");
+ var value2 = new Value("test");
+
+ // Act
+ var hashCode1 = value1.GetHashCode();
+ var hashCode2 = value2.GetHashCode();
+
+ // Assert
+ Assert.Equal(hashCode1, hashCode2);
+ }
+
+ [Fact]
+ public void GetHashCode_WithBooleanValues_ReturnsConsistentHashCode()
+ {
+ // Arrange
+ var value1 = new Value(true);
+ var value2 = new Value(true);
+ var value3 = new Value(false);
+
+ // Act
+ var hashCode1 = value1.GetHashCode();
+ var hashCode2 = value2.GetHashCode();
+ var hashCode3 = value3.GetHashCode();
+
+ // Assert
+ Assert.Equal(hashCode1, hashCode2);
+ Assert.NotEqual(hashCode1, hashCode3);
+ }
+
+ [Fact]
+ public void GetHashCode_WithNumberValues_ReturnsConsistentHashCode()
+ {
+ // Arrange
+ var value1 = new Value(42.5);
+ var value2 = new Value(42.5);
+ var value3 = new Value(42.6);
+
+ // Act
+ var hashCode1 = value1.GetHashCode();
+ var hashCode2 = value2.GetHashCode();
+ var hashCode3 = value3.GetHashCode();
+
+ // Assert
+ Assert.Equal(hashCode1, hashCode2);
+ Assert.NotEqual(hashCode1, hashCode3);
+ }
+
+ [Fact]
+ public void GetHashCode_WithStringValues_ReturnsConsistentHashCode()
+ {
+ // Arrange
+ var value1 = new Value("test");
+ var value2 = new Value("test");
+ var value3 = new Value("different");
+
+ // Act
+ var hashCode1 = value1.GetHashCode();
+ var hashCode2 = value2.GetHashCode();
+ var hashCode3 = value3.GetHashCode();
+
+ // Assert
+ Assert.Equal(hashCode1, hashCode2);
+ Assert.NotEqual(hashCode1, hashCode3);
+ }
+
+ [Fact]
+ public void GetHashCode_WithDateTimeValues_ReturnsConsistentHashCode()
+ {
+ // Arrange
+ var dateTime = DateTime.Now;
+ var value1 = new Value(dateTime);
+ var value2 = new Value(dateTime);
+ var value3 = new Value(dateTime.AddDays(1));
+
+ // Act
+ var hashCode1 = value1.GetHashCode();
+ var hashCode2 = value2.GetHashCode();
+ var hashCode3 = value3.GetHashCode();
+
+ // Assert
+ Assert.Equal(hashCode1, hashCode2);
+ Assert.NotEqual(hashCode1, hashCode3);
+ }
+
+ [Fact]
+ public void GetHashCode_WithStructureValues_ReturnsConsistentHashCode()
+ {
+ // Arrange
+ var structure1 = Structure.Builder()
+ .Set("key1", new Value("value1"))
+ .Set("key2", new Value(42))
+ .Build();
+ var structure2 = Structure.Builder()
+ .Set("key1", new Value("value1"))
+ .Set("key2", new Value(42))
+ .Build();
+ var value1 = new Value(structure1);
+ var value2 = new Value(structure2);
+
+ // Act
+ var hashCode1 = value1.GetHashCode();
+ var hashCode2 = value2.GetHashCode();
+
+ // Assert
+ Assert.Equal(hashCode1, hashCode2);
+ }
+
+ [Fact]
+ public void GetHashCode_WithListValues_ReturnsConsistentHashCode()
+ {
+ // Arrange
+ var list1 = new List { new("test"), new(42) };
+ var list2 = new List { new("test"), new(42) };
+ var value1 = new Value(list1);
+ var value2 = new Value(list2);
+
+ // Act
+ var hashCode1 = value1.GetHashCode();
+ var hashCode2 = value2.GetHashCode();
+
+ // Assert
+ Assert.Equal(hashCode1, hashCode2);
+ }
+
+ #endregion
+
+ #region Complex Nested Tests
+
+ [Fact]
+ public void Equals_WithNestedStructuresAndLists_ReturnsTrue()
+ {
+ // Arrange
+ var innerList1 = new List { new("nested"), new(123) };
+ var innerList2 = new List { new("nested"), new(123) };
+
+ var innerStructure1 = Structure.Builder()
+ .Set("nested_key", new Value("nested_value"))
+ .Set("nested_list", new Value(innerList1))
+ .Build();
+ var innerStructure2 = Structure.Builder()
+ .Set("nested_key", new Value("nested_value"))
+ .Set("nested_list", new Value(innerList2))
+ .Build();
+
+ var outerStructure1 = Structure.Builder()
+ .Set("outer_key", new Value("outer_value"))
+ .Set("inner", new Value(innerStructure1))
+ .Build();
+ var outerStructure2 = Structure.Builder()
+ .Set("outer_key", new Value("outer_value"))
+ .Set("inner", new Value(innerStructure2))
+ .Build();
+
+ var value1 = new Value(outerStructure1);
+ var value2 = new Value(outerStructure2);
+
+ // Act & Assert
+ Assert.True(value1.Equals(value2));
+ Assert.Equal(value1.GetHashCode(), value2.GetHashCode());
+ }
+
+ [Fact]
+ public void Equals_WithDeeplyNestedDifferences_ReturnsFalse()
+ {
+ // Arrange
+ var innerList1 = new List { new("nested"), new(123) };
+ var innerList2 = new List { new("nested"), new(124) }; // Different value
+
+ var innerStructure1 = Structure.Builder()
+ .Set("nested_key", new Value("nested_value"))
+ .Set("nested_list", new Value(innerList1))
+ .Build();
+ var innerStructure2 = Structure.Builder()
+ .Set("nested_key", new Value("nested_value"))
+ .Set("nested_list", new Value(innerList2))
+ .Build();
+
+ var outerStructure1 = Structure.Builder()
+ .Set("outer_key", new Value("outer_value"))
+ .Set("inner", new Value(innerStructure1))
+ .Build();
+ var outerStructure2 = Structure.Builder()
+ .Set("outer_key", new Value("outer_value"))
+ .Set("inner", new Value(innerStructure2))
+ .Build();
+
+ var value1 = new Value(outerStructure1);
+ var value2 = new Value(outerStructure2);
+
+ // Act & Assert
+ Assert.False(value1.Equals(value2));
+ }
+
+ #endregion
}