The Invoke method of Universal interceptor example, step 1: Routing synchronous calls to one object returns the result of the call. For an async method, this
result is a task, so Invoke would return before the operation completes, and it could not observe the result or the
exception of the operation. This article explains how the async methods of WeatherClient reach the interceptor.
The DescribeForecastAsync method of WeatherReport calls methods that return a Task, a ValueTask, a Task<T> and
a ValueTask<T>:
31 // Methods that return a Task, a ValueTask, a Task<T> and a ValueTask<T>.
32 public async Task<string> DescribeForecastAsync( string city, int days )
33 {
34 await this._client.RefreshAsync();
35 await this._client.ClearCacheAsync();
36 var forecast = await this._client.GetForecastAsync( city, days );
37 var current = await this._client.GetTemperatureAsync( city );
38
39 return $"{city}: {current} now, {forecast.AverageHigh} on average in the next {forecast.Days} days";
40 }
41When the program calls DescribeForecastAsync, CallLog writes the following lines:
[call] WeatherClient.RefreshAsync(CancellationToken) succeeded asynchronously.
[call] WeatherClient.ClearCacheAsync(CancellationToken) succeeded asynchronously.
[call] WeatherClient.GetForecastAsync(Prague, 3, CancellationToken) returned Forecast { City = Prague, Days = 3, AverageHigh = 13.5 } asynchronously.
[call] WeatherClient.GetTemperatureAsync(Prague, CancellationToken) returned 12.5 asynchronously.
Prague: 12.5 now, 13.5 on average in the next 3 days
The cancellationToken parameters are optional, and the call sites do not pass them. The generated interceptor has
the same parameters as the intercepted method, so the call site passes default(CancellationToken), and the tuple
contains it.
The run-time interface
IUniversalInterceptor has two InvokeAsync overloads, one for Task<TResult> and one for ValueTask<TResult>:
21 // Intercepts a call to a method that returns a Task or a Task<T>. For a Task, TResult is ValueTuple.
22 Task<TResult> InvokeAsync<TArgs, TResult>(
23 TArgs args,
24 InterceptionMetadata metadata,
25 Func<TArgs, Task<TResult>> proceed )
26 where TArgs : struct, ITuple;
27
28 // Intercepts a call to a method that returns a ValueTask or a ValueTask<T>. For a ValueTask, TResult is ValueTuple.
29 ValueTask<TResult> InvokeAsync<TArgs, TResult>(
30 TArgs args,
31 InterceptionMetadata metadata,
32 Func<TArgs, ValueTask<TResult>> proceed )
33 where TArgs : struct, ITuple;
34A method that returns a Task or a ValueTask has no result, so the generated code uses ValueTuple as TResult, as
for a void method. Two overloads thus handle the four return types.
The proceed delegate is a Func<TArgs, Task<TResult>> or a Func<TArgs, ValueTask<TResult>>, and it receives the tuple
by value. An async method cannot have ref parameters, so the delegate cannot write the arguments back to the tuple.
For this reason, the methods that return an awaitable type and have ref or out parameters are not intercepted. See
Universal interceptor example, step 3: Handling ref and out parameters.
The logger
CallLog implements InvokeAsync with async methods. It awaits proceed, so it writes the result or the exception
when the operation completes:
44 public async Task<TResult> InvokeAsync<TArgs, TResult>(
45 TArgs args,
46 InterceptionMetadata metadata,
47 Func<TArgs, Task<TResult>> proceed )
48 where TArgs : struct, ITuple
49 {
50 var call = FormatCall( args, metadata );
51
52 try
53 {
54 var result = await proceed( args );
55
56 Console.WriteLine( $"[call] {call} {FormatResult( result )} asynchronously." );
57
58 return result;
59 }
60 catch ( Exception e )
61 {
62 Console.WriteLine( $"[call] {call} threw {e.GetType().Name} asynchronously: {e.Message}" );
63
64 throw;
65 }
66 }
67
68 public async ValueTask<TResult> InvokeAsync<TArgs, TResult>(
69 TArgs args,
70 InterceptionMetadata metadata,
71 Func<TArgs, ValueTask<TResult>> proceed )
72 where TArgs : struct, ITuple
73 {
74 var call = FormatCall( args, metadata );
75
76 try
77 {
78 var result = await proceed( args );
79
80 Console.WriteLine( $"[call] {call} {FormatResult( result )} asynchronously." );
81
82 return result;
83 }
84 catch ( Exception e )
85 {
86 Console.WriteLine( $"[call] {call} threw {e.GetType().Name} asynchronously: {e.Message}" );
87
88 throw;
89 }
90 }
91Choosing the template
For a method that returns an awaitable type, RedirectToUniversalInterceptor chooses one of four templates from the
return type. The result type is the type of the awaited value, or null for a Task or a ValueTask. Other awaitable
types are not supported, and the call is not intercepted:
89// Chooses the template of a method that returns an awaitable type. The result type is the type of the awaited
90// value, or null for a Task or a ValueTask. Returns false, with the reason, when the method is not supported.
91private static bool TryChooseAsyncTemplate(
92 IMethod method,
93 out string template,
94 out IType? resultType,
95 out string reason )
96{
97 template = "";
98 resultType = null;
99 reason = "";
100
101 if ( method.Parameters.Any( p => p.RefKind is RefKind.Ref or RefKind.Out ) )
102 {
103 // The delegate that calls an async method receives the tuple by value, so the ref and out arguments
104 // cannot be written back.
105 reason = "the method returns an awaitable type and has ref or out parameters";
106
107 return false;
108 }
109
110 var asyncInfo = method.GetAsyncInfo();
111
112 switch ( (method.ReturnType as INamedType)?.Definition.SpecialType )
113 {
114 case SpecialType.Task_T:
115 template = nameof(UniversalInterceptorTemplates.InvokeTaskOfTAsync);
116 resultType = asyncInfo.ResultType;
117
118 return true;
119
120 case SpecialType.Task:
121 template = nameof(UniversalInterceptorTemplates.InvokeTaskAsync);
122
123 return true;
124
125 case SpecialType.ValueTask_T:
126 template = nameof(UniversalInterceptorTemplates.InvokeValueTaskOfTAsync);
127 resultType = asyncInfo.ResultType;
128
129 return true;
130
131 case SpecialType.ValueTask:
132 template = nameof(UniversalInterceptorTemplates.InvokeValueTaskAsync);
133
134 return true;
135
136 default:
137 reason = "its return type is awaitable but is not a Task or a ValueTask";
138
139 return false;
140 }
141}
142The templates
The template for Task<T> awaits InvokeAsync. Its local function returns the task of the intercepted method without
awaiting it:
91// For a method that returns a Task<T>.
92[Template]
93public static async Task<TResult> InvokeTaskOfTAsync<[CompileTime] TArgs, [CompileTime] TResult>(
94 [CompileTime] Type interceptorType,
95 [CompileTime] string memberName,
96 [CompileTime] bool isAsyncLocal )
97 where TArgs : struct, ITuple
98{
99 var argsType = TypeFactory.CreateTupleType( meta.Target.Method.Parameters );
100 var args = (TArgs) CreateArgs( argsType ).Value!;
101 var metadata = (InterceptionMetadata) CreateMetadata().Value!;
102 IUniversalInterceptor interceptor = GetInterceptor( interceptorType, memberName, isAsyncLocal ).Value!;
103
104 return await interceptor.InvokeAsync( args, metadata, ProceedAsync );
105
106 static Task<TResult> ProceedAsync( TArgs receivedArgs )
107 {
108 return GetInvoker( "receivedArgs", out var arguments )
109 .Invoke( arguments )!;
110 }
111}
112The template for Task awaits InvokeAsync too. Its local function must return a Task<ValueTuple>, so it awaits the
task of the intercepted method and returns default( ValueTuple ):
113// For a method that returns a Task. The result of the interceptor is a ValueTuple, which is ignored.
114[Template]
115public static async Task InvokeTaskAsync<[CompileTime] TArgs>(
116 [CompileTime] Type interceptorType,
117 [CompileTime] string memberName,
118 [CompileTime] bool isAsyncLocal )
119 where TArgs : struct, ITuple
120{
121 var argsType = TypeFactory.CreateTupleType( meta.Target.Method.Parameters );
122 var args = (TArgs) CreateArgs( argsType ).Value!;
123 var metadata = (InterceptionMetadata) CreateMetadata().Value!;
124 IUniversalInterceptor interceptor = GetInterceptor( interceptorType, memberName, isAsyncLocal ).Value!;
125
126 await interceptor.InvokeAsync( args, metadata, ProceedAsync );
127
128 static async Task<ValueTuple> ProceedAsync( TArgs receivedArgs )
129 {
130 await GetInvoker( "receivedArgs", out var arguments )
131 .Invoke( arguments )!;
132
133 return default;
134 }
135}
136The templates for ValueTask<T> and ValueTask are identical, with ValueTask instead of Task.
The templates must be async methods. Metalama generates an async interceptor for any method that returns an
awaitable type, so a template that returns the task directly produces code that does not compile.
The templates generate the following interceptors:
internal static async ValueTask WeatherClient_ClearCacheAsync_Interceptor(WeatherClient receiver, CancellationToken cancellationToken)
{
var args = (receiver, cancellationToken);
var metadata = new InterceptionMetadata(typeof(WeatherClient), "ClearCacheAsync", "WeatherClient.ClearCacheAsync(CancellationToken)", true);
IUniversalInterceptor interceptor = CallLog.Current.Value;
await interceptor.InvokeAsync(args, metadata, ProceedAsync);
static async ValueTask<ValueTuple> ProceedAsync((WeatherClient receiver, CancellationToken cancellationToken) receivedArgs)
{
await receivedArgs.receiver.ClearCacheAsync(receivedArgs.cancellationToken);
return default;
}
}
internal static async Task<Forecast> WeatherClient_GetForecastAsync_Interceptor(WeatherClient receiver, string city, int days, CancellationToken cancellationToken)
{
var args = (receiver, city, days, cancellationToken);
var metadata = new InterceptionMetadata(typeof(WeatherClient), "GetForecastAsync", "WeatherClient.GetForecastAsync(string, int, CancellationToken)", true);
IUniversalInterceptor interceptor = CallLog.Current.Value;
return await interceptor.InvokeAsync(args, metadata, ProceedAsync);
static Task<Forecast> ProceedAsync((WeatherClient receiver, string city, int days, CancellationToken cancellationToken) receivedArgs)
{
return receivedArgs.receiver.GetForecastAsync(receivedArgs.city, receivedArgs.days, receivedArgs.cancellationToken);
}
}
internal static async ValueTask<double> WeatherClient_GetTemperatureAsync_Interceptor(WeatherClient receiver, string city, CancellationToken cancellationToken)
{
var args = (receiver, city, cancellationToken);
var metadata = new InterceptionMetadata(typeof(WeatherClient), "GetTemperatureAsync", "WeatherClient.GetTemperatureAsync(string, CancellationToken)", true);
IUniversalInterceptor interceptor = CallLog.Current.Value;
return (double)await interceptor.InvokeAsync(args, metadata, ProceedAsync);
static ValueTask<double> ProceedAsync((WeatherClient receiver, string city, CancellationToken cancellationToken) receivedArgs)
{
return receivedArgs.receiver.GetTemperatureAsync(receivedArgs.city, receivedArgs.cancellationToken);
}
}
internal static async Task WeatherClient_RefreshAsync_Interceptor(WeatherClient receiver, CancellationToken cancellationToken)
{
var args = (receiver, cancellationToken);
var metadata = new InterceptionMetadata(typeof(WeatherClient), "RefreshAsync", "WeatherClient.RefreshAsync(CancellationToken)", true);
IUniversalInterceptor interceptor = CallLog.Current.Value;
await interceptor.InvokeAsync(args, metadata, ProceedAsync);
static async Task<ValueTuple> ProceedAsync((WeatherClient receiver, CancellationToken cancellationToken) receivedArgs)
{
await receivedArgs.receiver.RefreshAsync(receivedArgs.cancellationToken);
return default;
}
}