Skip to content

Commit cec7fbf

Browse files
pavelsavaraMackinnonBuckSteveSandersonMS
authored
[blazor] Use JSImport for loading satellite assemblies (#46477)
* use new generated JavaScript interop with [JSImport] Co-authored-by: Mackinnon Buck <[email protected]> Co-authored-by: Steve Sanderson <[email protected]>
1 parent 0588658 commit cec7fbf

File tree

4 files changed

+46
-113
lines changed

4 files changed

+46
-113
lines changed

src/Components/Web.JS/src/GlobalExports.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,9 @@ interface IBlazor {
5353
renderBatch?: (browserRendererId: number, batchAddress: Pointer) => void,
5454
getConfig?: (dotNetFileName: System_String) => System_Object | undefined,
5555
getApplicationEnvironment?: () => System_String,
56-
readSatelliteAssemblies?: () => System_Array<System_Object>,
5756
dotNetCriticalError?: any
5857
loadLazyAssembly?: any,
59-
getSatelliteAssemblies?: any,
58+
loadSatelliteAssemblies?: any,
6059
sendJSDataStream?: (data: any, streamId: number, chunkSize: number) => void,
6160
getJSDataStreamChunk?: (data: any, position: number, chunkSize: number) => Promise<Uint8Array>,
6261
receiveDotNetDataStream?: (streamId: number, data: any, bytesRead: number, errorMessage: string) => void,

src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts

+19-29
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc
233233
(moduleConfig as any).preloadPlugins = [];
234234

235235
let resourcesLoaded = 0;
236-
function setProgress() {
236+
function setProgress(){
237237
resourcesLoaded++;
238238
const percentage = resourcesLoaded / totalResources.length * 100;
239239
document.documentElement.style.setProperty('--blazor-load-percentage', `${percentage}%`);
@@ -347,36 +347,26 @@ async function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourc
347347

348348
// Wire-up callbacks for satellite assemblies. Blazor will call these as part of the application
349349
// startup sequence to load satellite assemblies for the application's culture.
350-
Blazor._internal.getSatelliteAssemblies = (culturesToLoadDotNetArray) => {
351-
const culturesToLoad = BINDING.mono_array_to_js_array(culturesToLoadDotNetArray);
352-
const satelliteResources = resourceLoader.bootConfig.resources.satelliteResources;
353-
354-
if (satelliteResources) {
355-
const resourcePromises = Promise.all(culturesToLoad!
356-
.filter(culture => satelliteResources.hasOwnProperty(culture))
357-
.map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => `_framework/${fileName}`, 'assembly'))
358-
.reduce((previous, next) => previous.concat(next), new Array<LoadingResource>())
359-
.map(async resource => (await resource.response).arrayBuffer()));
360-
361-
return BINDING.js_to_mono_obj(resourcePromises.then(resourcesToLoad => {
362-
if (resourcesToLoad.length) {
363-
Blazor._internal.readSatelliteAssemblies = () => {
364-
const array = BINDING.mono_obj_array_new(resourcesToLoad.length);
365-
for (let i = 0; i < resourcesToLoad.length; i++) {
366-
BINDING.mono_obj_array_set(array, i, BINDING.js_typed_array_to_array(new Uint8Array(resourcesToLoad[i])));
367-
}
368-
return array as any;
369-
};
370-
}
371-
372-
return resourcesToLoad.length;
373-
}));
374-
}
375-
return BINDING.js_to_mono_obj(Promise.resolve(0));
376-
};
377-
350+
Blazor._internal.loadSatelliteAssemblies = loadSatelliteAssemblies;
378351
};
379352

353+
async function loadSatelliteAssemblies(culturesToLoad: string[], loader: (wrapper: { dll: Uint8Array }) => void): Promise<void> {
354+
const satelliteResources = resourceLoader.bootConfig.resources.satelliteResources;
355+
if (!satelliteResources) {
356+
return;
357+
}
358+
await Promise.all(culturesToLoad!
359+
.filter(culture => satelliteResources.hasOwnProperty(culture))
360+
.map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => `_framework/${fileName}`, 'assembly'))
361+
.reduce((previous, next) => previous.concat(next), new Array<LoadingResource>())
362+
.map(async resource => {
363+
const response = await resource.response;
364+
const bytes = await response.arrayBuffer();
365+
const wrapper = { dll: new Uint8Array(bytes) };
366+
loader(wrapper);
367+
}));
368+
}
369+
380370
async function loadLazyAssembly(assemblyNameToLoad: string): Promise<{ dll: Uint8Array, pdb: Uint8Array | null }> {
381371
const lazyAssemblies = resources.lazyAssembly;
382372
if (!lazyAssemblies) {

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyCultureProvider.cs

+26-31
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33

44
using System.Diagnostics.CodeAnalysis;
55
using System.Globalization;
6+
using System.Runtime.InteropServices.JavaScript;
67
using System.Runtime.Loader;
8+
using System.Runtime.Versioning;
79
using Microsoft.AspNetCore.Components.WebAssembly.Services;
810
using Microsoft.JSInterop;
911

1012
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;
1113

1214
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "This type loads resx files. We don't expect it's dependencies to be trimmed in the ordinary case.")]
1315
#pragma warning disable CA1852 // Seal internal types
14-
internal class WebAssemblyCultureProvider
16+
internal partial class WebAssemblyCultureProvider
1517
#pragma warning restore CA1852 // Seal internal types
1618
{
1719
internal const string GetSatelliteAssemblies = "window.Blazor._internal.getSatelliteAssemblies";
@@ -63,45 +65,31 @@ public void ThrowIfCultureChangeIsUnsupported()
6365

6466
public virtual async ValueTask LoadCurrentCultureResourcesAsync()
6567
{
66-
var culturesToLoad = GetCultures(CultureInfo.CurrentCulture);
67-
68-
if (culturesToLoad.Count == 0)
68+
if (!OperatingSystem.IsBrowser())
6969
{
70-
return;
70+
throw new PlatformNotSupportedException("This method is only supported in the browser.");
7171
}
7272

73-
// Now that we know the cultures we care about, let WebAssemblyResourceLoader (in JavaScript) load these
74-
// assemblies. We effectively want to resovle a Task<byte[][]> but there is no way to express this
75-
// using interop. We'll instead do this in two parts:
76-
// getSatelliteAssemblies resolves when all satellite assemblies to be loaded in .NET are fetched and available in memory.
77-
#pragma warning disable CS0618 // Type or member is obsolete
78-
var count = (int)await _invoker.InvokeUnmarshalled<string[], object?, object?, Task<object>>(
79-
GetSatelliteAssemblies,
80-
culturesToLoad.ToArray(),
81-
null,
82-
null);
83-
84-
if (count == 0)
73+
var culturesToLoad = GetCultures(CultureInfo.CurrentCulture);
74+
75+
if (culturesToLoad.Length == 0)
8576
{
8677
return;
8778
}
8879

89-
// readSatelliteAssemblies resolves the assembly bytes
90-
var assemblies = _invoker.InvokeUnmarshalled<object?, object?, object?, object[]>(
91-
ReadSatelliteAssemblies,
92-
null,
93-
null,
94-
null);
95-
#pragma warning restore CS0618 // Type or member is obsolete
80+
await WebAssemblyCultureProviderInterop.LoadSatelliteAssemblies(culturesToLoad, LoadSatelliteAssembly);
81+
}
9682

97-
for (var i = 0; i < assemblies.Length; i++)
98-
{
99-
using var stream = new MemoryStream((byte[])assemblies[i]);
100-
AssemblyLoadContext.Default.LoadFromStream(stream);
101-
}
83+
[SupportedOSPlatform("browser")]
84+
private void LoadSatelliteAssembly(JSObject wrapper)
85+
{
86+
var dllBytes = wrapper.GetPropertyAsByteArray("dll")!;
87+
using var stream = new MemoryStream(dllBytes);
88+
AssemblyLoadContext.Default.LoadFromStream(stream);
89+
wrapper.Dispose();
10290
}
10391

104-
internal static List<string> GetCultures(CultureInfo cultureInfo)
92+
internal static string[] GetCultures(CultureInfo cultureInfo)
10593
{
10694
var culturesToLoad = new List<string>();
10795

@@ -122,6 +110,13 @@ internal static List<string> GetCultures(CultureInfo cultureInfo)
122110
cultureInfo = cultureInfo.Parent;
123111
}
124112

125-
return culturesToLoad;
113+
return culturesToLoad.ToArray();
114+
}
115+
116+
private partial class WebAssemblyCultureProviderInterop
117+
{
118+
[JSImport("Blazor._internal.loadSatelliteAssemblies", "blazor-internal")]
119+
public static partial Task<JSObject> LoadSatelliteAssemblies(string[] culturesToLoad,
120+
[JSMarshalAs<JSType.Function<JSType.Object>>] Action<JSObject> assemblyLoader);
126121
}
127122
}

