From 4f62bcd445accb2acf64ac58a9ec619284e8edf2 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Wed, 25 Feb 2026 17:41:22 +0000
Subject: [PATCH 1/7] feat(mocks): add non-generic AnyMatcher for ref struct
arg positions
---
TUnit.Mocks/Matchers/AnyMatcher.cs | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/TUnit.Mocks/Matchers/AnyMatcher.cs b/TUnit.Mocks/Matchers/AnyMatcher.cs
index 5ea02ba145..078256388c 100644
--- a/TUnit.Mocks/Matchers/AnyMatcher.cs
+++ b/TUnit.Mocks/Matchers/AnyMatcher.cs
@@ -2,6 +2,19 @@
namespace TUnit.Mocks.Matchers;
+///
+/// A non-generic argument matcher that matches any value including null.
+/// Used for ref struct parameter positions where the generic AnyMatcher<T> cannot be used.
+///
+internal sealed class AnyMatcher : IArgumentMatcher
+{
+ public static AnyMatcher Instance { get; } = new();
+
+ public bool Matches(object? value) => true;
+
+ public string Describe() => "Any";
+}
+
///
/// An argument matcher that matches any value of the specified type, including null.
///
From 23bd5ea497523ac06548215e83f617ec3a98137a Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Wed, 25 Feb 2026 17:42:29 +0000
Subject: [PATCH 2/7] feat(mocks): add RefStructArg with allows ref struct
for net9.0+
---
TUnit.Mocks/Arguments/RefStructArg.cs | 28 +++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
create mode 100644 TUnit.Mocks/Arguments/RefStructArg.cs
diff --git a/TUnit.Mocks/Arguments/RefStructArg.cs b/TUnit.Mocks/Arguments/RefStructArg.cs
new file mode 100644
index 0000000000..6e05229067
--- /dev/null
+++ b/TUnit.Mocks/Arguments/RefStructArg.cs
@@ -0,0 +1,28 @@
+#if NET9_0_OR_GREATER
+
+namespace TUnit.Mocks.Arguments;
+
+///
+/// Represents an argument matcher for a ref struct parameter in mock setup and verification expressions.
+/// Since ref struct types cannot be used as generic type arguments for ,
+/// this type uses the allows ref struct anti-constraint (C# 13+) to accept them.
+///
+/// The ref struct parameter type.
+public readonly struct RefStructArg where T : allows ref struct
+{
+ /// Gets the argument matcher. Public for generated code access. Not intended for direct use.
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
+ public IArgumentMatcher Matcher { get; }
+
+ /// Creates a RefStructArg with a matcher. Public for generated code access. Not intended for direct use.
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
+ public RefStructArg(IArgumentMatcher matcher)
+ {
+ Matcher = matcher;
+ }
+
+ /// Matches any value of the ref struct type. This is currently the only supported matcher for ref struct parameters.
+ public static RefStructArg Any => new(Mocks.Matchers.AnyMatcher.Instance);
+}
+
+#endif
From 83f9745da6d9523b222dfbfd767bdc82be6f2a02 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Wed, 25 Feb 2026 17:46:55 +0000
Subject: [PATCH 3/7] feat(mocks): source gen emits RefStructArg with #if
NET9_0_OR_GREATER blocks
---
.../Builders/MockImplBuilder.cs | 66 ++++++-
.../Builders/MockMembersBuilder.cs | 164 +++++++++++++++---
2 files changed, 199 insertions(+), 31 deletions(-)
diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
index e35f5c568b..74fd068fb5 100644
--- a/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
+++ b/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
@@ -188,7 +188,21 @@ private static void GenerateWrapMethodBody(CodeWriter writer, MockMemberModel me
}
}
- var argsArray = GetArgsArrayExpression(method);
+ string argsArray;
+ if (HasRefStructParams(method))
+ {
+ // Emit #if block so the variable is defined under both branches
+ writer.AppendLine("#if NET9_0_OR_GREATER");
+ writer.AppendLine($"var __args = {GetArgsArrayExpression(method, true)};");
+ writer.AppendLine("#else");
+ writer.AppendLine($"var __args = {GetArgsArrayExpression(method, false)};");
+ writer.AppendLine("#endif");
+ argsArray = "__args";
+ }
+ else
+ {
+ argsArray = GetArgsArrayExpression(method, false);
+ }
var argPassList = GetArgPassList(method);
if (method.IsVoid && !method.IsAsync)
@@ -461,7 +475,20 @@ private static void GeneratePartialMethodBody(CodeWriter writer, MockMemberModel
}
}
- var argsArray = GetArgsArrayExpression(method);
+ string argsArray;
+ if (HasRefStructParams(method))
+ {
+ writer.AppendLine("#if NET9_0_OR_GREATER");
+ writer.AppendLine($"var __args = {GetArgsArrayExpression(method, true)};");
+ writer.AppendLine("#else");
+ writer.AppendLine($"var __args = {GetArgsArrayExpression(method, false)};");
+ writer.AppendLine("#endif");
+ argsArray = "__args";
+ }
+ else
+ {
+ argsArray = GetArgsArrayExpression(method, false);
+ }
var argPassList = GetArgPassList(method);
if (method.IsVoid && !method.IsAsync)
@@ -551,7 +578,20 @@ private static void GenerateEngineDispatchBody(CodeWriter writer, MockMemberMode
}
}
- var argsArray = GetArgsArrayExpression(method);
+ string argsArray;
+ if (HasRefStructParams(method))
+ {
+ writer.AppendLine("#if NET9_0_OR_GREATER");
+ writer.AppendLine($"var __args = {GetArgsArrayExpression(method, true)};");
+ writer.AppendLine("#else");
+ writer.AppendLine($"var __args = {GetArgsArrayExpression(method, false)};");
+ writer.AppendLine("#endif");
+ argsArray = "__args";
+ }
+ else
+ {
+ argsArray = GetArgsArrayExpression(method, false);
+ }
var hasOutRef = HasOutRefParams(method);
@@ -955,14 +995,22 @@ private static void EmitOutRefReadback(CodeWriter writer, MockMemberModel method
}
}
- private static string GetArgsArrayExpression(MockMemberModel method)
+ private static bool HasRefStructParams(MockMemberModel method)
+ => method.Parameters.Any(p => p.IsRefStruct && p.Direction != ParameterDirection.Out);
+
+ private static string GetArgsArrayExpression(MockMemberModel method, bool includeRefStructSentinels)
{
- // Only include non-out, non-ref-struct parameters in args array
- // (ref structs cannot be boxed into object?[])
- var matchableParams = method.Parameters.Where(p => p.Direction != ParameterDirection.Out && !p.IsRefStruct).ToList();
+ var nonOutParams = method.Parameters.Where(p => p.Direction != ParameterDirection.Out).ToList();
+ if (includeRefStructSentinels)
+ {
+ if (nonOutParams.Count == 0) return "global::System.Array.Empty