Follow-ups to lazy-load from API review (#24169)

* Follow-ups to lazy-load from API review

* Address feedback from peer review
This commit is contained in:
Safia Abdalla 2020-07-22 21:47:01 +00:00 committed by GitHub
parent b121a2ff6a
commit 4734f47ba5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 59 deletions

View File

@ -68,12 +68,12 @@ namespace Microsoft.AspNetCore.Components.Routing
/// <summary>
/// Get or sets the content to display when asynchronous navigation is in progress.
/// </summary>
[Parameter] public RenderFragment Navigating { get; set; }
[Parameter] public RenderFragment? Navigating { get; set; }
/// <summary>
/// Gets or sets a handler that should be called before navigating to a new page.
/// </summary>
[Parameter] public Func<NavigationContext, Task> OnNavigateAsync { get; set; }
[Parameter] public Func<NavigationContext, Task>? OnNavigateAsync { get; set; }
private RouteTable Routes { get; set; }
@ -195,10 +195,6 @@ namespace Microsoft.AspNetCore.Components.Routing
private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNavigate)
{
if (OnNavigateAsync == null)
{
return true;
}
// Cancel the CTS instead of disposing it, since disposing does not
// actually cancel and can cause unintended Object Disposed Exceptions.
@ -210,6 +206,11 @@ namespace Microsoft.AspNetCore.Components.Routing
// invocation.
await previousOnNavigate;
if (OnNavigateAsync == null)
{
return true;
}
_onNavigateCts = new CancellationTokenSource();
var navigateContext = new NavigationContext(path, _onNavigateCts.Token);
@ -227,14 +228,12 @@ namespace Microsoft.AspNetCore.Components.Routing
if (e.CancellationToken != navigateContext.CancellationToken)
{
var rethrownException = new InvalidOperationException("OnNavigateAsync can only be cancelled via NavigateContext.CancellationToken.", e);
_renderHandle.Render(builder => ExceptionDispatchInfo.Capture(rethrownException).Throw());
return false;
_renderHandle.Render(builder => ExceptionDispatchInfo.Throw(rethrownException));
}
}
catch (Exception e)
{
_renderHandle.Render(builder => ExceptionDispatchInfo.Capture(e).Throw());
return false;
_renderHandle.Render(builder => ExceptionDispatchInfo.Throw(e));
}
return false;

File diff suppressed because one or more lines are too long

View File

