Do not attempt to load satellite assemblies until after MainAsync has executed (#21476)
WASM runtime does not like it when you attempt to load satellite assemblies for two sets of non-neutral cultures - the first is always loaded. This change defers loading satellite assemblies until after Program.Main has executed and the developer has configured the culture for the application. Fixes https://github.com/dotnet/aspnetcore/issues/21433
This commit is contained in:
parent
cc2b64ec5e
commit
9e412f4c7f
|
|
@ -15,19 +15,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
// In the future we may want Blazor.start to return something that exposes the possibly-async
|
||||
// entrypoint result to the JS caller. There's no requirement to do that today, and if we
|
||||
// do change this it will be non-breaking.
|
||||
public static void InvokeEntrypoint(string assemblyName, string[] args)
|
||||
=> InvokeEntrypoint(assemblyName, args, SatelliteResourcesLoader.Init());
|
||||
|
||||
internal static async void InvokeEntrypoint(string assemblyName, string[] args, SatelliteResourcesLoader satelliteResourcesLoader)
|
||||
public static async void InvokeEntrypoint(string assemblyName, string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Emscripten sets up the culture for the application based on the user's language preferences.
|
||||
// Before we execute the app's entry point, load satellite assemblies for this culture.
|
||||
// We'll allow users to configure their app culture as part of MainAsync. Loading satellite assemblies
|
||||
// for the configured culture will happen as part of WebAssemblyHost.RunAsync.
|
||||
await satelliteResourcesLoader.LoadCurrentCultureResourcesAsync();
|
||||
|
||||
var assembly = Assembly.Load(assemblyName);
|
||||
var entrypoint = FindUnderlyingEntrypoint(assembly);
|
||||
var @params = entrypoint.GetParameters().Length == 1 ? new object[] { args ?? Array.Empty<string>() } : new object[] { };
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -16,7 +15,6 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
internal const string ReadSatelliteAssemblies = "window.Blazor._internal.readSatelliteAssemblies";
|
||||
|
||||
private readonly WebAssemblyJSRuntimeInvoker _invoker;
|
||||
private CultureInfo _previousCulture;
|
||||
|
||||
// For unit testing.
|
||||
internal SatelliteResourcesLoader(WebAssemblyJSRuntimeInvoker invoker)
|
||||
|
|
@ -24,28 +22,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
_invoker = invoker;
|
||||
}
|
||||
|
||||
public static SatelliteResourcesLoader Instance { get; private set; }
|
||||
|
||||
public static SatelliteResourcesLoader Init()
|
||||
{
|
||||
Debug.Assert(Instance is null, "Init should not be called multiple times.");
|
||||
|
||||
Instance = new SatelliteResourcesLoader(WebAssemblyJSRuntimeInvoker.Instance);
|
||||
return Instance;
|
||||
}
|
||||
|
||||
public ValueTask LoadCurrentCultureResourcesAsync()
|
||||
{
|
||||
if (_previousCulture != CultureInfo.CurrentCulture)
|
||||
{
|
||||
_previousCulture = CultureInfo.CurrentCulture;
|
||||
return LoadSatelliteAssembliesForCurrentCultureAsync();
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
protected virtual async ValueTask LoadSatelliteAssembliesForCurrentCultureAsync()
|
||||
public virtual async ValueTask LoadCurrentCultureResourcesAsync()
|
||||
{
|
||||
var culturesToLoad = GetCultures(CultureInfo.CurrentCulture);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Rendering;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -58,7 +59,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
/// </summary>
|
||||
public IServiceProvider Services => _scope.ServiceProvider;
|
||||
|
||||
internal SatelliteResourcesLoader SatelliteResourcesLoader { get; set; } = SatelliteResourcesLoader.Instance;
|
||||
internal SatelliteResourcesLoader SatelliteResourcesLoader { get; set; } = new SatelliteResourcesLoader(WebAssemblyJSRuntimeInvoker.Instance);
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the host asynchronously.
|
||||
|
|
|
|||
|
|
@ -2,15 +2,12 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||
|
|
@ -36,7 +33,7 @@ static " + returnType + @" Main(" + paramsDecl + @")
|
|||
}", out var didMainExecute);
|
||||
|
||||
// Act
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { }, new TestSatelliteResourcesLoader());
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
|
||||
// Assert
|
||||
Assert.True(didMainExecute());
|
||||
|
|
@ -66,7 +63,7 @@ static async Task" + returnTypeGenericParam + @" Main(" + paramsDecl + @")
|
|||
// Act/Assert 1: Waits for task
|
||||
// The fact that we're not blocking here proves that we're not executing the
|
||||
// metadata-declared entrypoint, as that would block
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { }, new TestSatelliteResourcesLoader());
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
Assert.False(didMainExecute());
|
||||
|
||||
// Act/Assert 2: Continues
|
||||
|
|
@ -91,7 +88,7 @@ public static void Main()
|
|||
// to handle the exception. We can't assert about what it does here, because that
|
||||
// would involve capturing console output, which isn't safe in unit tests. Instead
|
||||
// we'll check this in E2E tests.
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { }, new TestSatelliteResourcesLoader());
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
Assert.True(didMainExecute());
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +107,7 @@ public static async Task Main()
|
|||
}", out var didMainExecute);
|
||||
|
||||
// Act/Assert 1: Waits for task
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { }, new TestSatelliteResourcesLoader());
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
Assert.False(didMainExecute());
|
||||
|
||||
// Act/Assert 2: Continues
|
||||
|
|
@ -152,15 +149,5 @@ namespace SomeApp
|
|||
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private class TestSatelliteResourcesLoader : SatelliteResourcesLoader
|
||||
{
|
||||
internal TestSatelliteResourcesLoader()
|
||||
: base(WebAssemblyJSRuntimeInvoker.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ValueTask LoadSatelliteAssembliesForCurrentCultureAsync() => default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,45 +70,5 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
// Assert
|
||||
invoker.Verify(i => i.InvokeUnmarshalled<object, object, object, object[]>(ReadSatelliteAssemblies, null, null, null), Times.Never());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadCurrentCultureResourcesAsync_AttemptsToReadAssemblies_IfCultureIsChangedBetweenInvocation()
|
||||
{
|
||||
// Arrange
|
||||
using var cultureReplacer = new CultureReplacer("en-GB");
|
||||
var invoker = new Mock<WebAssemblyJSRuntimeInvoker>();
|
||||
invoker.Setup(i => i.InvokeUnmarshalled<string[], object, object, Task<object>>(GetSatelliteAssemblies, It.IsAny<string[]>(), null, null))
|
||||
.Returns(Task.FromResult<object>(0))
|
||||
.Verifiable();
|
||||
|
||||
var loader = new SatelliteResourcesLoader(invoker.Object);
|
||||
|
||||
// Act
|
||||
await loader.LoadCurrentCultureResourcesAsync();
|
||||
CultureInfo.CurrentCulture = new CultureInfo("fr-fr");
|
||||
await loader.LoadCurrentCultureResourcesAsync();
|
||||
|
||||
invoker.Verify(i => i.InvokeUnmarshalled<object, object, object, Task<object>>(GetSatelliteAssemblies, It.IsAny<string[]>(), null, null),
|
||||
Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadCurrentCultureResourcesAsync_NoOps_WhenInvokedSecondTime_WithSameCulture()
|
||||
{
|
||||
// Arrange
|
||||
using var cultureReplacer = new CultureReplacer("en-GB");
|
||||
var invoker = new Mock<WebAssemblyJSRuntimeInvoker>();
|
||||
invoker.Setup(i => i.InvokeUnmarshalled<string[], object, object, Task<object>>(GetSatelliteAssemblies, It.IsAny<string[]>(), null, null))
|
||||
.Returns(Task.FromResult<object>(0));;
|
||||
|
||||
var loader = new SatelliteResourcesLoader(invoker.Object);
|
||||
|
||||
// Act
|
||||
await loader.LoadCurrentCultureResourcesAsync();
|
||||
await loader.LoadCurrentCultureResourcesAsync();
|
||||
|
||||
invoker.Verify(i => i.InvokeUnmarshalled<object, object, object, Task<object>>(GetSatelliteAssemblies, It.IsAny<string[]>(), null, null),
|
||||
Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
{
|
||||
}
|
||||
|
||||
protected override ValueTask LoadSatelliteAssembliesForCurrentCultureAsync() => default;
|
||||
public override ValueTask LoadCurrentCultureResourcesAsync() => default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue