Переглянути джерело

Eliminate framework use of 'IJSUnmarshalledRuntime' (#46693)

Co-authored-by: Steve Sanderson <[email protected]>
Mackinnon Buck 3 роки тому
батько
коміт
8fbf7f29d5
29 змінених файлів з 413 додано та 307 видалено
  1. 2 2
      src/Components/Shared/src/BrowserNavigationManagerInterop.cs
  2. 19 30
      src/Components/Web.JS/src/Boot.WebAssembly.ts
  3. 37 29
      src/Components/Web.JS/src/GlobalExports.ts
  4. 11 19
      src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts
  5. 3 6
      src/Components/Web.JS/src/Platform/WebAssemblyConfigLoader.ts
  6. 0 8
      src/Components/Web/src/Forms/InputFile.cs
  7. 1 0
      src/Components/Web/src/PublicAPI.Unshipped.txt
  8. 26 9
      src/Components/WebAssembly/JSInterop/src/InternalCalls.cs
  9. 1 0
      src/Components/WebAssembly/JSInterop/src/Microsoft.JSInterop.WebAssembly.csproj
  10. 10 30
      src/Components/WebAssembly/JSInterop/src/WebAssemblyJSRuntime.cs
  11. 1 1
      src/Components/WebAssembly/WebAssembly.Authentication/test/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests.csproj
  12. 15 15
      src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs
  13. 1 7
      src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyCultureProvider.cs
  14. 27 38
      src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs
  15. 12 9
      src/Components/WebAssembly/WebAssembly/src/HotReload/WebAssemblyHotReload.cs
  16. 18 7
      src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs
  17. 20 22
      src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs
  18. 33 0
      src/Components/WebAssembly/WebAssembly/src/Services/IInternalJSImportMethods.cs
  19. 93 0
      src/Components/WebAssembly/WebAssembly/src/Services/InternalJSImportMethods.cs
  20. 1 3
      src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationInterception.cs
  21. 1 1
      src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs
  22. 1 1
      src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyCultureProviderTest.cs
  23. 13 13
      src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs
  24. 4 4
      src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs
  25. 53 0
      src/Components/WebAssembly/WebAssembly/test/TestInternalJSImportMethods.cs
  26. 0 48
      src/Components/WebAssembly/WebAssembly/test/TestJSUnmarshalledRuntime.cs
  27. 3 3
      src/Components/test/E2ETest/Tests/InteropTest.cs
  28. 6 2
      src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs
  29. 1 0
      src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj

+ 2 - 2
src/Components/Shared/src/BrowserNavigationManagerInterop.cs

@@ -10,9 +10,9 @@ internal static class BrowserNavigationManagerInterop
 
     public const string EnableNavigationInterception = Prefix + "enableNavigationInterception";
 
-    public const string GetLocationHref = Prefix + "getUnmarshalledLocationHref";
+    public const string GetLocationHref = Prefix + "getLocationHref";
 
-    public const string GetBaseUri = Prefix + "getUnmarshalledBaseURI";
+    public const string GetBaseUri = Prefix + "getBaseURI";
 
     public const string NavigateTo = Prefix + "navigateTo";
 

+ 19 - 30
src/Components/Web.JS/src/Boot.WebAssembly.ts

@@ -5,14 +5,14 @@
 import { DotNet } from '@microsoft/dotnet-js-interop';
 import { Blazor } from './GlobalExports';
 import * as Environment from './Environment';
-import { byteArrayBeingTransferred, Module, BINDING, monoPlatform } from './Platform/Mono/MonoPlatform';
+import { Module, BINDING, monoPlatform } from './Platform/Mono/MonoPlatform';
 import { renderBatch, getRendererer, attachRootComponentToElement, attachRootComponentToLogicalElement } from './Rendering/Renderer';
 import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch';
 import { shouldAutoStart } from './BootCommon';
 import { WebAssemblyResourceLoader } from './Platform/WebAssemblyResourceLoader';
 import { WebAssemblyConfigLoader } from './Platform/WebAssemblyConfigLoader';
 import { BootConfigResult } from './Platform/BootConfig';
-import { Pointer, System_Array, System_Boolean, System_Byte, System_Int, System_Object, System_String } from './Platform/Platform';
+import { Pointer } from './Platform/Platform';
 import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
 import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
 import { discoverComponents, discoverPersistedState, WebAssemblyComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
@@ -52,9 +52,9 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
 
   // Configure JS interop
   Blazor._internal.invokeJSFromDotNet = invokeJSFromDotNet;
+  Blazor._internal.invokeJSJson = invokeJSJson;
   Blazor._internal.endInvokeDotNetFromJS = endInvokeDotNetFromJS;
   Blazor._internal.receiveByteArray = receiveByteArray;
-  Blazor._internal.retrieveByteArray = retrieveByteArray;
 
   // Configure environment for execution under Mono WebAssembly with shared-memory rendering
   const platform = Environment.setPlatform(monoPlatform);
@@ -73,12 +73,6 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
     }
   };
 
