1414
1515#if FEATURE_BYREFLIKE
1616
17+ #nullable enable
18+
1719namespace Castle . DynamicProxy . Tests
1820{
1921 using System ;
@@ -38,6 +40,8 @@ public void Can_proxy_type(Type interfaceType)
3840 _ = generator . CreateInterfaceProxyWithoutTarget ( interfaceType ) ;
3941 }
4042
43+ #region Can methods with by-ref-like parameters be intercepted without crashing?
44+
4145 [ Test ]
4246 public void Can_invoke_method_with_by_ref_like_parameter ( )
4347 {
@@ -76,6 +80,10 @@ public void Can_invoke_method_with_by_ref_like_return_type()
7680 _ = proxy . Method ( ) ;
7781 }
7882
83+ #endregion
84+
85+ #region Can methods with by-ref-like parameters be proceeded to without crashing?
86+
7987 [ Test ]
8088 public void Can_proceed_to_target_method_with_by_ref_like_parameter ( )
8189 {
@@ -119,6 +127,8 @@ public void Can_proceed_to_target_method_with_by_ref_like_return_type()
119127 _ = proxy . Method ( ) ;
120128 }
121129
130+ #endregion
131+
122132 public ref struct ByRefLike
123133 {
124134 }
@@ -184,6 +194,227 @@ public virtual ByRefLike Method()
184194 return default ;
185195 }
186196 }
197+
198+ #region What values do interceptors see for by-ref-like arguments?
199+
200+ [ Test ]
201+ public void By_ref_like_arguments_are_replaced_with_null_in_invocation ( )
202+ {
203+ var interceptor = new ObservingInterceptor ( ) ;
204+ var proxy = generator . CreateClassProxy < HasMethodWithSpanParameter > ( interceptor ) ;
205+ var arg = "original" . AsSpan ( ) ;
206+ proxy . Method ( arg ) ;
207+ Assert . IsNull ( interceptor . ObservedArg ) ;
208+ }
209+
210+ [ Test ]
211+ public void By_ref_like_in_arguments_are_replaced_with_null_in_invocation ( )
212+ {
213+ var interceptor = new ObservingInterceptor ( ) ;
214+ var proxy = generator . CreateClassProxy < HasMethodWithSpanInParameter > ( interceptor ) ;
215+ var arg = "original" . AsSpan ( ) ;
216+ proxy . Method ( in arg ) ;
217+ Assert . IsNull ( interceptor . ObservedArg ) ;
218+ }
219+
220+ [ Test ]
221+ public void By_ref_like_ref_arguments_are_replaced_with_null_in_invocation ( )
222+ {
223+ var interceptor = new ObservingInterceptor ( ) ;
224+ var proxy = generator . CreateClassProxy < HasMethodWithSpanRefParameter > ( interceptor ) ;
225+ var arg = "original" . AsSpan ( ) ;
226+ proxy . Method ( ref arg ) ;
227+ Assert . IsNull ( interceptor . ObservedArg ) ;
228+ }
229+
230+ // Note the somewhat weird semantics of this test: DynamicProxy allows you to read the incoming values
231+ // of `out` arguments, which would be illegal in plain C# ("use of unassigned out parameter").
232+ // DynamicProxy does not distinguish between `ref` and `out` in this regard.
233+ [ Test ]
234+ public void By_ref_like_out_arguments_are_replaced_with_null_in_invocation ( )
235+ {
236+ var interceptor = new ObservingInterceptor ( ) ;
237+ var proxy = generator . CreateClassProxy < HasMethodWithSpanOutParameter > ( interceptor ) ;
238+ var arg = "original" . AsSpan ( ) ;
239+ proxy . Method ( out arg ) ;
240+ Assert . IsNull ( interceptor . ObservedArg ) ;
241+ }
242+
243+ #endregion
244+
245+ #region What values do proceeded-to targets see for by-ref-like arguments?
246+
247+ // This test merely describes the status quo, and not the behavior we'd ideally want.
248+ [ Test ]
249+ public void By_ref_like_arguments_arrive_reset_to_default_value_at_target ( )
250+ {
251+ var target = new HasMethodWithSpanParameter ( ) ;
252+ var proxy = generator . CreateClassProxyWithTarget ( target , new StandardInterceptor ( ) ) ;
253+ var arg = "original" . AsSpan ( ) ;
254+ proxy . Method ( arg ) ;
255+ Assert . AreEqual ( "" , target . RecordedArg ) ;
256+ }
257+
258+ // This test merely describes the status quo, and not the behavior we'd ideally want.
259+ [ Test ]
260+ public void By_ref_like_in_arguments_arrive_reset_to_default_value_at_target ( )
261+ {
262+ var target = new HasMethodWithSpanInParameter ( ) ;
263+ var proxy = generator . CreateClassProxyWithTarget ( target , new StandardInterceptor ( ) ) ;
264+ var arg = "original" . AsSpan ( ) ;
265+ proxy . Method ( in arg ) ;
266+ Assert . AreEqual ( "" , target . RecordedArg ) ;
267+ }
268+
269+ // This test merely describes the status quo, and not the behavior we'd ideally want.
270+ [ Test ]
271+ public void By_ref_like_ref_arguments_arrive_reset_to_default_value_at_target ( )
272+ {
273+ var target = new HasMethodWithSpanRefParameter ( ) ;
274+ var proxy = generator . CreateClassProxyWithTarget ( target , new StandardInterceptor ( ) ) ;
275+ var arg = "original" . AsSpan ( ) ;
276+ proxy . Method ( ref arg ) ;
277+ Assert . AreEqual ( "" , target . RecordedArg ) ;
278+ }
279+
280+ #endregion
281+
282+ #region How are by-ref-like by-ref arguments changed by interception?
283+
284+ [ Test ]
285+ public void By_ref_like_in_arguments_are_left_unchanged ( )
286+ {
287+ var proxy = generator . CreateClassProxy < HasMethodWithSpanInParameter > ( new DoNothingInterceptor ( ) ) ;
288+ var arg = "original" . AsSpan ( ) ;
289+ proxy . Method ( in arg ) ;
290+ Assert . AreEqual ( "original" , arg . ToString ( ) ) ;
291+ }
292+
293+ [ Test ]
294+ public void By_ref_like_in_arguments_are_left_unchanged_if_interception_includes_proceed_to_target ( )
295+ {
296+ var target = new HasMethodWithSpanInParameter ( ) ;
297+ var proxy = generator . CreateClassProxyWithTarget ( target , new StandardInterceptor ( ) ) ;
298+ var arg = "original" . AsSpan ( ) ;
299+ proxy . Method ( in arg ) ;
300+ Assert . AreEqual ( "original" , arg . ToString ( ) ) ;
301+ }
302+
303+ [ Test ]
304+ public void By_ref_like_ref_arguments_are_left_unchanged ( )
305+ {
306+ var proxy = generator . CreateClassProxy < HasMethodWithSpanRefParameter > ( new DoNothingInterceptor ( ) ) ;
307+ var arg = "original" . AsSpan ( ) ;
308+ proxy . Method ( ref arg ) ;
309+ Assert . AreEqual ( "original" , arg . ToString ( ) ) ;
310+ }
311+
312+ [ Test ]
313+ public void By_ref_like_ref_arguments_are_left_unchanged_if_interception_includes_proceed_to_target ( )
314+ {
315+ var target = new HasMethodWithSpanRefParameter ( ) ;
316+ var proxy = generator . CreateClassProxyWithTarget ( target , new DoNothingInterceptor ( ) ) ;
317+ var arg = "original" . AsSpan ( ) ;
318+ proxy . Method ( ref arg ) ;
319+ Assert . AreEqual ( "original" , arg . ToString ( ) ) ;
320+ }
321+
322+ // This test merely describes the status quo, and not the behavior we'd ideally want:
323+ // DynamicProxy records the initial values of all by-ref arguments in `IInvocation.Arguments` and,
324+ // unless changed during interception, writes out the final value for both `ref` and `out` parameters
325+ // from there... meaning all non-by-ref-like by-ref arguments are by default left unchanged.
326+ // This cannot work for by-ref-likes, since their initial value cannot be preserved in `Arguments`.
327+ // To honor the semantics of `out` parameters DynamicProxy *does* write a value (unlike with `ref`,
328+ // above, where it is free to choose not to).
329+ [ Test ]
330+ public void By_ref_like_out_arguments_are_reset_to_default_value ( )
331+ {
332+ var proxy = generator . CreateClassProxy < HasMethodWithSpanOutParameter > ( new DoNothingInterceptor ( ) ) ;
333+ var arg = "original" . AsSpan ( ) ;
334+ proxy . Method ( out arg ) ;
335+ Assert . AreEqual ( "" , arg . ToString ( ) ) ;
336+ }
337+
338+ // Once we manage to change the implementation so that `out` arguments aren't reset,
339+ // and the above test is replaced with an `are_left_unchanged` version, then we should also add
340+ // an additional `are_left_unchanged_if_interception_includes_proceed_to_target` test variant.
341+
342+ #endregion
343+
344+ #region Can interception targets set by-ref-like by-ref arguments?
345+
346+ // This test merely describes the status quo, and not the behavior we'd ideally want.
347+ [ Test ]
348+ public void By_ref_like_ref_arguments_cannot_be_set_by_target ( )
349+ {
350+ var target = new HasMethodWithSpanRefParameter ( ) ;
351+ var proxy = generator . CreateClassProxyWithTarget ( target , new StandardInterceptor ( ) ) ;
352+ var arg = "original" . AsSpan ( ) ;
353+ proxy . Method ( ref arg ) ;
354+ Assert . AreEqual ( "original" , arg . ToString ( ) ) ; // ideally, would be equal to "set"
355+ }
356+
357+ // This test merely describes the status quo, and not the behavior we'd ideally want.
358+ [ Test ]
359+ public void By_ref_like_out_arguments_cannot_be_set_by_target ( )
360+ {
361+ var target = new HasMethodWithSpanOutParameter ( ) ;
362+ var proxy = generator . CreateClassProxyWithTarget ( target , new StandardInterceptor ( ) ) ;
363+ var arg = "original" . AsSpan ( ) ;
364+ proxy . Method ( out arg ) ;
365+ Assert . AreEqual ( "" , arg . ToString ( ) ) ; // ideally, would be equal to "set"
366+ }
367+
368+ #endregion
369+
370+ public class HasMethodWithSpanParameter
371+ {
372+ public string ? RecordedArg ;
373+
374+ public virtual void Method ( ReadOnlySpan < char > arg )
375+ {
376+ RecordedArg = arg . ToString ( ) ;
377+ }
378+ }
379+
380+ public class HasMethodWithSpanInParameter
381+ {
382+ public string ? RecordedArg ;
383+
384+ public virtual void Method ( in ReadOnlySpan < char > arg )
385+ {
386+ RecordedArg = arg . ToString ( ) ;
387+ }
388+ }
389+
390+ public class HasMethodWithSpanRefParameter
391+ {
392+ public string ? RecordedArg ;
393+
394+ public virtual void Method ( ref ReadOnlySpan < char > arg )
395+ {
396+ RecordedArg = arg . ToString ( ) ;
397+ arg = "set" . AsSpan ( ) ;
398+ }
399+ }
400+
401+ public class HasMethodWithSpanOutParameter
402+ {
403+ public virtual void Method ( out ReadOnlySpan < char > arg )
404+ {
405+ arg = "set" . AsSpan ( ) ;
406+ }
407+ }
408+
409+ public class ObservingInterceptor : IInterceptor
410+ {
411+ public object ? ObservedArg ;
412+
413+ public void Intercept ( IInvocation invocation )
414+ {
415+ ObservedArg = invocation . Arguments [ 0 ] ;
416+ }
417+ }
187418 }
188419}
189420
0 commit comments