Skip to content

Commit bff6766

Browse files
committed
Follow-ups to lazy-load from API review
1 parent c18571c commit bff6766

File tree

7 files changed

+99
-60
lines changed

7 files changed

+99
-60
lines changed

src/Components/Components/src/Routing/Router.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
6868
/// <summary>
6969
/// Get or sets the content to display when asynchronous navigation is in progress.
7070
/// </summary>
71-
[Parameter] public RenderFragment Navigating { get; set; }
71+
[Parameter] public RenderFragment? Navigating { get; set; }
7272

7373
/// <summary>
7474
/// Gets or sets a handler that should be called before navigating to a new page.
7575
/// </summary>
76-
[Parameter] public Func<NavigationContext, Task> OnNavigateAsync { get; set; }
76+
[Parameter] public Func<NavigationContext, Task>? OnNavigateAsync { get; set; }
7777

7878
private RouteTable Routes { get; set; }
7979

@@ -195,10 +195,6 @@ internal virtual void Refresh(bool isNavigationIntercepted)
195195

196196
private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNavigate)
197197
{
198-
if (OnNavigateAsync == null)
199-
{
200-
return true;
201-
}
202198

203199
// Cancel the CTS instead of disposing it, since disposing does not
204200
// actually cancel and can cause unintended Object Disposed Exceptions.
@@ -210,6 +206,11 @@ private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNav
210206
// invocation.
211207
await previousOnNavigate;
212208

209+
if (OnNavigateAsync == null)
210+
{
211+
return true;
212+
}
213+
213214
_onNavigateCts = new CancellationTokenSource();
214215
var navigateContext = new NavigationContext(path, _onNavigateCts.Token);
215216

@@ -227,14 +228,12 @@ private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNav
227228
if (e.CancellationToken != navigateContext.CancellationToken)
228229
{
229230
var rethrownException = new InvalidOperationException("OnNavigateAsync can only be cancelled via NavigateContext.CancellationToken.", e);
230-
_renderHandle.Render(builder => ExceptionDispatchInfo.Capture(rethrownException).Throw());
231-
return false;
231+
_renderHandle.Render(builder => ExceptionDispatchInfo.Throw(rethrownException));
232232
}
233233
}
234234
catch (Exception e)
235235
{
236-
_renderHandle.Render(builder => ExceptionDispatchInfo.Capture(e).Throw());
237-
return false;
236+
_renderHandle.Render(builder => ExceptionDispatchInfo.Throw(e));
238237
}
239238

240239
return false;

src/Components/Web.JS/dist/Release/blazor.webassembly.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -324,9 +324,18 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
324324
const assembliesToLoad = BINDING.mono_array_to_js_array<System_String, string>(assembliesToLoadDotNetArray);
325325
const lazyAssemblies = resourceLoader.bootConfig.resources.lazyAssembly;
326326