-  // Configure navigation via JS Interop
-  const getBaseUri = Blazor._internal.navigationManager.getBaseURI;
-  const getLocationHref = Blazor._internal.navigationManager.getLocationHref;
-  Blazor._internal.navigationManager.getUnmarshalledBaseURI = () => BINDING.js_string_to_mono_string(getBaseUri());
-  Blazor._internal.navigationManager.getUnmarshalledLocationHref = () => BINDING.js_string_to_mono_string(getLocationHref());
-
   Blazor._internal.navigationManager.listenForNavigationEvents(async (uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
     await DotNet.invokeMethodAsync(
       'Microsoft.AspNetCore.Components.WebAssembly',
@@ -114,13 +108,13 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
   Blazor._internal.registeredComponents = {
     getRegisteredComponentsCount: () => componentAttacher.getCount(),
     getId: (index) => componentAttacher.getId(index),
-    getAssembly: (id) => BINDING.js_string_to_mono_string(componentAttacher.getAssembly(id)),
-    getTypeName: (id) => BINDING.js_string_to_mono_string(componentAttacher.getTypeName(id)),
-    getParameterDefinitions: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterDefinitions(id) || ''),
-    getParameterValues: (id) => BINDING.js_string_to_mono_string(componentAttacher.getParameterValues(id) || ''),
+    getAssembly: (id) => componentAttacher.getAssembly(id),
+    getTypeName: (id) => componentAttacher.getTypeName(id),
+    getParameterDefinitions: (id) => componentAttacher.getParameterDefinitions(id) || '',
+    getParameterValues: (id) => componentAttacher.getParameterValues(id) || '',
   };
 
-  Blazor._internal.getPersistedState = () => BINDING.js_string_to_mono_string(discoverPersistedState(document) || '');
+  Blazor._internal.getPersistedState = () => discoverPersistedState(document) || '';
 
   Blazor._internal.attachRootComponentToElement = (selector, componentId, rendererId: any) => {
     const element = componentAttacher.resolveRegisteredElement(selector);
@@ -191,26 +185,21 @@ function invokeJSFromDotNet(callInfo: Pointer, arg0: any, arg1: any, arg2: any):
   }
 }
 
-function endInvokeDotNetFromJS(callId: System_String, success: System_Boolean, resultJsonOrErrorMessage: System_String): void {
-  const callIdString = BINDING.conv_string(callId)!;
-  const successBool = (success as any as number) !== 0;
-  const resultJsonOrErrorMessageString = BINDING.conv_string(resultJsonOrErrorMessage)!;
-  DotNet.jsCallDispatcher.endInvokeDotNetFromJS(callIdString, successBool, resultJsonOrErrorMessageString);
+function invokeJSJson(identifier: string, targetInstanceId: number, resultType: number, argsJson: string, asyncHandle: number): string | null {
+  if (asyncHandle !== 0) {
+    DotNet.jsCallDispatcher.beginInvokeJSFromDotNet(asyncHandle, identifier, argsJson, resultType, targetInstanceId);
+    return null;
+  } else {
+    return DotNet.jsCallDispatcher.invokeJSFromDotNet(identifier, argsJson, resultType, targetInstanceId);
+  }
 }
 
-function receiveByteArray(id: System_Int, data: System_Array<System_Byte>): void {
-  const idLong = id as unknown as number;
-  const dataByteArray = monoPlatform.toUint8Array(data);
-  DotNet.jsCallDispatcher.receiveByteArray(idLong, dataByteArray);
+function endInvokeDotNetFromJS(callId: string, success: boolean, resultJsonOrErrorMessage: string): void {
+  DotNet.jsCallDispatcher.endInvokeDotNetFromJS(callId, success, resultJsonOrErrorMessage);
 }
 
-function retrieveByteArray(): System_Object {
-  if (byteArrayBeingTransferred === null) {
-    throw new Error('Byte array not available for transfer');
-  }
-
-  const typedArray = BINDING.js_typed_array_to_array(byteArrayBeingTransferred);
-  return typedArray;
+function receiveByteArray(id: number, data: Uint8Array): void {
+  DotNet.jsCallDispatcher.receiveByteArray(id, data);
 }
 
 function inAuthRedirectIframe(): boolean {

+ 37 - 29
src/Components/Web.JS/src/GlobalExports.ts

@@ -12,7 +12,7 @@ import { NavigationLock } from './NavigationLock';
 import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
 import { CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
 import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
-import { Platform, Pointer, System_String, System_Array, System_Object, System_Boolean, System_Byte, System_Int } from './Platform/Platform';
+import { Platform, Pointer } from './Platform/Platform';
 import { getNextChunk, receiveDotNetDataStream } from './StreamingInterop';
 import { RootComponentsFunctions } from './Rendering/JSRootComponents';
 import { attachWebRendererInterop } from './Rendering/WebRendererInteropMethods';
@@ -29,41 +29,49 @@ interface IBlazor {
   rootComponents: typeof RootComponentsFunctions;
 
   _internal: {
-    navigationManager: typeof navigationManagerInternalFunctions | any,
-    domWrapper: typeof domFunctions,
-    Virtualize: typeof Virtualize,
-    PageTitle: typeof PageTitle,
+    navigationManager: typeof navigationManagerInternalFunctions | any;
+    domWrapper: typeof domFunctions;
+    Virtualize: typeof Virtualize;
+    PageTitle: typeof PageTitle;
     forceCloseConnection?: () => Promise<void>;
-    InputFile?: typeof InputFile,
-    NavigationLock: typeof NavigationLock,
+    InputFile?: typeof InputFile;
+    NavigationLock: typeof NavigationLock;
     invokeJSFromDotNet?: (callInfo: Pointer, arg0: any, arg1: any, arg2: any) => any;
-    endInvokeDotNetFromJS?: (callId: System_String, success: System_Boolean, resultJsonOrErrorMessage: System_String) => void;
-    receiveByteArray?: (id: System_Int, data: System_Array<System_Byte>) => void;
-    retrieveByteArray?: () => System_Object;
-    getPersistedState?: () => System_String;
+    invokeJSJson?: (identifier: string, targetInstanceId: number, resultType: number, argsJson: string, asyncHandle: number) => string | null;
+    endInvokeDotNetFromJS?: (callId: string, success: boolean, resultJsonOrErrorMessage: string) => void;
+    receiveByteArray?: (id: number, data: Uint8Array) => void;
+    getPersistedState?: () => string;
     attachRootComponentToElement?: (arg0: any, arg1: any, arg2: any, arg3: any) => void;
     registeredComponents?: {
-      getRegisteredComponentsCount: () => number,
-      getId: (index) => number,
-      getAssembly: (id) => System_String,
-      getTypeName: (id) => System_String,
-      getParameterDefinitions: (id) => System_String,
-      getParameterValues: (id) => any,
+      getRegisteredComponentsCount: () => number;
+      getId: (index) => number;
+      getAssembly: (id) => string;
+      getTypeName: (id) => string;
+      getParameterDefinitions: (id) => string;
+      getParameterValues: (id) => any;
     };
-    renderBatch?: (browserRendererId: number, batchAddress: Pointer) => void,
-    getConfig?: (dotNetFileName: System_String) => System_Object | undefined,
-    getApplicationEnvironment?: () => System_String,
-    dotNetCriticalError?: any
-    loadLazyAssembly?: any,
-    loadSatelliteAssemblies?: any,
-    sendJSDataStream?: (data: any, streamId: number, chunkSize: number) => void,
-    getJSDataStreamChunk?: (data: any, position: number, chunkSize: number) => Promise<Uint8Array>,
-    receiveDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void,
-    attachWebRendererInterop?: typeof attachWebRendererInterop,
+    renderBatch?: (browserRendererId: number, batchAddress: Pointer) => void;
+    getConfig?: (fileName: string) => Uint8Array | undefined;
+    getApplicationEnvironment?: () => string;
+    dotNetCriticalError?: any;
+    loadLazyAssembly?: any;
+    loadSatelliteAssemblies?: any;
+    sendJSDataStream?: (data: any, streamId: number, chunkSize: number) => void;
+    getJSDataStreamChunk?: (data: any, position: number, chunkSize: number) => Promise<Uint8Array>;
+    receiveDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void;
+    attachWebRendererInterop?: typeof attachWebRendererInterop;
+
+    // JSExport APIs
+    dotNetExports?: {
+      InvokeDotNet: (assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number, argsJson: string) => string | null;
+      EndInvokeJS: (argsJson: string) => void;
+      BeginInvokeDotNet: (callId: string | null, assemblyNameOrDotNetObjectId: string, methodIdentifier: string, argsJson: string) => void;
+      ReceiveByteArrayFromJS: (id: number, data: Uint8Array) => void;
+    }
 
     // APIs invoked by hot reload
-    applyHotReload?: (id: string, metadataDelta: string, ilDelta: string, pdbDelta: string | undefined) => void,
-    getApplyUpdateCapabilities?: () => string,
+    applyHotReload?: (id: string, metadataDelta: string, ilDelta: string, pdbDelta: string | undefined) => void;
+    getApplyUpdateCapabilities?: () => string;
   }
 }
 

+ 11 - 19
src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

@@ -369,6 +369,12 @@ async function createRuntimeInstance(resourceLoader: WebAssemblyResourceLoader):
   setModuleImports('blazor-internal', {
     Blazor: { _internal: Blazor._internal },
   });
+  const exports = await runtime.getAssemblyExports('Microsoft.AspNetCore.Components.WebAssembly');
+  Object.assign(Blazor._internal, {
+    dotNetExports: {
+      ...exports.Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime,
+    },
+  });
   attachInteropInvoker();
   if (resourceLoader.bootConfig.debugBuild && resourceLoader.bootConfig.cacheBootResources) {
     resourceLoader.logToConsole();
@@ -412,7 +418,6 @@ async function loadSatelliteAssemblies(resourceLoader: WebAssemblyResourceLoader
     }));
 }
 
-
 async function loadLazyAssembly(resourceLoader: WebAssemblyResourceLoader, assemblyNameToLoad: string): Promise<{ dll: Uint8Array, pdb: Uint8Array | null }> {
   const resources = resourceLoader.bootConfig.resources;
   const lazyAssemblies = resources.lazyAssembly;
@@ -449,19 +454,7 @@ function getArrayDataPointer<T>(array: System_Array<T>): number {
   return <number><any>array + 12; // First byte from here is length, then following bytes are entries
 }
 
-function bindStaticMethod(assembly: string, typeName: string, method: string) {
-  // Fully qualified name looks like this: "[debugger-test] Math:IntAdd"
-  const fqn = `[${assembly}] ${typeName}:${method}`;
-  return BINDING.bind_static_method(fqn);
-}
-
-export let byteArrayBeingTransferred: Uint8Array | null = null;
 function attachInteropInvoker(): void {
-  const dotNetDispatcherInvokeMethodHandle = bindStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime', 'InvokeDotNet');
-  const dotNetDispatcherBeginInvokeMethodHandle = bindStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime', 'BeginInvokeDotNet');
-  const dotNetDispatcherEndInvokeJSMethodHandle = bindStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime', 'EndInvokeJS');
-  const dotNetDispatcherNotifyByteArrayAvailableMethodHandle = bindStaticMethod('Microsoft.AspNetCore.Components.WebAssembly', 'Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime', 'NotifyByteArrayAvailable');
-
   DotNet.attachDispatcher({
     beginInvokeDotNetFromJS: (callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: any | null, argsJson: string): void => {
       assertHeapIsNotLocked();
@@ -474,7 +467,7 @@ function attachInteropInvoker(): void {
         ? dotNetObjectId.toString()
         : assemblyName;
 
-      dotNetDispatcherBeginInvokeMethodHandle(
+      Blazor._internal.dotNetExports!.BeginInvokeDotNet!(
         callId ? callId.toString() : null,
         assemblyNameOrDotNetObjectId,
         methodIdentifier,
@@ -482,18 +475,17 @@ function attachInteropInvoker(): void {
       );
     },
     endInvokeJSFromDotNet: (asyncHandle, succeeded, serializedArgs): void => {
-      dotNetDispatcherEndInvokeJSMethodHandle(serializedArgs);
+      Blazor._internal.dotNetExports!.EndInvokeJS(serializedArgs);
     },
     sendByteArray: (id: number, data: Uint8Array): void => {
-      byteArrayBeingTransferred = data;
-      dotNetDispatcherNotifyByteArrayAvailableMethodHandle(id);
+      Blazor._internal.dotNetExports!.ReceiveByteArrayFromJS(id, data);
     },
     invokeDotNetFromJS: (assemblyName, methodIdentifier, dotNetObjectId, argsJson) => {
       assertHeapIsNotLocked();
-      return dotNetDispatcherInvokeMethodHandle(
+      return Blazor._internal.dotNetExports!.InvokeDotNet(
         assemblyName ? assemblyName : null,
         methodIdentifier,
-        dotNetObjectId ? dotNetObjectId.toString() : null,
+        dotNetObjectId ?? 0,
         argsJson,
       ) as string;
     },

+ 3 - 6
src/Components/Web.JS/src/Platform/WebAssemblyConfigLoader.ts

@@ -3,22 +3,19 @@
 
 import { BootConfigResult } from './BootConfig';
 import { WebAssemblyStartOptions } from './WebAssemblyStartOptions';
-import { System_String, System_Object } from './Platform';
 import { Blazor } from '../GlobalExports';
-import { BINDING } from './Mono/MonoPlatform';
 
 export class WebAssemblyConfigLoader {
   static async initAsync(bootConfigResult: BootConfigResult, startOptions: Partial<WebAssemblyStartOptions>): Promise<void> {
-    Blazor._internal.getApplicationEnvironment = () => BINDING.js_string_to_mono_string(bootConfigResult.applicationEnvironment);
+    Blazor._internal.getApplicationEnvironment = () => bootConfigResult.applicationEnvironment;
 
     const configFiles = await Promise.all((bootConfigResult.bootConfig.config || [])
       .filter(name => name === 'appsettings.json' || name === `appsettings.${bootConfigResult.applicationEnvironment}.json`)
       .map(async name => ({ name, content: await getConfigBytes(name) })));
 
-    Blazor._internal.getConfig = (dotNetFileName: System_String) : System_Object | undefined => {
-      const fileName = BINDING.conv_string(dotNetFileName);
+    Blazor._internal.getConfig = (fileName: string) : Uint8Array | undefined => {
       const resolvedFile = configFiles.find(f => f.name === fileName);
-      return resolvedFile ? BINDING.js_typed_array_to_array(resolvedFile.content) : undefined;
+      return resolvedFile ? resolvedFile.content : undefined;
     };
 
     async function getConfigBytes(file: string): Promise<Uint8Array> {

+ 0 - 8
src/Components/Web/src/Forms/InputFile.cs

@@ -14,8 +14,6 @@ public class InputFile : ComponentBase, IInputFileJsCallbacks, IDisposable
 {
     private ElementReference _inputFileElement;
 
-    private IJSUnmarshalledRuntime? _jsUnmarshalledRuntime;
-
     private InputFileJsCallbacksRelay? _jsCallbacksRelay;
 
     [Inject]
@@ -46,12 +44,6 @@ public class InputFile : ComponentBase, IInputFileJsCallbacks, IDisposable
         protected set => _inputFileElement = value!.Value;
     }
 
-    /// <inheritdoc/>
-    protected override void OnInitialized()
-    {
-        _jsUnmarshalledRuntime = JSRuntime as IJSUnmarshalledRuntime;
-    }
-
     /// <inheritdoc/>
     protected override async Task OnAfterRenderAsync(bool firstRender)
     {

+ 1 - 0
src/Components/Web/src/PublicAPI.Unshipped.txt

@@ -1,4 +1,5 @@
 #nullable enable
+*REMOVED*override Microsoft.AspNetCore.Components.Forms.InputFile.OnInitialized() -> void
 Microsoft.AspNetCore.Components.Web.HtmlComponent
 Microsoft.AspNetCore.Components.Web.HtmlComponent.ToHtmlString() -> string!
 Microsoft.AspNetCore.Components.Web.HtmlComponent.WaitForQuiescenceAsync() -> System.Threading.Tasks.Task!

+ 26 - 9
src/Components/WebAssembly/JSInterop/src/InternalCalls.cs

@@ -3,19 +3,36 @@
 
 using System.Diagnostics.CodeAnalysis;
 using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices.JavaScript;
 
 namespace WebAssembly.JSInterop;
 
-/// <summary>
-/// Methods that map to the functions compiled into the Mono WebAssembly runtime,
-/// as defined by 'mono_add_internal_call' calls in driver.c.
-/// </summary>
-internal static class InternalCalls
+internal static partial class InternalCalls
 {
-    // The exact namespace, type, and method names must match the corresponding entries
-    // in driver.c in the Mono distribution
-    /// See: https://github.com/mono/mono/blob/90574987940959fe386008a850982ea18236a533/sdks/wasm/src/driver.c#L318-L319
-
+    // This method only exists for backwards compatibility and will be removed in the future.
+    // The exact namespace, type, and method name must match the corresponding entries
+    // in driver.c in the Mono distribution.
+    // See: https://github.com/mono/mono/blob/90574987940959fe386008a850982ea18236a533/sdks/wasm/src/driver.c#L318-L319
     [MethodImpl(MethodImplOptions.InternalCall)]
+    [Obsolete]
     public static extern TRes InvokeJS<T0, T1, T2, TRes>(out string exception, ref JSCallInfo callInfo, [AllowNull] T0 arg0, [AllowNull] T1 arg1, [AllowNull] T2 arg2);
+
+    [JSImport("Blazor._internal.invokeJSJson", "blazor-internal")]
+    public static partial string InvokeJSJson(
+        string identifier,
+        [JSMarshalAs<JSType.Number>] long targetInstanceId,
+        int resultType,
+        string argsJson,
+        [JSMarshalAs<JSType.Number>] long asyncHandle);
+
+    [JSImport("Blazor._internal.endInvokeDotNetFromJS", "blazor-internal")]
+    public static partial void EndInvokeDotNetFromJS(
+        string? id,
+        bool success,
+        string jsonOrError);
+
+    [JSImport("Blazor._internal.receiveByteArray", "blazor-internal")]
+    public static partial void ReceiveByteArray(
+        int id,
+        byte[] data);
 }

+ 1 - 0
src/Components/WebAssembly/JSInterop/src/Microsoft.JSInterop.WebAssembly.csproj

@@ -8,6 +8,7 @@
     <IsShippingPackage>true</IsShippingPackage>
     <Nullable>enable</Nullable>
     <IsTrimmable>true</IsTrimmable>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <!-- TODO: Address Native AOT analyzer warnings https://github.com/dotnet/aspnetcore/issues/45473 -->
     <EnableAOTAnalyzer>false</EnableAOTAnalyzer>
   </PropertyGroup>

+ 10 - 30
src/Components/WebAssembly/JSInterop/src/WebAssemblyJSRuntime.cs

@@ -25,35 +25,20 @@ public abstract class WebAssemblyJSRuntime : JSInProcessRuntime, IJSUnmarshalled
     /// <inheritdoc />
     protected override string InvokeJS(string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId)
     {
-        var callInfo = new JSCallInfo
+        try
         {
-            FunctionIdentifier = identifier,
-            TargetInstanceId = targetInstanceId,
-            ResultType = resultType,
-            MarshalledCallArgsJson = argsJson ?? "[]",
-            MarshalledCallAsyncHandle = default
-        };
-
-        var result = InternalCalls.InvokeJS<object, object, object, string>(out var exception, ref callInfo, null, null, null);
-
-        return exception != null
-            ? throw new JSException(exception)
-            : result;
+            return InternalCalls.InvokeJSJson(identifier, targetInstanceId, (int)resultType, argsJson ?? "[]", 0);
+        }
+        catch (Exception ex)
+        {
+            throw new JSException(ex.Message, ex);
+        }
     }
 
     /// <inheritdoc />
     protected override void BeginInvokeJS(long asyncHandle, string identifier, [StringSyntax(StringSyntaxAttribute.Json)] string? argsJson, JSCallResultType resultType, long targetInstanceId)
     {
-        var callInfo = new JSCallInfo
-        {
-            FunctionIdentifier = identifier,
-            TargetInstanceId = targetInstanceId,
-            ResultType = resultType,
-            MarshalledCallArgsJson = argsJson ?? "[]",
-            MarshalledCallAsyncHandle = asyncHandle
-        };
-
-        InternalCalls.InvokeJS<object, object, object, string>(out _, ref callInfo, null, null, null);
+        InternalCalls.InvokeJSJson(identifier, targetInstanceId, (int)resultType, argsJson ?? "[]", asyncHandle);
     }
 
     /// <inheritdoc />
@@ -63,18 +48,13 @@ public abstract class WebAssemblyJSRuntime : JSInProcessRuntime, IJSUnmarshalled
         var resultJsonOrErrorMessage = dispatchResult.Success
             ? dispatchResult.ResultJson!
             : dispatchResult.Exception!.ToString();
-#pragma warning disable CS0618 // Type or member is obsolete
-        InvokeUnmarshalled<string?, bool, string, object>("Blazor._internal.endInvokeDotNetFromJS",
-            callInfo.CallId, dispatchResult.Success, resultJsonOrErrorMessage);
-#pragma warning restore CS0618 // Type or member is obsolete
+        InternalCalls.EndInvokeDotNetFromJS(callInfo.CallId, dispatchResult.Success, resultJsonOrErrorMessage);
     }
 
     /// <inheritdoc />
     protected override void SendByteArray(int id, byte[] data)
     {
-#pragma warning disable CS0618 // Type or member is obsolete
-        InvokeUnmarshalled<int, byte[], object>("Blazor._internal.receiveByteArray", id, data);
-#pragma warning restore CS0618 // Type or member is obsolete
+        InternalCalls.ReceiveByteArray(id, data);
     }
 
     [Obsolete("This method is obsolete. Use JSImportAttribute instead.")]

+ 1 - 1
src/Components/WebAssembly/WebAssembly.Authentication/test/Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests.csproj

@@ -13,7 +13,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <Compile Include="..\..\WebAssembly\test\TestJSUnmarshalledRuntime.cs" Link="Shared\TestWebAssemblyJSRuntimeInvoker.cs" />
+    <Compile Include="..\..\WebAssembly\test\TestInternalJSImportMethods.cs" Link="Shared\TestInternalJSImportMethods.cs" />
   </ItemGroup>
 
 </Project>

+ 15 - 15
src/Components/WebAssembly/WebAssembly.Authentication/test/WebAssemblyAuthenticationServiceCollectionExtensionsTests.cs

@@ -17,7 +17,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void CanResolve_AccessTokenProvider()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         builder.Services.AddApiAuthorization();
         var host = builder.Build();
 
@@ -27,7 +27,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void CanResolve_IRemoteAuthenticationService()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         builder.Services.AddApiAuthorization();
         var host = builder.Build();
 
@@ -37,7 +37,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void ApiAuthorizationOptions_ConfigurationDefaultsGetApplied()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         builder.Services.AddApiAuthorization();
         var host = builder.Build();
 
@@ -71,7 +71,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void ApiAuthorizationOptionsConfigurationCallback_GetsCalledOnce()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         var calls = 0;
         builder.Services.AddApiAuthorization(options =>
         {
@@ -98,7 +98,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void ApiAuthorizationTestAuthenticationState_SetsUpConfiguration()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         var calls = 0;
         builder.Services.AddApiAuthorization<TestAuthenticationState>(options => calls++);
 
@@ -124,7 +124,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void ApiAuthorizationTestAuthenticationState_NoCallback_SetsUpConfiguration()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         builder.Services.AddApiAuthorization<TestAuthenticationState>();
 
         var host = builder.Build();
@@ -147,7 +147,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void ApiAuthorizationCustomAuthenticationStateAndAccount_SetsUpConfiguration()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         var calls = 0;
         builder.Services.AddApiAuthorization<TestAuthenticationState, TestAccount>(options => calls++);
 
@@ -173,7 +173,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void ApiAuthorizationTestAuthenticationStateAndAccount_NoCallback_SetsUpConfiguration()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         builder.Services.AddApiAuthorization<TestAuthenticationState, TestAccount>();
 
         var host = builder.Build();
@@ -196,7 +196,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void ApiAuthorizationOptions_DefaultsCanBeOverriden()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         builder.Services.AddApiAuthorization(options =>
         {
             options.AuthenticationPaths.LogInPath = "a";
@@ -247,7 +247,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void OidcOptions_ConfigurationDefaultsGetApplied()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         builder.Services.Replace(ServiceDescriptor.Singleton<NavigationManager, TestNavigationManager>());
         builder.Services.AddOidcAuthentication(options => { });
         var host = builder.Build();
@@ -286,7 +286,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void OidcOptions_DefaultsCanBeOverriden()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         builder.Services.AddOidcAuthentication(options =>
         {
             options.AuthenticationPaths.LogInPath = "a";
@@ -348,7 +348,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void AddOidc_ConfigurationGetsCalledOnce()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         var calls = 0;
 
         builder.Services.AddOidcAuthentication(options => calls++);
@@ -365,7 +365,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void AddOidc_CustomState_SetsUpConfiguration()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         var calls = 0;
 
         builder.Services.AddOidcAuthentication<TestAuthenticationState>(options => options.ProviderOptions.Authority = (++calls).ToString(CultureInfo.InvariantCulture));
@@ -387,7 +387,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void AddOidc_CustomStateAndAccount_SetsUpConfiguration()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         var calls = 0;
 
         builder.Services.AddOidcAuthentication<TestAuthenticationState, TestAccount>(options => options.ProviderOptions.Authority = (++calls).ToString(CultureInfo.InvariantCulture));
@@ -409,7 +409,7 @@ public class WebAssemblyAuthenticationServiceCollectionExtensionsTests
     [Fact]
     public void OidcProviderOptionsAndDependencies_NotResolvedFromRootScope()
     {
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
 
         var calls = 0;
 

+ 1 - 7
src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyCultureProvider.cs

@@ -6,8 +6,6 @@ using System.Globalization;
 using System.Runtime.InteropServices.JavaScript;
 using System.Runtime.Loader;
 using System.Runtime.Versioning;
-using Microsoft.AspNetCore.Components.WebAssembly.Services;
-using Microsoft.JSInterop;
 
 namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;
 
@@ -19,12 +17,9 @@ internal partial class WebAssemblyCultureProvider
     internal const string GetSatelliteAssemblies = "window.Blazor._internal.getSatelliteAssemblies";
     internal const string ReadSatelliteAssemblies = "window.Blazor._internal.readSatelliteAssemblies";
 
-    private readonly IJSUnmarshalledRuntime _invoker;
-
     // For unit testing.
-    internal WebAssemblyCultureProvider(IJSUnmarshalledRuntime invoker, CultureInfo initialCulture, CultureInfo initialUICulture)
+    internal WebAssemblyCultureProvider(CultureInfo initialCulture, CultureInfo initialUICulture)
     {
-        _invoker = invoker;
         InitialCulture = initialCulture;
         InitialUICulture = initialUICulture;
     }
@@ -38,7 +33,6 @@ internal partial class WebAssemblyCultureProvider
     internal static void Initialize()
     {
         Instance = new WebAssemblyCultureProvider(
-            DefaultWebAssemblyJSRuntime.Instance,
             initialCulture: CultureInfo.CurrentCulture,
             initialUICulture: CultureInfo.CurrentUICulture);
     }

+ 27 - 38
src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs

@@ -45,10 +45,9 @@ public sealed class WebAssemblyHostBuilder
     {
         // We don't use the args for anything right now, but we want to accept them
         // here so that it shows up this way in the project templates.
-        var jsRuntime = DefaultWebAssemblyJSRuntime.Instance;
         var builder = new WebAssemblyHostBuilder(
-            jsRuntime,
-            jsRuntime.ReadJsonSerializerOptions());
+            InternalJSImportMethods.Instance,
+            DefaultWebAssemblyJSRuntime.Instance.ReadJsonSerializerOptions());
 
         WebAssemblyCultureProvider.Initialize();
 
@@ -62,7 +61,9 @@ public sealed class WebAssemblyHostBuilder
     /// <summary>
     /// Creates an instance of <see cref="WebAssemblyHostBuilder"/> with the minimal configuration.
     /// </summary>
-    internal WebAssemblyHostBuilder(IJSUnmarshalledRuntime jsRuntime, JsonSerializerOptions jsonOptions)
+    internal WebAssemblyHostBuilder(
+        IInternalJSImportMethods jsMethods,
+        JsonSerializerOptions jsonOptions)
     {
         // Private right now because we don't have much reason to expose it. This can be exposed
         // in the future if we want to give people a choice between CreateDefault and something
@@ -74,12 +75,12 @@ public sealed class WebAssemblyHostBuilder
         Logging = new LoggingBuilder(Services);
 
         // Retrieve required attributes from JSRuntimeInvoker
-        InitializeNavigationManager(jsRuntime);
-        InitializeRegisteredRootComponents(jsRuntime);
-        InitializePersistedState(jsRuntime);
+        InitializeNavigationManager(jsMethods);
+        InitializeRegisteredRootComponents(jsMethods);
+        InitializePersistedState(jsMethods);
         InitializeDefaultServices();
 
-        var hostEnvironment = InitializeEnvironment(jsRuntime);
+        var hostEnvironment = InitializeEnvironment(jsMethods);
         HostEnvironment = hostEnvironment;
 
         _createServiceProvider = () =>
@@ -89,10 +90,9 @@ public sealed class WebAssemblyHostBuilder
     }
 
     [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Root components are expected to be defined in assemblies that do not get trimmed.")]
-    private void InitializeRegisteredRootComponents(IJSUnmarshalledRuntime jsRuntime)
+    private void InitializeRegisteredRootComponents(IInternalJSImportMethods jsMethods)
     {
-#pragma warning disable CS0618 // Type or member is obsolete
-        var componentsCount = jsRuntime.InvokeUnmarshalled<int>(RegisteredComponentsInterop.GetRegisteredComponentsCount);
+        var componentsCount = jsMethods.RegisteredComponents_GetRegisteredComponentsCount();
         if (componentsCount == 0)
         {
             return;
@@ -101,14 +101,13 @@ public sealed class WebAssemblyHostBuilder
         var registeredComponents = new WebAssemblyComponentMarker[componentsCount];
         for (var i = 0; i < componentsCount; i++)
         {
-            var id = jsRuntime.InvokeUnmarshalled<int, int>(RegisteredComponentsInterop.GetId, i);
-            var assembly = jsRuntime.InvokeUnmarshalled<int, string>(RegisteredComponentsInterop.GetAssembly, id);
-            var typeName = jsRuntime.InvokeUnmarshalled<int, string>(RegisteredComponentsInterop.GetTypeName, id);
-            var serializedParameterDefinitions = jsRuntime.InvokeUnmarshalled<int, object?, object?, string>(RegisteredComponentsInterop.GetParameterDefinitions, id, null, null);
-            var serializedParameterValues = jsRuntime.InvokeUnmarshalled<int, object?, object?, string>(RegisteredComponentsInterop.GetParameterValues, id, null, null);
+            var id = jsMethods.RegisteredComponents_GetId(i);
+            var assembly = jsMethods.RegisteredComponents_GetAssembly(id);
+            var typeName = jsMethods.RegisteredComponents_GetTypeName(id);
+            var serializedParameterDefinitions = jsMethods.RegisteredComponents_GetParameterDefinitions(id);
+            var serializedParameterValues = jsMethods.RegisteredComponents_GetParameterValues(id);
             registeredComponents[i] = new WebAssemblyComponentMarker(WebAssemblyComponentMarker.ClientMarkerType, assembly, typeName, serializedParameterDefinitions, serializedParameterValues, id.ToString(CultureInfo.InvariantCulture));
         }
-#pragma warning restore CS0618 // Type or member is obsolete
 
         var componentDeserializer = WebAssemblyComponentParameterDeserializer.Instance;
         foreach (var registeredComponent in registeredComponents)
@@ -130,45 +129,35 @@ public sealed class WebAssemblyHostBuilder
         }
     }
 
-    private void InitializePersistedState(IJSUnmarshalledRuntime jsRuntime)
+    private void InitializePersistedState(IInternalJSImportMethods jsMethods)
     {
-#pragma warning disable CS0618 // Type or member is obsolete
-        _persistedState = jsRuntime.InvokeUnmarshalled<string>("Blazor._internal.getPersistedState");
-#pragma warning restore CS0618 // Type or member is obsolete
+        _persistedState = jsMethods.GetPersistedState();
     }
 
-    private static void InitializeNavigationManager(IJSUnmarshalledRuntime jsRuntime)
+    private static void InitializeNavigationManager(IInternalJSImportMethods jsMethods)
     {
-#pragma warning disable CS0618 // Type or member is obsolete
-        var baseUri = jsRuntime.InvokeUnmarshalled<string>(BrowserNavigationManagerInterop.GetBaseUri);
-        var uri = jsRuntime.InvokeUnmarshalled<string>(BrowserNavigationManagerInterop.GetLocationHref);
-#pragma warning restore CS0618 // Type or member is obsolete
+        var baseUri = jsMethods.NavigationManager_GetBaseUri();
+        var uri = jsMethods.NavigationManager_GetLocationHref();
 
         WebAssemblyNavigationManager.Instance = new WebAssemblyNavigationManager(baseUri, uri);
     }
 
-    private WebAssemblyHostEnvironment InitializeEnvironment(IJSUnmarshalledRuntime jsRuntime)
+    private WebAssemblyHostEnvironment InitializeEnvironment(IInternalJSImportMethods jsMethods)
     {
-#pragma warning disable CS0618 // Type or member is obsolete
-        var applicationEnvironment = jsRuntime.InvokeUnmarshalled<string>("Blazor._internal.getApplicationEnvironment");
-#pragma warning restore CS0618 // Type or member is obsolete
+        var applicationEnvironment = jsMethods.GetApplicationEnvironment();
         var hostEnvironment = new WebAssemblyHostEnvironment(applicationEnvironment, WebAssemblyNavigationManager.Instance.BaseUri);
 
         Services.AddSingleton<IWebAssemblyHostEnvironment>(hostEnvironment);
 
         var configFiles = new[]
         {
-                "appsettings.json",
-                $"appsettings.{applicationEnvironment}.json"
-            };
+            "appsettings.json",
+            $"appsettings.{applicationEnvironment}.json"
+        };
 
         foreach (var configFile in configFiles)
         {
-#pragma warning disable CS0618 // Type or member is obsolete
-            var appSettingsJson = jsRuntime.InvokeUnmarshalled<string, byte[]>(
-                "Blazor._internal.getConfig", configFile);
-#pragma warning restore CS0618 // Type or member is obsolete
-
+            var appSettingsJson = jsMethods.GetConfig(configFile);
             if (appSettingsJson != null)
             {
                 // Perf: Using this over AddJsonStream. This allows the linker to trim out the "File"-specific APIs and assemblies

+ 12 - 9
src/Components/WebAssembly/WebAssembly/src/HotReload/WebAssemblyHotReload.cs

@@ -5,7 +5,7 @@ using System.ComponentModel;
 using System.Diagnostics;
 using System.Globalization;
 using System.Reflection;
-using Microsoft.AspNetCore.Components.WebAssembly.Services;
+using System.Runtime.InteropServices.JavaScript;
 using Microsoft.Extensions.HotReload;
 using Microsoft.JSInterop;
 
@@ -16,8 +16,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.HotReload;
 /// code.
 /// </summary>
 [EditorBrowsable(EditorBrowsableState.Never)]
-public static class WebAssemblyHotReload
+public static partial class WebAssemblyHotReload
 {
+    private const string BlazorHotReloadModuleName = "blazor-hotreload";
+
     private static HotReloadAgent? _hotReloadAgent;
     private static readonly UpdateDelta[] _updateDeltas = new[]
     {
@@ -26,16 +28,14 @@ public static class WebAssemblyHotReload
 
     internal static async Task InitializeAsync()
     {
-        if (Environment.GetEnvironmentVariable("__ASPNETCORE_BROWSER_TOOLS") == "true")
+        if (Environment.GetEnvironmentVariable("__ASPNETCORE_BROWSER_TOOLS") == "true" &&
+            OperatingSystem.IsBrowser())
         {
             // Attempt to read previously applied hot reload deltas if the ASP.NET Core browser tools are available (indicated by the presence of the Environment variable).
-            // The agent is injected in to the hosted app and can serve this script that can provide results from local-storage .
+            // The agent is injected in to the hosted app and can serve this script that can provide results from local-storage.
             // See https://github.com/dotnet/aspnetcore/issues/37357#issuecomment-941237000
-
-            var jsObjectReference = (IJSUnmarshalledObjectReference)(await DefaultWebAssemblyJSRuntime.Instance.InvokeAsync<IJSObjectReference>("import", "/_framework/blazor-hotreload.js"));
-#pragma warning disable CS0618 // Type or member is obsolete
-            await jsObjectReference.InvokeUnmarshalled<Task<int>>("receiveHotReload");
-#pragma warning restore CS0618 // Type or member is obsolete
+            await JSHost.ImportAsync(BlazorHotReloadModuleName, "/_framework/blazor-hotreload.js");
+            ReceiveHotReload();
         }
     }
 
@@ -73,4 +73,7 @@ public static class WebAssemblyHotReload
         }
         return (string)method.Invoke(obj: null, parameters: null)!;
     }
+
+    [JSImport("receiveHotReload", BlazorHotReloadModuleName)]
+    private static partial void ReceiveHotReload();
 }

+ 18 - 7
src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs

@@ -2,6 +2,8 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices.JavaScript;
 using Microsoft.AspNetCore.Components.RenderTree;
 using Microsoft.AspNetCore.Components.Web.Infrastructure;
 using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
@@ -81,14 +83,20 @@ internal sealed partial class WebAssemblyRenderer : WebRenderer
     private void CallBaseProcessPendingRender() => base.ProcessPendingRender();
 
     /// <inheritdoc />
-    protected override Task UpdateDisplayAsync(in RenderBatch batch)
+    protected override unsafe Task UpdateDisplayAsync(in RenderBatch batch)
     {
-#pragma warning disable CS0618 // Type or member is obsolete
-        DefaultWebAssemblyJSRuntime.Instance.InvokeUnmarshalled<int, RenderBatch, object>(
-            "Blazor._internal.renderBatch",
-            RendererId,
-            batch);
-#pragma warning restore CS0618 // Type or member is obsolete
+        // This is a GC hazard - it would be ideal to pin 'batch' and all its contents to prevent
+        // it from getting moved, or pause the GC for the duration of the 'RenderBatch()' call.
+        // The key mitigation is that the JS-side code always processes renderbatches synchronously
+        // and never calls back into .NET during that process, so GC cannot run (assuming it would
+        // only run on the current thread).
+        // As an early-warning system in case we accidentally introduce bugs and violate that rule,
+        // or for edge cases where user code can be invoked during rendering (e.g., DOM mutation
+        // observers) we further enforce it on the JS side using a notion of "locking the heap"
+        // during rendering, which prevents any JS-to-.NET calls that go through Blazor APIs such
+        // as DotNet.invokeMethod or event handlers.
+        var batchCopy = batch;
+        RenderBatch(RendererId, Unsafe.AsPointer(ref batchCopy));
 
         if (WebAssemblyCallQueue.HasUnstartedWork)
         {
@@ -129,4 +137,7 @@ internal sealed partial class WebAssemblyRenderer : WebRenderer
         [LoggerMessage(100, LogLevel.Critical, "Unhandled exception rendering component: {Message}", EventName = "ExceptionRenderingComponent")]
         public static partial void UnhandledExceptionRenderingComponent(ILogger logger, string message, Exception exception);
     }
+
+    [JSImport("Blazor._internal.renderBatch", "blazor-internal")]
+    private static unsafe partial void RenderBatch(int id, void* batch);
 }

+ 20 - 22
src/Components/WebAssembly/WebAssembly/src/Services/DefaultWebAssemblyJSRuntime.cs

@@ -3,6 +3,8 @@
 
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
+using System.Runtime.InteropServices.JavaScript;
+using System.Runtime.Versioning;
 using System.Text.Json;
 using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
 using Microsoft.JSInterop;
@@ -11,7 +13,7 @@ using Microsoft.JSInterop.WebAssembly;
 
 namespace Microsoft.AspNetCore.Components.WebAssembly.Services;
 
-internal sealed class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime
+internal sealed partial class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime
 {
     internal static readonly DefaultWebAssemblyJSRuntime Instance = new();
 
@@ -20,7 +22,7 @@ internal sealed class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime
     [DynamicDependency(nameof(InvokeDotNet))]
     [DynamicDependency(nameof(EndInvokeJS))]
     [DynamicDependency(nameof(BeginInvokeDotNet))]
-    [DynamicDependency(nameof(NotifyByteArrayAvailable))]
+    [DynamicDependency(nameof(ReceiveByteArrayFromJS))]
     private DefaultWebAssemblyJSRuntime()
     {
         ElementReferenceContext = new WebElementReferenceContext(this);
@@ -29,14 +31,20 @@ internal sealed class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime
 
     public JsonSerializerOptions ReadJsonSerializerOptions() => JsonSerializerOptions;
 
-    // The following methods are invoke via Mono's JS interop mechanism (invoke_method)
-    public static string? InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson)
+    [JSExport]
+    [SupportedOSPlatform("browser")]
+    public static string? InvokeDotNet(
+        string? assemblyName,
+        string methodIdentifier,
+        [JSMarshalAs<JSType.Number>] long dotNetObjectId,
+        string argsJson)
     {
-        var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId, CultureInfo.InvariantCulture), callId: null);
+        var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId, callId: null);
         return DotNetDispatcher.Invoke(Instance, callInfo, argsJson);
     }
 
-    // Invoked via Mono's JS interop mechanism (invoke_method)
+    [JSExport]
+    [SupportedOSPlatform("browser")]
     public static void EndInvokeJS(string argsJson)
     {
         WebAssemblyCallQueue.Schedule(argsJson, static argsJson =>
@@ -47,8 +55,9 @@ internal sealed class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime
         });
     }
 
-    // Invoked via Mono's JS interop mechanism (invoke_method)
-    public static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson)
+    [JSExport]
+    [SupportedOSPlatform("browser")]
+    public static void BeginInvokeDotNet(string? callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson)
     {
         // Figure out whether 'assemblyNameOrDotNetObjectId' is the assembly name or the instance ID
         // We only need one for any given call. This helps to work around the limitation that we can
@@ -75,21 +84,10 @@ internal sealed class DefaultWebAssemblyJSRuntime : WebAssemblyJSRuntime
         });
     }
 
-    /// <summary>
-    /// Invoked via Mono's JS interop mechanism (invoke_method)
-    ///
-    /// Notifies .NET of an array that's available for transfer from JS to .NET
-    ///
-    /// Ideally that byte array would be transferred directly as a parameter on this
-    /// call, however that's not currently possible due to: <see href="https://github.com/dotnet/runtime/issues/53378"/>.
-    /// </summary>
-    /// <param name="id">Id of the byte array</param>
-    public static void NotifyByteArrayAvailable(int id)
+    [JSExport]
+    [SupportedOSPlatform("browser")]
+    private static void ReceiveByteArrayFromJS(int id, byte[] data)
     {
-#pragma warning disable CS0618 // Type or member is obsolete
-        var data = Instance.InvokeUnmarshalled<byte[]>("Blazor._internal.retrieveByteArray");
-#pragma warning restore CS0618 // Type or member is obsolete
-
         DotNetDispatcher.ReceiveByteArray(Instance, id, data);
     }
 

+ 33 - 0
src/Components/WebAssembly/WebAssembly/src/Services/IInternalJSImportMethods.cs

@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Components.WebAssembly.Services;
+
+internal interface IInternalJSImportMethods
+{
+    string GetPersistedState();
+
+    string GetApplicationEnvironment();
+
+    byte[]? GetConfig(string configFile);
+
+    void NavigationManager_EnableNavigationInterception();
+
+    string NavigationManager_GetLocationHref();
+
+    string NavigationManager_GetBaseUri();
+
+    void NavigationManager_SetHasLocationChangingListeners(bool value);
+
+    int RegisteredComponents_GetRegisteredComponentsCount();
+
+    int RegisteredComponents_GetId(int index);
+
+    string RegisteredComponents_GetAssembly(int id);
+
+    string RegisteredComponents_GetTypeName(int id);
+
+    string RegisteredComponents_GetParameterDefinitions(int id);
+
+    string RegisteredComponents_GetParameterValues(int id);
+}

+ 93 - 0
src/Components/WebAssembly/WebAssembly/src/Services/InternalJSImportMethods.cs

@@ -0,0 +1,93 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices.JavaScript;
+using Microsoft.AspNetCore.Components.Web;
+using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+
+namespace Microsoft.AspNetCore.Components.WebAssembly.Services;
+
+internal partial class InternalJSImportMethods : IInternalJSImportMethods
+{
+    public static readonly InternalJSImportMethods Instance = new();
+
+    private InternalJSImportMethods() { }
+
+    public string GetPersistedState()
+        => GetPersistedStateCore();
+
+    public string GetApplicationEnvironment()
+        => GetApplicationEnvironmentCore();
+
+    public byte[]? GetConfig(string configFile)
+        => GetConfigCore(configFile);
+
+    public void NavigationManager_EnableNavigationInterception()
+        => NavigationManager_EnableNavigationInterceptionCore();
+
+    public string NavigationManager_GetLocationHref()
+        => NavigationManager_GetLocationHrefCore();
+
+    public string NavigationManager_GetBaseUri()
+        => NavigationManager_GetBaseUriCore();
+
+    public void NavigationManager_SetHasLocationChangingListeners(bool value)
+        => NavigationManager_SetHasLocationChangingListenersCore(value);
+
+    public int RegisteredComponents_GetRegisteredComponentsCount()
+        => RegisteredComponents_GetRegisteredComponentsCountCore();
+
+    public int RegisteredComponents_GetId(int index)
+        => RegisteredComponents_GetIdCore(index);
+
+    public string RegisteredComponents_GetAssembly(int id)
+        => RegisteredComponents_GetAssemblyCore(id);
+
+    public string RegisteredComponents_GetTypeName(int id)
+        => RegisteredComponents_GetTypeNameCore(id);
+
+    public string RegisteredComponents_GetParameterDefinitions(int id)
+        => RegisteredComponents_GetParameterDefinitionsCore(id);
+
+    public string RegisteredComponents_GetParameterValues(int id)
+        => RegisteredComponents_GetParameterValuesCore(id);
+
+    [JSImport("Blazor._internal.getPersistedState", "blazor-internal")]
+    private static partial string GetPersistedStateCore();
+
+    [JSImport("Blazor._internal.getApplicationEnvironment", "blazor-internal")]
+    private static partial string GetApplicationEnvironmentCore();
+
+    [JSImport("Blazor._internal.getConfig", "blazor-internal")]
+    private static partial byte[] GetConfigCore(string configFile);
+
+    [JSImport(BrowserNavigationManagerInterop.EnableNavigationInterception, "blazor-internal")]
+    private static partial void NavigationManager_EnableNavigationInterceptionCore();
+
+    [JSImport(BrowserNavigationManagerInterop.GetLocationHref, "blazor-internal")]
+    private static partial string NavigationManager_GetLocationHrefCore();
+
+    [JSImport(BrowserNavigationManagerInterop.GetBaseUri, "blazor-internal")]
+    private static partial string NavigationManager_GetBaseUriCore();
+
+    [JSImport(BrowserNavigationManagerInterop.SetHasLocationChangingListeners, "blazor-internal")]
+    private static partial void NavigationManager_SetHasLocationChangingListenersCore(bool value);
+
+    [JSImport(RegisteredComponentsInterop.GetRegisteredComponentsCount, "blazor-internal")]
+    private static partial int RegisteredComponents_GetRegisteredComponentsCountCore();
+
+    [JSImport(RegisteredComponentsInterop.GetId, "blazor-internal")]
+    private static partial int RegisteredComponents_GetIdCore(int index);
+
+    [JSImport(RegisteredComponentsInterop.GetAssembly, "blazor-internal")]
+    private static partial string RegisteredComponents_GetAssemblyCore(int id);
+
+    [JSImport(RegisteredComponentsInterop.GetTypeName, "blazor-internal")]
+    private static partial string RegisteredComponents_GetTypeNameCore(int id);
+
+    [JSImport(RegisteredComponentsInterop.GetParameterDefinitions, "blazor-internal")]
+    private static partial string RegisteredComponents_GetParameterDefinitionsCore(int id);
+
+    [JSImport(RegisteredComponentsInterop.GetParameterValues, "blazor-internal")]
+    private static partial string RegisteredComponents_GetParameterValuesCore(int id);
+}

+ 1 - 3
src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationInterception.cs

@@ -2,8 +2,6 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using Microsoft.AspNetCore.Components.Routing;
-using Microsoft.JSInterop;
-using Interop = Microsoft.AspNetCore.Components.Web.BrowserNavigationManagerInterop;
 
 namespace Microsoft.AspNetCore.Components.WebAssembly.Services;
 
@@ -13,7 +11,7 @@ internal sealed class WebAssemblyNavigationInterception : INavigationInterceptio
 
     public Task EnableNavigationInterceptionAsync()
     {
-        DefaultWebAssemblyJSRuntime.Instance.InvokeVoid(Interop.EnableNavigationInterception);
+        InternalJSImportMethods.Instance.NavigationManager_EnableNavigationInterception();
         return Task.CompletedTask;
     }
 }

+ 1 - 1
src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs

@@ -85,7 +85,7 @@ internal sealed partial class WebAssemblyNavigationManager : NavigationManager
     }
 
     protected override void SetNavigationLockState(bool value)
-        => DefaultWebAssemblyJSRuntime.Instance.InvokeVoid(Interop.SetHasLocationChangingListeners, value);
+        => InternalJSImportMethods.Instance.NavigationManager_SetHasLocationChangingListeners(value);
 
     private static partial class Log
     {

+ 1 - 1
src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyCultureProviderTest.cs

@@ -32,7 +32,7 @@ public class WebAssemblyCultureProviderTest
         try
         {
             // WebAssembly is initialized with en-US
-            var cultureProvider = new WebAssemblyCultureProvider(DefaultWebAssemblyJSRuntime.Instance, new CultureInfo("en-US"), new CultureInfo("en-US"));
+            var cultureProvider = new WebAssemblyCultureProvider(new CultureInfo("en-US"), new CultureInfo("en-US"));
 
             // Culture is changed to fr-FR as part of the app
             using var cultureReplacer = new CultureReplacer("fr-FR");

+ 13 - 13
src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs

@@ -20,12 +20,12 @@ public class WebAssemblyHostBuilderTest
     public void Build_AllowsConfiguringConfiguration()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
 
         builder.Configuration.AddInMemoryCollection(new[]
         {
-                new KeyValuePair<string, string>("key", "value"),
-            });
+            new KeyValuePair<string, string>("key", "value"),
+        });
 
         // Act
         var host = builder.Build();
@@ -38,7 +38,7 @@ public class WebAssemblyHostBuilderTest
     public void Build_AllowsConfiguringServices()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
 
         // This test also verifies that we create a scope.
         builder.Services.AddScoped<StringBuilder>();
@@ -54,7 +54,7 @@ public class WebAssemblyHostBuilderTest
     public void Build_AllowsConfiguringContainer()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
 
         builder.Services.AddScoped<StringBuilder>();
         var factory = new MyFakeServiceProviderFactory();
@@ -72,7 +72,7 @@ public class WebAssemblyHostBuilderTest
     public void Build_AllowsConfiguringContainer_WithDelegate()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
 
         builder.Services.AddScoped<StringBuilder>();
 
@@ -95,7 +95,7 @@ public class WebAssemblyHostBuilderTest
     public void Build_InDevelopment_ConfiguresWithServiceProviderWithScopeValidation()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(environment: "Development"), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development"), JsonOptions);
 
         builder.Services.AddScoped<StringBuilder>();
         builder.Services.AddSingleton<TestServiceThatTakesStringBuilder>();
@@ -112,7 +112,7 @@ public class WebAssemblyHostBuilderTest
     public void Build_InProduction_ConfiguresWithServiceProviderWithScopeValidation()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
 
         builder.Services.AddScoped<StringBuilder>();
         builder.Services.AddSingleton<TestServiceThatTakesStringBuilder>();
@@ -129,7 +129,7 @@ public class WebAssemblyHostBuilderTest
     public void Builder_InDevelopment_SetsHostEnvironmentProperty()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(environment: "Development"), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development"), JsonOptions);
 
         // Assert
         Assert.NotNull(builder.HostEnvironment);
@@ -140,7 +140,7 @@ public class WebAssemblyHostBuilderTest
     public void Builder_CreatesNavigationManager()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(environment: "Development"), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(environment: "Development"), JsonOptions);
 
         // Act
         var host = builder.Build();
@@ -190,7 +190,7 @@ public class WebAssemblyHostBuilderTest
     public void Build_AddsConfigurationToServices()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
 
         builder.Configuration.AddInMemoryCollection(new[]
         {
@@ -225,7 +225,7 @@ public class WebAssemblyHostBuilderTest
     public void Constructor_AddsDefaultServices()
     {
         // Arrange & Act
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
 
         foreach (var type in DefaultServiceTypes)
         {
@@ -237,7 +237,7 @@ public class WebAssemblyHostBuilderTest
     public void Builder_SupportsConfiguringLogging()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         var provider = new Mock<ILoggerProvider>();
 
         // Act

+ 4 - 4
src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs

@@ -19,7 +19,7 @@ public class WebAssemblyHostTest
     public async Task RunAsync_CanExitBasedOnCancellationToken()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         var host = builder.Build();
         var cultureProvider = new TestSatelliteResourcesLoader();
 
@@ -38,7 +38,7 @@ public class WebAssemblyHostTest
     public async Task RunAsync_CallingTwiceCausesException()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         var host = builder.Build();
         var cultureProvider = new TestSatelliteResourcesLoader();
 
@@ -59,7 +59,7 @@ public class WebAssemblyHostTest
     public async Task DisposeAsync_CanDisposeAfterCallingRunAsync()
     {
         // Arrange
-        var builder = new WebAssemblyHostBuilder(new TestJSUnmarshalledRuntime(), JsonOptions);
+        var builder = new WebAssemblyHostBuilder(new TestInternalJSImportMethods(), JsonOptions);
         builder.Services.AddSingleton<DisposableService>();
         var host = builder.Build();
         var cultureProvider = new TestSatelliteResourcesLoader();
@@ -95,7 +95,7 @@ public class WebAssemblyHostTest
     private class TestSatelliteResourcesLoader : WebAssemblyCultureProvider
     {
         internal TestSatelliteResourcesLoader()
-            : base(DefaultWebAssemblyJSRuntime.Instance, CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture)
+            : base(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture)
         {
         }
 

+ 53 - 0
src/Components/WebAssembly/WebAssembly/test/TestInternalJSImportMethods.cs

@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Components.WebAssembly.Services;
+
+namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+
+internal sealed class TestInternalJSImportMethods : IInternalJSImportMethods
+{
+    private readonly string _environment;
+
+    public TestInternalJSImportMethods(string environment = "Production")
+    {
+        _environment = environment;
+    }
+
+    public string GetApplicationEnvironment()
+        => _environment;
+
+    public byte[] GetConfig(string configFile)
+        => null;
+
+    public string GetPersistedState()
+        => null;
+
+    public void NavigationManager_EnableNavigationInterception() { }
+
+    public string NavigationManager_GetBaseUri()
+        => "https://www.example.com/awesome-part-that-will-be-truncated-in-tests";
+
+    public string NavigationManager_GetLocationHref()
+        => "https://www.example.com/awesome-part-that-will-be-truncated-in-tests/cool";
+
+    public void NavigationManager_SetHasLocationChangingListeners(bool value) { }
+
+    public string RegisteredComponents_GetAssembly(int id)
+        => string.Empty;
+
+    public int RegisteredComponents_GetId(int index)
+        => 0;
+
+    public string RegisteredComponents_GetParameterDefinitions(int id)
+        => string.Empty;
+
+    public string RegisteredComponents_GetParameterValues(int id)
+        => string.Empty;
+
+    public int RegisteredComponents_GetRegisteredComponentsCount()
+        => 0;
+
+    public string RegisteredComponents_GetTypeName(int id)
+        => string.Empty;
+}

+ 0 - 48
src/Components/WebAssembly/WebAssembly/test/TestJSUnmarshalledRuntime.cs

@@ -1,48 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Microsoft.JSInterop;
-
-namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;
-
-internal class TestJSUnmarshalledRuntime : IJSUnmarshalledRuntime
-{
-    private readonly string _environment;
-
-    public TestJSUnmarshalledRuntime(string environment = "Production")
-    {
-        _environment = environment;
-    }
-
-    public TResult InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2)
-    {
-        switch (identifier)
-        {
-            case "Blazor._internal.getApplicationEnvironment":
-                return (TResult)(object)_environment;
-            case "Blazor._internal.getConfig":
-                return (TResult)(object)null;
-            case "Blazor._internal.navigationManager.getUnmarshalledBaseURI":
-                var testUri = "https://www.example.com/awesome-part-that-will-be-truncated-in-tests";
-                return (TResult)(object)testUri;
-            case "Blazor._internal.navigationManager.getUnmarshalledLocationHref":
-                var testHref = "https://www.example.com/awesome-part-that-will-be-truncated-in-tests/cool";
-                return (TResult)(object)testHref;
-            case "Blazor._internal.registeredComponents.getRegisteredComponentsCount":
-                return (TResult)(object)0;
-            case "Blazor._internal.getPersistedState":
-                return (TResult)(object)null;
-            default:
-                throw new NotImplementedException($"{nameof(TestJSUnmarshalledRuntime)} has no implementation for '{identifier}'.");
-        }
-    }
-
-    public TResult InvokeUnmarshalled<TResult>(string identifier)
-        => InvokeUnmarshalled<object, object, object, TResult>(identifier, null, null, null);
-
-    public TResult InvokeUnmarshalled<T0, TResult>(string identifier, T0 arg0)
-        => InvokeUnmarshalled<T0, object, object, TResult>(identifier, arg0, null, null);
-
-    public TResult InvokeUnmarshalled<T0, T1, TResult>(string identifier, T0 arg0, T1 arg1)
-        => InvokeUnmarshalled<T0, T1, object, TResult>(identifier, arg0, arg1, null);
-}

+ 3 - 3
src/Components/test/E2ETest/Tests/InteropTest.cs

@@ -114,9 +114,9 @@ public class InteropTest : ServerTestBase<ToggleExecutionModeServerFixture<Progr
             ["result8"] = @"[{""id"":7,""isValid"":false,""data"":{""source"":""Some random text with at least 7 characters"",""start"":7,""length"":7}},7,123,28,56,7.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5]]",
             ["result9"] = @"[{""id"":8,""isValid"":true,""data"":{""source"":""Some random text with at least 8 characters"",""start"":8,""length"":8}},8,123,32,64,8.25,[0.5,1.5,2.5,3.5,4.5,5.5,6.5,7.5],{""source"":""Some random text with at least 7 characters"",""start"":9,""length"":9}]",
             ["roundTripJSObjectReference"] = @"""successful""",
-            ["invokeDisposedJSObjectReferenceException"] = @"""JS object instance with ID",
-            ["ThrowException"] = @"""System.InvalidOperationException: Threw an exception!",
-            ["ExceptionFromSyncMethod"] = "Function threw an exception!",
+            ["invokeDisposedJSObjectReferenceException"] = @"""Error: JS object instance with ID",
+            ["ThrowException"] = @"""Threw an exception!",
+            ["ExceptionFromSyncMethod"] = "Error: Function threw an exception!",
             ["roundTripByteArrayFromJS"] = @"{""0"":1,""1"":5,""2"":7,""3"":17,""4"":200,""5"":138}",
             ["roundTripByteArrayWrapperObjectFromJS"] = @"{""strVal"":""Some string"",""byteArrayVal"":{""0"":1,""1"":5,""2"":7,""3"":17,""4"":200,""5"":138},""intVal"":42}",
             ["roundTripByteArrayFromDotNet"] = @"1,5,7,15,35,200",

+ 6 - 2
src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices.JavaScript;
 using static Microsoft.AspNetCore.Internal.LinkerFlags;
 
 namespace Microsoft.JSInterop.Implementation;
@@ -9,7 +10,7 @@ namespace Microsoft.JSInterop.Implementation;
 /// <summary>
 /// Implements functionality for <see cref="IJSInProcessObjectReference"/>.
 /// </summary>
-public class JSInProcessObjectReference : JSObjectReference, IJSInProcessObjectReference
+public partial class JSInProcessObjectReference : JSObjectReference, IJSInProcessObjectReference
 {
     private readonly JSInProcessRuntime _jsRuntime;
 
@@ -39,7 +40,10 @@ public class JSInProcessObjectReference : JSObjectReference, IJSInProcessObjectR
         {
             Disposed = true;
 
-            _jsRuntime.InvokeVoid("DotNet.jsCallDispatcher.disposeJSObjectReferenceById", Id);
+            DisposeJSObjectReferenceById(Id);
         }
     }
+
+    [JSImport("DotNet.jsCallDispatcher.disposeJSObjectReferenceById", "blazor-internal")]
+    private static partial void DisposeJSObjectReferenceById([JSMarshalAs<JSType.Number>] long id);
 }

+ 1 - 0
src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj

@@ -9,6 +9,7 @@
     <Nullable>enable</Nullable>
     <IsTrimmable>true</IsTrimmable>
     <DefineConstants>$(DefineConstants);JS_INTEROP</DefineConstants>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <!-- TODO: Address Native AOT analyzer warnings https://github.com/dotnet/aspnetcore/issues/45473 -->
     <EnableAOTAnalyzer>false</EnableAOTAnalyzer>
   </PropertyGroup>