The generated interceptor copies the arguments into a tuple, and the intercepted method receives the elements of this
tuple. When a parameter is ref or out, the intercepted method writes to the element of the tuple, not to the
variable of the caller. This article explains how these values are written back to the call site.
The WeatherReport class calls a method with an out parameter, TryGetHumidity, and a method with a ref
parameter, GetAlerts, which moves a cursor after the returned alerts:
45 // A method with an out parameter.
46 public string DescribeHumidity( string city )
47 => this._client.TryGetHumidity( city, out var humidity )
48 ? $"{city}: humidity {humidity} %"
49 : $"{city}: humidity unknown";
50
51 // A method with a ref parameter: GetAlerts moves the cursor after the returned alerts.
52 public List<string> GetAllAlerts()
53 {
54 var alerts = new List<string>();
55 var cursor = 0;
56 string[] page;
57
58 while ( (page = this._client.GetAlerts( ref cursor, 2 )).Length > 0 )
59 {
60 alerts.AddRange( page );
61 }
62
63 return alerts;
64 }
65When the program calls these methods, CallLog writes the arguments before and after each call:
[call] WeatherClient.TryGetHumidity(Prague, 0) returned True, and changed the arguments to (Prague, 71).
Prague: humidity 71 %
[call] WeatherClient.TryGetHumidity(Lisbon, 0) returned False.
Lisbon: humidity unknown
[call] WeatherClient.GetAlerts(0, 2) returned [Lisbon: strong wind on Monday., Prague: frost on Tuesday.], and changed the arguments to (2, 2).
[call] WeatherClient.GetAlerts(2, 2) returned [Prague: heavy rain on Thursday.], and changed the arguments to (3, 2).
[call] WeatherClient.GetAlerts(3, 2) returned [].
Alerts: Lisbon: strong wind on Monday. Prague: frost on Tuesday. Prague: heavy rain on Thursday.
Passing the tuple by reference
The Invoke method of IUniversalInterceptor receives the tuple by reference, and passes it by reference to the
delegate:
10 // Intercepts a call to a synchronous method. The ref and out arguments are written back to the call site from args
11 // after the call. For a void method, TResult is ValueTuple.
12 TResult Invoke<TArgs, TResult>(
13 ref TArgs args,
14 InterceptionMetadata metadata,
15 InterceptorDelegate<TArgs, TResult> proceed )
16 where TArgs : struct, ITuple;
171using System.Runtime.CompilerServices;
2
3namespace Metalama.Samples.UniversalInterceptor;
4
5// Calls the intercepted method with the receiver and the arguments read from args. The method writes its ref and out
6// arguments to args.
7public delegate TResult InterceptorDelegate<TArgs, out TResult>( ref TArgs args )
8 where TArgs : struct, ITuple;
9
Thus, the intercepted method writes its ref and out arguments to the tuple of the generated interceptor, and the
interceptor can read the new values after proceed returns. CallLog.Invoke uses this to compare the arguments
before and after the call:
13 public TResult Invoke<TArgs, TResult>(
14 ref TArgs args,
15 InterceptionMetadata metadata,
16 InterceptorDelegate<TArgs, TResult> proceed )
17 where TArgs : struct, ITuple
18 {
19 var arguments = FormatArguments( args, metadata );
20 var call = $"{metadata.DeclaringType.Name}.{metadata.MethodName}({arguments})";
21
22 try
23 {
24 // The intercepted method writes its ref and out arguments to args.
25 var result = proceed( ref args );
26
27 var argumentsAfterCall = FormatArguments( args, metadata );
28
29 Console.WriteLine(
30 argumentsAfterCall == arguments
31 ? $"[call] {call} {FormatResult( result )}."
32 : $"[call] {call} {FormatResult( result )}, and changed the arguments to ({argumentsAfterCall})." );
33
34 return result;
35 }
36 catch ( Exception e )
37 {
38 Console.WriteLine( $"[call] {call} threw {e.GetType().Name}: {e.Message}" );
39
40 throw;
41 }
42 }
43An interceptor that does not call proceed, such as a fake that returns a canned result, can also assign the elements
of the tuple, and these values are written back to the call site.
The templates
The templates prepare the tuple in three places.
First, CreateArgs creates the tuple from the parameters of the generated method. An out parameter has no value
before the call, so its element is the default value of its type:
183// Creates the expression of the argument tuple from the parameters of the generated interceptor. An out parameter
184// has no value before the call, so its element is the default value.
185[CompileTime]
186private static IExpression CreateArgs( ITupleType argsType )
187 => argsType.CreateCreateInstanceExpression(
188 meta.Target.Method.Parameters.Select(
189 p => p.RefKind == RefKind.Out ? ExpressionFactory.Default( p.Type ) : (IExpression) p ) );
190Second, GetInvoker passes the elements of the tuple to the intercepted method. The local function receives the
tuple as a ref parameter. ExpressionFactory.Parse creates the expression of this parameter with the tuple type and
with isReferenceable: true, so that its elements can be passed as ref and out arguments:
246// Returns the invoker of the intercepted method, and the arguments read from the tuple, which is the parameter of
247// the local function named tupleParameterName. For an instance call, the invoker calls the method on the receiver
248// read from the tuple.
249[CompileTime]
250private static IMethodInvoker GetInvoker( string tupleParameterName, out IEnumerable<IExpression> arguments )
251{
252 var interception = meta.Interception.Method;
253 var argsType = TypeFactory.CreateTupleType( meta.Target.Method.Parameters );
254
255 // The expression must have the tuple type and be referenceable, so that its elements can be passed by ref or out.
256 var tuple = ExpressionFactory.Parse( tupleParameterName, argsType, isReferenceable: true )!;
257
258 var receiver = interception.ReceiverParameter == null
259 ? null
260 : argsType.TupleElements[interception.ReceiverParameter.Index].WithObject( tuple );
261
262 var argumentList = interception.ArgumentParameters
263 .Select( p => argsType.TupleElements[p.Index].WithObject( tuple ) )
264 .ToList();
265
266 // The intercepted method of a call to an extension method in reduced form is the static method. The receiver
267 // is its first argument.
268 if ( receiver != null && interception.InterceptedMethod.Parameters is [{ IsThis: true }, ..] )
269 {
270 argumentList.Insert( 0, receiver );
271 arguments = argumentList;
272
273 return interception.InterceptedMethod;
274 }
275
276 arguments = argumentList;
277
278 return receiver == null ? interception.InterceptedMethod : interception.InterceptedMethod.WithObject( receiver );
279}Third, after the call, the Invoke and InvokeVoid templates assign the ref and out parameters of the generated
method from the tuple. The assignment is in a finally block, so it also happens when the intercepted method throws an
exception after it has assigned an argument. The templates add this block only when the method has a ref or out
parameter:
16// For a synchronous method that returns a value.
17[Template]
18public static TResult Invoke<[CompileTime] TArgs, [CompileTime] TResult>(
19 [CompileTime] Type interceptorType,
20 [CompileTime] string memberName,
21 [CompileTime] bool isAsyncLocal )
22 where TArgs : struct, ITuple
23{
24 var argsType = TypeFactory.CreateTupleType( meta.Target.Method.Parameters );
25 var args = (TArgs) CreateArgs( argsType ).Value!;
26 var metadata = (InterceptionMetadata) CreateMetadata().Value!;
27 IUniversalInterceptor interceptor = GetInterceptor( interceptorType, memberName, isAsyncLocal ).Value!;
28
29 if ( !HasWritableParameters() )
30 {
31 return interceptor.Invoke( ref args, metadata, Proceed );
32 }
33
34 var argsExpression = ExpressionFactory.Capture( args );
35
36 try
37 {
38 return interceptor.Invoke( ref args, metadata, Proceed );
39 }
40 finally
41 {
42 WriteBackArguments( argsType, argsExpression );
43 }
44
45 static TResult Proceed( ref TArgs receivedArgs )
46 {
47 return GetInvoker( "receivedArgs", out var arguments )
48 .Invoke( arguments )!;
49 }
50}
51236// Assigns the ref and out parameters of the generated interceptor from the tuple, after the call.
237[Template]
238private static void WriteBackArguments( [CompileTime] ITupleType argsType, [CompileTime] IExpression args )
239{
240 foreach ( var parameter in meta.Target.Method.Parameters.Where( p => p.RefKind is RefKind.Ref or RefKind.Out ) )
241 {
242 parameter.Value = argsType.TupleElements[parameter.Index].WithObject( args ).Value;
243 }
244}
245The templates generate the following interceptors:
internal static string[] WeatherClient_GetAlerts_Interceptor(WeatherClient receiver, ref int cursor, int pageSize)
{
var args = (receiver, cursor, pageSize);
var metadata = new InterceptionMetadata(typeof(WeatherClient), "GetAlerts", "WeatherClient.GetAlerts(ref int, int)", true);
IUniversalInterceptor interceptor = CallLog.Current.Value;
try
{
return interceptor.Invoke(ref args, metadata, Proceed);
}
finally
{
cursor = args.cursor;
}
static string[] Proceed(ref (WeatherClient receiver, int cursor, int pageSize) receivedArgs)
{
return receivedArgs.receiver.GetAlerts(ref receivedArgs.cursor, receivedArgs.pageSize);
}
}
internal static bool WeatherClient_TryGetHumidity_Interceptor(WeatherClient receiver, string city, out int humidity)
{
var args = (receiver, city, humidity: default(int));
var metadata = new InterceptionMetadata(typeof(WeatherClient), "TryGetHumidity", "WeatherClient.TryGetHumidity(string, out int)", true);
IUniversalInterceptor interceptor = CallLog.Current.Value;
try
{
return interceptor.Invoke(ref args, metadata, Proceed);
}
finally
{
humidity = args.humidity;
}
static bool Proceed(ref (WeatherClient receiver, string city, int humidity) receivedArgs)
{
return receivedArgs.receiver.TryGetHumidity(receivedArgs.city, out receivedArgs.humidity);
}
}
Async methods with ref or out parameters
The delegate of an async method receives the tuple by value, because an async method cannot have ref parameters. So,
the arguments could not be written back. TryChooseAsyncTemplate rejects such methods, and
RedirectToUniversalInterceptor reports the warning UNIV05 and leaves the call unchanged. See
Universal interceptor example, step 2: Handling async methods. C# does not allow ref and out parameters in an async method, so such
methods are rare: they return a task without being async themselves.