77using System . Linq ;
88using System . Reflection ;
99using System . Runtime . ExceptionServices ;
10+ using System . Text ;
1011using System . Text . Json ;
1112using System . Threading . Tasks ;
1213
@@ -18,7 +19,6 @@ namespace Microsoft.JSInterop
1819 public static class DotNetDispatcher
1920 {
2021 internal static readonly JsonEncodedText DotNetObjectRefKey = JsonEncodedText . Encode ( "__dotNetObject" ) ;
21- private static readonly Type [ ] EndInvokeParameterTypes = new Type [ ] { typeof ( long ) , typeof ( bool ) , typeof ( JSAsyncCallResult ) } ;
2222
2323 private static readonly ConcurrentDictionary < AssemblyKey , IReadOnlyDictionary < string , ( MethodInfo , Type [ ] ) > > _cachedMethodsByAssembly
2424 = new ConcurrentDictionary < AssemblyKey , IReadOnlyDictionary < string , ( MethodInfo , Type [ ] ) > > ( ) ;
@@ -74,7 +74,6 @@ public static void BeginInvoke(string callId, string assemblyName, string method
7474 // code has to implement its own way of returning async results.
7575 var jsRuntimeBaseInstance = ( JSRuntimeBase ) JSRuntime . Current ;
7676
77-
7877 // Using ExceptionDispatchInfo here throughout because we want to always preserve
7978 // original stack traces.
8079 object syncResult = null ;
@@ -165,81 +164,64 @@ private static object InvokeSynchronously(string assemblyName, string methodIden
165164 }
166165 }
167166
168- private static object [ ] ParseArguments ( string methodIdentifier , string argsJson , Type [ ] parameterTypes )
167+ internal static object [ ] ParseArguments ( string methodIdentifier , string arguments , Type [ ] parameterTypes )
169168 {
170169 if ( parameterTypes . Length == 0 )
171170 {
172171 return Array . Empty < object > ( ) ;
173172 }
174173
175- // There's no direct way to say we want to deserialize as an array with heterogenous
176- // entry types (e.g., [string, int, bool]), so we need to deserialize in two phases.
177- var jsonDocument = JsonDocument . Parse ( argsJson ) ;
178- var shouldDisposeJsonDocument = true ;
179- try
174+ var utf8JsonBytes = Encoding . UTF8 . GetBytes ( arguments ) ;
175+ var reader = new Utf8JsonReader ( utf8JsonBytes ) ;
176+ if ( ! reader . Read ( ) || reader . TokenType != JsonTokenType . StartArray )
180177 {
181- if ( jsonDocument . RootElement . ValueKind != JsonValueKind . Array )
182- {
183- throw new ArgumentException ( $ "Expected a JSON array but got { jsonDocument . RootElement . ValueKind } .") ;
184- }
178+ throw new JsonException ( "Invalid JSON" ) ;
179+ }
185180
186- var suppliedArgsLength = jsonDocument . RootElement . GetArrayLength ( ) ;
181+ var suppliedArgs = new object [ parameterTypes . Length ] ;
187182
188- if ( suppliedArgsLength != parameterTypes . Length )
183+ var index = 0 ;
184+ while ( index < parameterTypes . Length && reader . Read ( ) && reader . TokenType != JsonTokenType . EndArray )
185+ {
186+ var parameterType = parameterTypes [ index ] ;
187+ if ( reader . TokenType == JsonTokenType . StartObject && IsIncorrectDotNetObjectRefUse ( parameterType , reader ) )
189188 {
190- throw new ArgumentException ( $ "In call to '{ methodIdentifier } ', expected { parameterTypes . Length } parameters but received { suppliedArgsLength } .") ;
189+ throw new InvalidOperationException ( $ "In call to '{ methodIdentifier } ', parameter of type ' { parameterType . Name } ' at index { ( index + 1 ) } must be declared as type 'DotNetObjectRef< { parameterType . Name } >' to receive the incoming value .") ;
191190 }
192191
193- // Second, convert each supplied value to the type expected by the method
194- var suppliedArgs = new object [ parameterTypes . Length ] ;
195- var index = 0 ;
196- foreach ( var item in jsonDocument . RootElement . EnumerateArray ( ) )
197- {
198- var parameterType = parameterTypes [ index ] ;
199-
200- if ( parameterType == typeof ( JSAsyncCallResult ) )
201- {
202- // We will pass the JsonDocument instance to JAsyncCallResult and make JSRuntimeBase
203- // responsible for disposing it.
204- shouldDisposeJsonDocument = false ;
205- // For JS async call results, we have to defer the deserialization until
206- // later when we know what type it's meant to be deserialized as
207- suppliedArgs [ index ] = new JSAsyncCallResult ( jsonDocument , item ) ;
208- }
209- else if ( IsIncorrectDotNetObjectRefUse ( item , parameterType ) )
210- {
211- throw new InvalidOperationException ( $ "In call to '{ methodIdentifier } ', parameter of type '{ parameterType . Name } ' at index { ( index + 1 ) } must be declared as type 'DotNetObjectRef<{ parameterType . Name } >' to receive the incoming value.") ;
212- }
213- else
214- {
215- suppliedArgs [ index ] = JsonSerializer . Deserialize ( item . GetRawText ( ) , parameterType , JsonSerializerOptionsProvider . Options ) ;
216- }
217-
218- index ++ ;
219- }
220-
221- if ( shouldDisposeJsonDocument )
222- {
223- jsonDocument . Dispose ( ) ;
224- }
192+ suppliedArgs [ index ] = JsonSerializer . Deserialize ( ref reader , parameterType , JsonSerializerOptionsProvider . Options ) ;
193+ index ++ ;
194+ }
225195
226- return suppliedArgs ;
196+ if ( index < parameterTypes . Length )
197+ {
198+ // If we parsed fewer parameters, we can always make a definitive claim about how many parameters were received.
199+ throw new ArgumentException ( $ "The call to '{ methodIdentifier } ' expects '{ parameterTypes . Length } ' parameters, but received '{ index } '.") ;
227200 }
228- catch
201+
202+ if ( ! reader . Read ( ) || reader . TokenType != JsonTokenType . EndArray )
229203 {
230- // Always dispose the JsonDocument in case of an error.
231- jsonDocument ? . Dispose ( ) ;
232- throw ;
204+ // Either we received more parameters than we expected or the JSON is malformed.
205+ throw new JsonException ( $ "Unexpected JSON token { reader . TokenType } . Ensure that the call to `{ methodIdentifier } ' is supplied with exactly '{ parameterTypes . Length } ' parameters.") ;
233206 }
234207
235- static bool IsIncorrectDotNetObjectRefUse ( JsonElement item , Type parameterType )
208+ return suppliedArgs ;
209+
210+ // Note that the JsonReader instance is intentionally not passed by ref (or an in parameter) since we want a copy of the original reader.
211+ static bool IsIncorrectDotNetObjectRefUse ( Type parameterType , Utf8JsonReader jsonReader )
236212 {
237213 // Check for incorrect use of DotNetObjectRef<T> at the top level. We know it's
238214 // an incorrect use if there's a object that looks like { '__dotNetObject': <some number> },
239215 // but we aren't assigning to DotNetObjectRef{T}.
240- return item . ValueKind == JsonValueKind . Object &&
241- item . TryGetProperty ( DotNetObjectRefKey . EncodedUtf8Bytes , out _ ) &&
242- ! typeof ( IDotNetObjectRef ) . IsAssignableFrom ( parameterType ) ;
216+ if ( jsonReader . Read ( ) &&
217+ jsonReader . TokenType == JsonTokenType . PropertyName &&
218+ jsonReader . ValueTextEquals ( DotNetObjectRefKey . EncodedUtf8Bytes ) )
219+ {
220+ // The JSON payload has the shape we expect from a DotNetObjectRef instance.
221+ return ! parameterType . IsGenericType || parameterType . GetGenericTypeDefinition ( ) != typeof ( DotNetObjectRef < > ) ;
222+ }
223+
224+ return false ;
243225 }
244226 }
245227
@@ -248,9 +230,9 @@ static bool IsIncorrectDotNetObjectRefUse(JsonElement item, Type parameterType)
248230 /// associated <see cref="Task"/> as completed.
249231 /// </summary>
250232 /// <remarks>
251- /// All exceptions from <see cref="EndInvoke(long, bool, JSAsyncCallResult) "/> are caught
233+ /// All exceptions from <see cref="EndInvoke"/> are caught
252234 /// are delivered via JS interop to the JavaScript side when it requests confirmation, as
253- /// the mechanism to call <see cref="EndInvoke(long, bool, JSAsyncCallResult) "/> relies on
235+ /// the mechanism to call <see cref="EndInvoke"/> relies on
254236 /// using JS->.NET interop. This overload is meant for directly triggering completion callbacks
255237 /// for .NET -> JS operations without going through JS interop, so the callsite for this
256238 /// method is responsible for handling any possible exception generated from the arguments
@@ -263,16 +245,40 @@ static bool IsIncorrectDotNetObjectRefUse(JsonElement item, Type parameterType)
263245 /// </exception>
264246 public static void EndInvoke ( string arguments )
265247 {
266- var parsedArgs = ParseArguments (
267- nameof ( EndInvoke ) ,
268- arguments ,
269- EndInvokeParameterTypes ) ;
270-
271- EndInvoke ( ( long ) parsedArgs [ 0 ] , ( bool ) parsedArgs [ 1 ] , ( JSAsyncCallResult ) parsedArgs [ 2 ] ) ;
248+ var jsRuntimeBase = ( JSRuntimeBase ) JSRuntime . Current ;
249+ ParseEndInvokeArguments ( jsRuntimeBase , arguments ) ;
272250 }
273251
274- private static void EndInvoke ( long asyncHandle , bool succeeded , JSAsyncCallResult result )
275- => ( ( JSRuntimeBase ) JSRuntime . Current ) . EndInvokeJS ( asyncHandle , succeeded , result ) ;
252+ internal static void ParseEndInvokeArguments ( JSRuntimeBase jsRuntimeBase , string arguments )
253+ {
254+ var utf8JsonBytes = Encoding . UTF8 . GetBytes ( arguments ) ;
255+
256+ // The payload that we're trying to parse is of the format
257+ // [ taskId: long, success: boolean, value: string? | object ]
258+ // where value is the .NET type T originally specified on InvokeAsync<T> or the error string if success is false.
259+ // We parse the first two arguments and call in to JSRuntimeBase to deserialize the actual value.
260+
261+ var reader = new Utf8JsonReader ( utf8JsonBytes ) ;
262+
263+ if ( ! reader . Read ( ) || reader . TokenType != JsonTokenType . StartArray )
264+ {
265+ throw new JsonException ( "Invalid JSON" ) ;
266+ }
267+
268+ reader . Read ( ) ;
269+ var taskId = reader . GetInt64 ( ) ;
270+
271+ reader . Read ( ) ;
272+ var success = reader . GetBoolean ( ) ;
273+
274+ reader . Read ( ) ;
275+ jsRuntimeBase . EndInvokeJS ( taskId , success , ref reader ) ;
276+
277+ if ( ! reader . Read ( ) || reader . TokenType != JsonTokenType . EndArray )
278+ {
279+ throw new JsonException ( "Invalid JSON" ) ;
280+ }
281+ }
276282
277283 /// <summary>
278284 /// Releases the reference to the specified .NET object. This allows the .NET runtime
@@ -362,7 +368,13 @@ private static Assembly GetRequiredLoadedAssembly(AssemblyKey assemblyKey)
362368 // In some edge cases this might force developers to explicitly call something on the
363369 // target assembly (from .NET) before they can invoke its allowed methods from JS.
364370 var loadedAssemblies = AppDomain . CurrentDomain . GetAssemblies ( ) ;
365- return loadedAssemblies . FirstOrDefault ( a => new AssemblyKey ( a ) . Equals ( assemblyKey ) )
371+
372+ // Using LastOrDefault to workaround for https://github.com/dotnet/arcade/issues/2816.
373+ // In most ordinary scenarios, we wouldn't have two instances of the same Assembly in the AppDomain
374+ // so this doesn't change the outcome.
375+ var assembly = loadedAssemblies . LastOrDefault ( a => new AssemblyKey ( a ) . Equals ( assemblyKey ) ) ;
376+
377+ return assembly
366378 ?? throw new ArgumentException ( $ "There is no loaded assembly with the name '{ assemblyKey . AssemblyName } '.") ;
367379 }
368380
@@ -396,6 +408,5 @@ public bool Equals(AssemblyKey other)
396408
397409 public override int GetHashCode ( ) => StringComparer . Ordinal . GetHashCode ( AssemblyName ) ;
398410 }
399-
400411 }
401412}
0 commit comments