diff --git a/TUnit.Assertions.Tests/Bugs/Tests5040.cs b/TUnit.Assertions.Tests/Bugs/Tests5040.cs
new file mode 100644
index 0000000000..3901b73b7a
--- /dev/null
+++ b/TUnit.Assertions.Tests/Bugs/Tests5040.cs
@@ -0,0 +1,119 @@
+namespace TUnit.Assertions.Tests.Bugs;
+
+///
+/// Tests for issue #5040: IsEquivalentTo throws NotSupportedException on IEquatable types with private state.
+/// When a type has no public members but implements IEquatable, structural comparison should fall back to Equals().
+///
+public class Tests5040
+{
+ #region Test Types
+
+ private sealed class PrivateStatePath : IEquatable
+ {
+ private readonly string _value;
+
+ public PrivateStatePath(string value)
+ {
+ _value = value;
+ }
+
+ public bool Equals(PrivateStatePath? other)
+ {
+ if (other is null)
+ {
+ return false;
+ }
+
+ return _value == other._value;
+ }
+
+ public override bool Equals(object? obj) => Equals(obj as PrivateStatePath);
+
+ public override int GetHashCode() => _value.GetHashCode();
+
+ public override string ToString() => _value;
+ }
+
+ private class PublicStateMessage
+ {
+ public string Content { get; set; } = string.Empty;
+ }
+
+ #endregion
+
+ [Test]
+ public async Task IEquatable_Class_With_No_Public_Members_Equal_Values_Passes()
+ {
+ var path1 = new PrivateStatePath("/home/user/file.txt");
+ var path2 = new PrivateStatePath("/home/user/file.txt");
+
+ await Assert.That(path1).IsEquivalentTo(path2);
+ }
+
+ [Test]
+ public async Task IEquatable_Class_With_No_Public_Members_Different_Values_Fails()
+ {
+ var path1 = new PrivateStatePath("/home/user/file1.txt");
+ var path2 = new PrivateStatePath("/home/user/file2.txt");
+
+ var exception = await Assert.ThrowsAsync(
+ async () => await Assert.That(path1).IsEquivalentTo(path2));
+
+ await Assert.That(exception).IsNotNull();
+ }
+
+ [Test]
+ public async Task IEquatable_Class_Collection_Unordered_Passes()
+ {
+ // Reproduces the exact scenario from issue #5040
+ var list = new List
+ {
+ new("/home/user/a.txt"),
+ new("/home/user/b.txt"),
+ new("/home/user/c.txt"),
+ };
+
+ var expected = new List
+ {
+ new("/home/user/c.txt"),
+ new("/home/user/a.txt"),
+ new("/home/user/b.txt"),
+ };
+
+ await Assert.That(list).IsEquivalentTo(expected);
+ }
+
+ [Test]
+ public async Task No_Public_Members_Vs_Public_Members_Fails()
+ {
+ // Expected has no public members, actual has public members — types are incompatible
+ var expected = new PrivateStatePath("/home/user/file.txt");
+ var actual = new PublicStateMessage { Content = "/home/user/file.txt" };
+
+ var exception = await Assert.ThrowsAsync(
+ async () => await Assert.That(actual).IsEquivalentTo(expected));
+
+ await Assert.That(exception).IsNotNull();
+ }
+
+ [Test]
+ public async Task Partial_Equivalency_With_No_Public_Members_Equal_Values_Passes()
+ {
+ var path1 = new PrivateStatePath("/home/user/file.txt");
+ var path2 = new PrivateStatePath("/home/user/file.txt");
+
+ await Assert.That(path1).IsEquivalentTo(path2).WithPartialEquivalency();
+ }
+
+ [Test]
+ public async Task Partial_Equivalency_With_No_Public_Members_Different_Values_Fails()
+ {
+ var path1 = new PrivateStatePath("/home/user/file1.txt");
+ var path2 = new PrivateStatePath("/home/user/file2.txt");
+
+ var exception = await Assert.ThrowsAsync(
+ async () => await Assert.That(path1).IsEquivalentTo(path2).WithPartialEquivalency());
+
+ await Assert.That(exception).IsNotNull();
+ }
+}
diff --git a/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs b/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs
index ab52490c39..5f0079393e 100644
--- a/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs
+++ b/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs
@@ -25,7 +25,7 @@ public static List GetMembersToCompare(
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in properties)
{
- if (prop.GetIndexParameters().Length == 0)
+ if (prop.GetIndexParameters().Length == 0 && prop.CanRead && prop.GetMethod?.IsPublic == true)
{
members.Add(prop);
}
diff --git a/TUnit.Assertions/Conditions/Helpers/StructuralEqualityComparer.cs b/TUnit.Assertions/Conditions/Helpers/StructuralEqualityComparer.cs
index 13273b4a45..389195e0d9 100644
--- a/TUnit.Assertions/Conditions/Helpers/StructuralEqualityComparer.cs
+++ b/TUnit.Assertions/Conditions/Helpers/StructuralEqualityComparer.cs
@@ -105,6 +105,14 @@ private bool CompareStructurally(object? x, object? y, HashSet