@ -324,9 +324,18 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
const assembliesToLoad = BINDING.mono_array_to_js_array<System_String, string>(assembliesToLoadDotNetArray);
const lazyAssemblies = resourceLoader.bootConfig.resources.lazyAssembly;
if (lazyAssemblies) {
const resourcePromises = Promise.all(assembliesToLoad
.filter(assembly => lazyAssemblies.hasOwnProperty(assembly))
if (!lazyAssemblies) {
throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly.");
}
var assembliesMarkedAsLazy = assembliesToLoad.filter(assembly => lazyAssemblies.hasOwnProperty(assembly));
if (assembliesMarkedAsLazy.length != assembliesToLoad.length) {
var notMarked = assembliesToLoad.filter(assembly => !assembliesMarkedAsLazy.includes(assembly));
throw new Error(`${notMarked.join()} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);
}
const resourcePromises = Promise.all(assembliesMarkedAsLazy
.map(assembly => resourceLoader.loadResource(assembly, `_framework/${assembly}`, lazyAssemblies[assembly], 'assembly'))
.map(async resource => (await resource.response).arrayBuffer()));
@ -345,8 +354,6 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
return resourcesToLoad.length;
}));
}
return BINDING.js_to_mono_obj(Promise.resolve(0));
}
});
module.postRun.push(() => {

View File

@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
/// <summary>
/// Gets the logging builder for configuring logging services.
/// </summary>
public ILoggingBuilder Logging { get; }
public ILoggingBuilder Logging { get; }
/// <summary>
/// Registers a <see cref="IServiceProviderFactory{TBuilder}" /> instance to be used to create the <see cref="IServiceProvider" />.
@ -189,7 +189,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
Services.AddSingleton<IJSRuntime>(DefaultWebAssemblyJSRuntime.Instance);
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
Services.AddSingleton(provider => new LazyAssemblyLoader(provider));
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
Services.AddLogging(builder => {
builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
});

View File

@ -9,7 +9,6 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Microsoft.JSInterop.WebAssembly;
@ -20,19 +19,18 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
///
/// Supports finding pre-loaded assemblies in a server or pre-rendering context.
/// </summary>
public class LazyAssemblyLoader
public sealed class LazyAssemblyLoader
{
internal const string GetDynamicAssemblies = "window.Blazor._internal.getLazyAssemblies";
internal const string ReadDynamicAssemblies = "window.Blazor._internal.readLazyAssemblies";
private List<Assembly> _loadedAssemblyCache = new List<Assembly>();
private readonly IJSRuntime _jsRuntime;
private readonly HashSet<string> _loadedAssemblyCache;
private readonly IServiceProvider _provider;
public LazyAssemblyLoader(IServiceProvider provider)
public LazyAssemblyLoader(IJSRuntime jsRuntime)
{
_provider = provider;
_loadedAssemblyCache = AppDomain.CurrentDomain.GetAssemblies().ToList();
_jsRuntime = jsRuntime;
_loadedAssemblyCache = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name + ".dll").ToHashSet();
}
/// <summary>
@ -55,37 +53,45 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
private Task<IEnumerable<Assembly>> LoadAssembliesInServerAsync(IEnumerable<string> assembliesToLoad)
{
var loadedAssemblies = _loadedAssemblyCache.Where(assembly =>
assembliesToLoad.Contains(assembly.GetName().Name + ".dll"));
var loadedAssemblies = new List<Assembly>();
if (loadedAssemblies.Count() != assembliesToLoad.Count())
try
{
var unloadedAssemblies = assembliesToLoad.Except(loadedAssemblies.Select(a => a.GetName().Name + ".dll"));
throw new InvalidOperationException($"Unable to find the following assemblies: {string.Join(",", unloadedAssemblies)}. Make sure that the appplication is referencing the assemblies and that they are present in the output folder.");
foreach (var assemblyName in assembliesToLoad)
{
loadedAssemblies.Add(Assembly.Load(Path.GetFileNameWithoutExtension(assemblyName)));
}
}
catch (FileNotFoundException ex)
{
throw new InvalidOperationException($"Unable to find the following assembly: {ex.FileName}. Make sure that the appplication is referencing the assemblies and that they are present in the output folder.");
}
return Task.FromResult(loadedAssemblies);
return Task.FromResult<IEnumerable<Assembly>>(loadedAssemblies);
}
private async Task<IEnumerable<Assembly>> LoadAssembliesInClientAsync(IEnumerable<string> assembliesToLoad)
{
var jsRuntime = _provider.GetRequiredService<IJSRuntime>();
// Only load assemblies that haven't already been lazily-loaded
var newAssembliesToLoad = assembliesToLoad.Except(_loadedAssemblyCache.Select(a => a.GetName().Name + ".dll"));
// Check to see if the assembly has already been loaded and avoids reloading it if so.
// Note: in the future, as an extra precuation, we can call `Assembly.Load` and check
// to see if it throws FileNotFound to ensure that an assembly hasn't been loaded
// between when the cache of loaded assemblies was instantiated in the constructor
// and the invocation of this method.
var newAssembliesToLoad = assembliesToLoad.Where(assembly => !_loadedAssemblyCache.Contains(assembly));
var loadedAssemblies = new List<Assembly>();
var count = (int)await ((WebAssemblyJSRuntime)jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
GetDynamicAssemblies,
assembliesToLoad.ToArray(),
null,
null);
var count = (int)await ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
GetDynamicAssemblies,
newAssembliesToLoad.ToArray(),
null,
null);
if (count == 0)
{
return loadedAssemblies;
}
var assemblies = ((WebAssemblyJSRuntime)jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
var assemblies = ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
ReadDynamicAssemblies,
null,
null,
@ -99,7 +105,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
// into the default app context.
var loadedAssembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(assembly));
loadedAssemblies.Add(loadedAssembly);
_loadedAssemblyCache.Add(loadedAssembly);
_loadedAssemblyCache.Add(loadedAssembly.GetName().Name + ".dll");
}
return loadedAssemblies;

View File

@ -111,6 +111,21 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.True(renderedElement.Displayed);
}
[Fact]
public void ThrowsErrorForUnavailableAssemblies()
{
// Navigate to a page with lazy loaded assemblies for the first time
SetUrlViaPushState("/Other");
var app = Browser.MountTestComponent<TestRouterWithLazyAssembly>();
// Should've thrown an error for unhandled error
var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10));
Assert.NotNull(errorUiElem);
AssertLogContainsCriticalMessages("DoesNotExist.dll must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.");
}
private string SetUrlViaPushState(string relativeUri)
{
var pathBaseWithoutHash = ServerPathBase.Split('#')[0];
@ -145,5 +160,18 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
});
}
}
void AssertLogContainsCriticalMessages(params string[] messages)
{
var log = Browser.Manage().Logs.GetLog(LogType.Browser);
foreach (var message in messages)
{
Assert.Contains(log, entry =>
{
return entry.Level == LogLevel.Severe
&& entry.Message.Contains(message);
});
}
}
}
}

View File

@ -1,6 +1,6 @@
@using Microsoft.AspNetCore.Components.Routing
@using System.Reflection
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@inject LazyAssemblyLoader lazyLoader
@ -31,25 +31,24 @@
private async Task LoadAssemblies(string uri)
{
try
if (uri.EndsWith("WithLazyAssembly"))
{
if (uri.EndsWith("WithLazyAssembly"))
{
Console.WriteLine($"Loading assemblies for WithLazyAssembly...");
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "Newtonsoft.Json.dll" });
lazyLoadedAssemblies.AddRange(assemblies);
}
if (uri.EndsWith("WithLazyLoadedRoutes"))
{
Console.WriteLine($"Loading assemblies for WithLazyLoadedRoutes...");
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "LazyTestContentPackage.dll" });
lazyLoadedAssemblies.AddRange(assemblies);
}
Console.WriteLine($"Loading assemblies for WithLazyAssembly...");
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "Newtonsoft.Json.dll" });
lazyLoadedAssemblies.AddRange(assemblies);
}
catch (Exception e)
if (uri.EndsWith("WithLazyLoadedRoutes"))
{
Console.WriteLine($"Error when loading assemblies: {e}");
Console.WriteLine($"Loading assemblies for WithLazyLoadedRoutes...");
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "LazyTestContentPackage.dll" });
lazyLoadedAssemblies.AddRange(assemblies);
}
if (uri.EndsWith("Other")) {
Console.WriteLine($"Loading assemblies for Other...");
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "DoesNotExist.dll" });
lazyLoadedAssemblies.AddRange(assemblies);
}
}
}