327-
if (lazyAssemblies) {
328-
const resourcePromises = Promise.all(assembliesToLoad
329-
.filter(assembly => lazyAssemblies.hasOwnProperty(assembly))
327+
if (!lazyAssemblies) {
328+
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.");
329+
}
330+
331+
var assembliesMarkedAsLazy = assembliesToLoad.filter(assembly => lazyAssemblies.hasOwnProperty(assembly));
332+
333+
if (assembliesMarkedAsLazy.length != assembliesToLoad.length) {
334+
var notMarked = assembliesToLoad.filter(assembly => !assembliesMarkedAsLazy.includes(assembly));
335+
throw new Error(`${notMarked.join()} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);
336+
}
337+
338+
const resourcePromises = Promise.all(assembliesMarkedAsLazy
330339
.map(assembly => resourceLoader.loadResource(assembly, `_framework/${assembly}`, lazyAssemblies[assembly], 'assembly'))
331340
.map(async resource => (await resource.response).arrayBuffer()));
332341

@@ -345,8 +354,6 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
345354
return resourcesToLoad.length;
346355
}));
347356
}
348-
return BINDING.js_to_mono_obj(Promise.resolve(0));
349-
}
350357
});
351358

352359
module.postRun.push(() => {

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ private WebAssemblyHostEnvironment InitializeEnvironment(WebAssemblyJSRuntimeInv
130130
/// <summary>
131131
/// Gets the logging builder for configuring logging services.
132132
/// </summary>
133-
public ILoggingBuilder Logging { get; }
133+
public ILoggingBuilder Logging { get; }
134134

135135
/// <summary>
136136
/// Registers a <see cref="IServiceProviderFactory{TBuilder}" /> instance to be used to create the <see cref="IServiceProvider" />.
@@ -189,7 +189,7 @@ internal void InitializeDefaultServices()
189189
Services.AddSingleton<IJSRuntime>(DefaultWebAssemblyJSRuntime.Instance);
190190
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
191191
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
192-
Services.AddSingleton(provider => new LazyAssemblyLoader(provider));
192+
Services.AddSingleton(new LazyAssemblyLoader(DefaultWebAssemblyJSRuntime.Instance));
193193
Services.AddLogging(builder => {
194194
builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
195195
});

src/Components/WebAssembly/WebAssembly/src/Services/LazyAssemblyLoader.cs

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using System.Runtime.InteropServices;
1010
using System.Runtime.Loader;
1111
using System.Threading.Tasks;
12-
using Microsoft.Extensions.DependencyInjection;
1312
using Microsoft.JSInterop;
1413
using Microsoft.JSInterop.WebAssembly;
1514

@@ -20,19 +19,18 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
2019
///
2120
/// Supports finding pre-loaded assemblies in a server or pre-rendering context.
2221
/// </summary>
23-
public class LazyAssemblyLoader
22+
public sealed class LazyAssemblyLoader
2423
{
2524
internal const string GetDynamicAssemblies = "window.Blazor._internal.getLazyAssemblies";
2625
internal const string ReadDynamicAssemblies = "window.Blazor._internal.readLazyAssemblies";
2726

28-
private List<Assembly> _loadedAssemblyCache = new List<Assembly>();
27+
private readonly IJSRuntime _jsRuntime;
28+
private readonly Dictionary<string, Assembly> _loadedAssemblyCache;
2929

30-
private readonly IServiceProvider _provider;
31-
32-
public LazyAssemblyLoader(IServiceProvider provider)
30+
public LazyAssemblyLoader(IJSRuntime jsRuntime)
3331
{
34-
_provider = provider;
35-
_loadedAssemblyCache = AppDomain.CurrentDomain.GetAssemblies().ToList();
32+
_jsRuntime = jsRuntime;
33+
_loadedAssemblyCache = AppDomain.CurrentDomain.GetAssemblies().ToDictionary(assembly => assembly.GetName().Name + ".dll", assembly => assembly);
3634
}
3735

3836
/// <summary>
@@ -53,39 +51,47 @@ public async Task<IEnumerable<Assembly>> LoadAssembliesAsync(IEnumerable<string>
5351
return await LoadAssembliesInServerAsync(assembliesToLoad);
5452
}
5553

56-
private Task<IEnumerable<Assembly>> LoadAssembliesInServerAsync(IEnumerable<string> assembliesToLoad)
54+
private async Task<IEnumerable<Assembly>> LoadAssembliesInServerAsync(IEnumerable<string> assembliesToLoad)
5755
{
58-
var loadedAssemblies = _loadedAssemblyCache.Where(assembly =>
59-
assembliesToLoad.Contains(assembly.GetName().Name + ".dll"));
56+
var loadedAssemblies = new List<Assembly>();
6057

61-
if (loadedAssemblies.Count() != assembliesToLoad.Count())
58+
try
59+
{
60+
foreach (var assemblyName in assembliesToLoad)
61+
{
62+
loadedAssemblies.Add(Assembly.Load(Path.GetFileNameWithoutExtension(assemblyName)));
63+
}
64+
}
65+
catch (FileNotFoundException ex)
6266
{
63-
var unloadedAssemblies = assembliesToLoad.Except(loadedAssemblies.Select(a => a.GetName().Name + ".dll"));
64-
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.");
67+
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.");
6568
}
6669

67-
return Task.FromResult(loadedAssemblies);
70+
return await Task.FromResult(loadedAssemblies);
6871
}
6972

7073
private async Task<IEnumerable<Assembly>> LoadAssembliesInClientAsync(IEnumerable<string> assembliesToLoad)
7174
{
72-
var jsRuntime = _provider.GetRequiredService<IJSRuntime>();
73-
// Only load assemblies that haven't already been lazily-loaded
74-
var newAssembliesToLoad = assembliesToLoad.Except(_loadedAssemblyCache.Select(a => a.GetName().Name + ".dll"));
75+
// Check to see if the assembly has already been loaded and avoids reloading it if so.
76+
// Note: in the future, as an extra precuation, we can call `Assembly.Load` and check
77+
// to see if it throws FileNotFound to ensure that an assembly hasn't been loaded
78+
// between when the cache of loaded assemblies was instantiated in the constructor
79+
// and the invocation of this method.
80+
var newAssembliesToLoad = assembliesToLoad.Except(_loadedAssemblyCache.Keys);
7581
var loadedAssemblies = new List<Assembly>();
7682

77-
var count = (int)await ((WebAssemblyJSRuntime)jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
78-
GetDynamicAssemblies,
79-
assembliesToLoad.ToArray(),
80-
null,
81-
null);
83+
var count = (int)await ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
84+
GetDynamicAssemblies,
85+
newAssembliesToLoad.ToArray(),
86+
null,
87+
null);
8288

8389
if (count == 0)
8490
{
8591
return loadedAssemblies;
8692
}
8793

88-
var assemblies = ((WebAssemblyJSRuntime)jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
94+
var assemblies = ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
8995
ReadDynamicAssemblies,
9096
null,
9197
null,
@@ -99,7 +105,7 @@ private async Task<IEnumerable<Assembly>> LoadAssembliesInClientAsync(IEnumerabl
99105
// into the default app context.
100106
var loadedAssembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(assembly));
101107
loadedAssemblies.Add(loadedAssembly);
102-
_loadedAssemblyCache.Add(loadedAssembly);
108+
_loadedAssemblyCache.Add(loadedAssembly.GetName().Name + ".dll", loadedAssembly);
103109
}
104110

105111
return loadedAssemblies;

src/Components/test/E2ETest/Tests/WebAssemblyLazyLoadTest.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,21 @@ public void CanLazyLoadAssemblyWithRoutes()
111111
Assert.True(renderedElement.Displayed);
112112
}
113113

114+
[Fact]
115+
public void ThrowsErrorForUnavailableAssemblies()
116+
{
117+
// Navigate to a page with lazy loaded assemblies for the first time
118+
SetUrlViaPushState("/Other");
119+
var app = Browser.MountTestComponent<TestRouterWithLazyAssembly>();
120+
121+
// Should've thrown an error for unhandled error
122+
var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10));
123+
Assert.NotNull(errorUiElem);
124+
125+
126+
AssertLogContainsCriticalMessages("DoesNotExist.dll must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.");
127+
}
128+
114129
private string SetUrlViaPushState(string relativeUri)
115130
{
116131
var pathBaseWithoutHash = ServerPathBase.Split('#')[0];
@@ -145,5 +160,18 @@ private void AssertLogDoesNotContainCriticalMessages(params string[] messages)
145160
});
146161
}
147162
}
163+
164+
void AssertLogContainsCriticalMessages(params string[] messages)
165+
{
166+
var log = Browser.Manage().Logs.GetLog(LogType.Browser);
167+
foreach (var message in messages)
168+
{
169+
Assert.Contains(log, entry =>
170+
{
171+
return entry.Level == LogLevel.Severe
172+
&& entry.Message.Contains(message);
173+
});
174+
}
175+
}
148176
}
149177
}

src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithLazyAssembly.razor

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@using Microsoft.AspNetCore.Components.Routing
22
@using System.Reflection
3-
@using Microsoft.AspNetCore.Components.WebAssembly.Services
3+
@using Microsoft.AspNetCore.Components.WebAssembly.Services
44

55
@inject LazyAssemblyLoader lazyLoader
66

@@ -31,25 +31,24 @@
3131

3232
private async Task LoadAssemblies(string uri)
3333
{
34-
try
34+
if (uri.EndsWith("WithLazyAssembly"))
3535
{
36-
if (uri.EndsWith("WithLazyAssembly"))
37-
{
38-
Console.WriteLine($"Loading assemblies for WithLazyAssembly...");
39-
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "Newtonsoft.Json.dll" });
40-
lazyLoadedAssemblies.AddRange(assemblies);
41-
}
42-
43-
if (uri.EndsWith("WithLazyLoadedRoutes"))
44-
{
45-
Console.WriteLine($"Loading assemblies for WithLazyLoadedRoutes...");
46-
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "LazyTestContentPackage.dll" });
47-
lazyLoadedAssemblies.AddRange(assemblies);
48-
}
36+
Console.WriteLine($"Loading assemblies for WithLazyAssembly...");
37+
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "Newtonsoft.Json.dll" });
38+
lazyLoadedAssemblies.AddRange(assemblies);
4939
}
50-
catch (Exception e)
40+
41+
if (uri.EndsWith("WithLazyLoadedRoutes"))
5142
{
52-
Console.WriteLine($"Error when loading assemblies: {e}");
43+
Console.WriteLine($"Loading assemblies for WithLazyLoadedRoutes...");
44+
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "LazyTestContentPackage.dll" });
45+
lazyLoadedAssemblies.AddRange(assemblies);
46+
}
47+
48+
if (uri.EndsWith("Other")) {
49+
Console.WriteLine($"Loading assemblies for Other...");
50+
var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "DoesNotExist.dll" });
51+
lazyLoadedAssemblies.AddRange(assemblies);
5352
}
5453
}
5554
}

0 commit comments

Comments
 (0)