src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyCultureProviderTest.cs

-51
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
using System.Globalization;
55
using Microsoft.AspNetCore.Components.WebAssembly.Services;
66
using Microsoft.AspNetCore.Testing;
7-
using Microsoft.JSInterop;
8-
using Moq;
9-
using static Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyCultureProvider;
107

118
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting;
129

@@ -27,54 +24,6 @@ public void GetCultures_ReturnsCultureClosure(string cultureName, string[] expec
2724
Assert.Equal(expected, actual);
2825
}
2926

30-
[Fact]
31-
public async Task LoadCurrentCultureResourcesAsync_ReadsAssemblies()
32-
{
33-
// Arrange
34-
using var cultureReplacer = new CultureReplacer("en-GB");
35-
var invoker = new Mock<IJSUnmarshalledRuntime>();
36-
#pragma warning disable CS0618 // Type or member is obsolete
37-
invoker.Setup(i => i.InvokeUnmarshalled<string[], object, object, Task<object>>(GetSatelliteAssemblies, new[] { "en-GB", "en" }, null, null))
38-
.Returns(Task.FromResult<object>(1))
39-
.Verifiable();
40-
41-
invoker.Setup(i => i.InvokeUnmarshalled<object, object, object, object[]>(ReadSatelliteAssemblies, null, null, null))
42-
.Returns(new object[] { File.ReadAllBytes(GetType().Assembly.Location) })
43-
.Verifiable();
44-
#pragma warning restore CS0618 // Type or member is obsolete
45-
46-
var loader = new WebAssemblyCultureProvider(invoker.Object, CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture);
47-
48-
// Act
49-
await loader.LoadCurrentCultureResourcesAsync();
50-
51-
// Assert
52-
invoker.Verify();
53-
}
54-
55-
[Fact]
56-
public async Task LoadCurrentCultureResourcesAsync_DoesNotReadAssembliesWhenThereAreNone()
57-
{
58-
// Arrange
59-
using var cultureReplacer = new CultureReplacer("en-GB");
60-
var invoker = new Mock<IJSUnmarshalledRuntime>();
61-
#pragma warning disable CS0618 // Type or member is obsolete
62-
invoker.Setup(i => i.InvokeUnmarshalled<string[], object, object, Task<object>>(GetSatelliteAssemblies, new[] { "en-GB", "en" }, null, null))
63-
.Returns(Task.FromResult<object>(0))
64-
.Verifiable();
65-
#pragma warning restore CS0618 // Type or member is obsolete
66-
67-
var loader = new WebAssemblyCultureProvider(invoker.Object, CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture);
68-
69-
// Act
70-
await loader.LoadCurrentCultureResourcesAsync();
71-
72-
#pragma warning disable CS0618 // Type or member is obsolete
73-
// Assert
74-
invoker.Verify(i => i.InvokeUnmarshalled<object, object, object, object[]>(ReadSatelliteAssemblies, null, null, null), Times.Never());
75-
#pragma warning restore CS0618 // Type or member is obsolete
76-
}
77-
7827
[Fact]
7928
public void ThrowIfCultureChangeIsUnsupported_ThrowsIfCulturesAreDifferentAndICUShardingIsUsed()
8029
{

0 commit comments

Comments
 (0)