diff --git a/TUnit.Assertions.Tests/Bugs/Tests5841.cs b/TUnit.Assertions.Tests/Bugs/Tests5841.cs
new file mode 100644
index 0000000000..ca0549b6df
--- /dev/null
+++ b/TUnit.Assertions.Tests/Bugs/Tests5841.cs
@@ -0,0 +1,39 @@
+namespace TUnit.Assertions.Tests.Bugs;
+
+///
+/// Regression tests for issue #5841.
+/// IsEquivalentTo must not invoke property/field getters whose return type is a ref struct
+/// (e.g. ReadOnlySpan<T>, reachable via ReadOnlyMemory<T>.Span). Ref structs cannot
+/// be boxed, so RuntimeMethodInfo.Invoke throws NotSupportedException.
+///
+public class Tests5841
+{
+ public class HasMemoryProperty
+ {
+ public ReadOnlyMemory Memory { get; init; }
+ }
+
+ public class HasSpanProperty
+ {
+ public ReadOnlySpan Span => default;
+ public int Value { get; init; }
+ }
+
+ [Test]
+ public async Task IsEquivalentTo_with_ReadOnlyMemory_property_does_not_invoke_Span_getter()
+ {
+ var a = new HasMemoryProperty { Memory = new byte[] { 1, 2, 3 } };
+ var b = new HasMemoryProperty { Memory = new byte[] { 1, 2, 3 } };
+
+ await Assert.That(a).IsEquivalentTo(b);
+ }
+
+ [Test]
+ public async Task IsEquivalentTo_skips_ref_struct_returning_property()
+ {
+ var a = new HasSpanProperty { Value = 7 };
+ var b = new HasSpanProperty { Value = 7 };
+
+ await Assert.That(a).IsEquivalentTo(b);
+ }
+}
diff --git a/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs b/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs
index 501523b5be..c9e932848d 100644
--- a/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs
+++ b/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs
@@ -35,19 +35,54 @@ private static MemberInfo[] BuildMembersToCompare(Type type)
var members = new List();
// Filter out indexed properties (properties with parameters like this[int index])
+ // and members whose type is a ref struct (IsByRefLike). Ref structs cannot be boxed,
+ // so PropertyInfo.GetValue / FieldInfo.GetValue throws NotSupportedException on them.
+ // Common offender: ReadOnlyMemory.Span returns ReadOnlySpan.
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in properties)
{
- if (prop.GetIndexParameters().Length == 0 && prop.CanRead && prop.GetMethod?.IsPublic == true)
+ if (prop.GetIndexParameters().Length == 0
+ && prop.CanRead
+ && prop.GetMethod?.IsPublic == true
+ && !IsByRefLike(prop.PropertyType))
{
members.Add(prop);
}
}
- members.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.Instance));
+ foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
+ {
+ if (!IsByRefLike(field.FieldType))
+ {
+ members.Add(field);
+ }
+ }
+
return members.ToArray();
}
+ private static bool IsByRefLike(Type type)
+ {
+#if NET5_0_OR_GREATER
+ return type.IsByRefLike;
+#else
+ if (!type.IsValueType)
+ {
+ return false;
+ }
+
+ foreach (var attr in type.GetCustomAttributesData())
+ {
+ if (attr.AttributeType.FullName == "System.Runtime.CompilerServices.IsByRefLikeAttribute")
+ {
+ return true;
+ }
+ }
+
+ return false;
+#endif
+ }
+
///
/// Gets the value of a member (property or field) from an object.
///