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:
Pranav K 2020-05-05 09:06:42 -07:00 committed by GitHub
parent cc2b64ec5e
commit 9e412f4c7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 9 additions and 93 deletions

View File

@ -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[] { };

View File

@ -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);

View File

@ -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.

View File

@ -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;
}
}
}

View File

@ -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());
}
}
}

View File

@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
{
}
protected override ValueTask LoadSatelliteAssembliesForCurrentCultureAsync() => default;
public override ValueTask LoadCurrentCultureResourcesAsync() => default;
}
}
}