From 9e412f4c7f0bf6f0e43db27a17bc938c36160f41 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 5 May 2020 09:06:42 -0700 Subject: [PATCH] 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 --- .../src/Hosting/EntrypointInvoker.cs | 11 +---- .../src/Hosting/SatelliteResourcesLoader.cs | 25 +----------- .../src/Hosting/WebAssemblyHost.cs | 3 +- .../test/Hosting/EntrypointInvokerTest.cs | 21 ++-------- .../Hosting/SatelliteResourcesLoaderTest.cs | 40 ------------------- .../test/Hosting/WebAssemblyHostTest.cs | 2 +- 6 files changed, 9 insertions(+), 93 deletions(-) diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/EntrypointInvoker.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/EntrypointInvoker.cs index 1fe153f17a..5fdea9e41b 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/EntrypointInvoker.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/EntrypointInvoker.cs @@ -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() } : new object[] { }; diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/SatelliteResourcesLoader.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/SatelliteResourcesLoader.cs index c4a5c33c68..79f780c584 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/SatelliteResourcesLoader.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/SatelliteResourcesLoader.cs @@ -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); diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs index 308d698c8f..c81dd2ac28 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs @@ -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 /// public IServiceProvider Services => _scope.ServiceProvider; - internal SatelliteResourcesLoader SatelliteResourcesLoader { get; set; } = SatelliteResourcesLoader.Instance; + internal SatelliteResourcesLoader SatelliteResourcesLoader { get; set; } = new SatelliteResourcesLoader(WebAssemblyJSRuntimeInvoker.Instance); /// /// Disposes the host asynchronously. diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/EntrypointInvokerTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/EntrypointInvokerTest.cs index aef12b3c7f..eafc042993 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/EntrypointInvokerTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/EntrypointInvokerTest.cs @@ -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; - } } } diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/SatelliteResourcesLoaderTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/SatelliteResourcesLoaderTest.cs index d541d404dc..c4bb704bb4 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/SatelliteResourcesLoaderTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/SatelliteResourcesLoaderTest.cs @@ -70,45 +70,5 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting // Assert invoker.Verify(i => i.InvokeUnmarshalled(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(); - invoker.Setup(i => i.InvokeUnmarshalled>(GetSatelliteAssemblies, It.IsAny(), null, null)) - .Returns(Task.FromResult(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>(GetSatelliteAssemblies, It.IsAny(), 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(); - invoker.Setup(i => i.InvokeUnmarshalled>(GetSatelliteAssemblies, It.IsAny(), null, null)) - .Returns(Task.FromResult(0));; - - var loader = new SatelliteResourcesLoader(invoker.Object); - - // Act - await loader.LoadCurrentCultureResourcesAsync(); - await loader.LoadCurrentCultureResourcesAsync(); - - invoker.Verify(i => i.InvokeUnmarshalled>(GetSatelliteAssemblies, It.IsAny(), null, null), - Times.Once()); - } } } diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs index c827250134..71c273c74c 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs @@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting { } - protected override ValueTask LoadSatelliteAssembliesForCurrentCultureAsync() => default; + public override ValueTask LoadCurrentCultureResourcesAsync() => default; } } }