Skip to content

Commit ec5f407

Browse files
committed
Add framework support for lazy-loading assemblies on route change
1 parent f6a6e4b commit ec5f407

File tree

14 files changed

+202
-10
lines changed

14 files changed

+202
-10
lines changed

src/Components/Components/src/NavigationManager.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Reflection;
7+
using System.Threading.Tasks;
58
using Microsoft.AspNetCore.Components.Routing;
69

710
namespace Microsoft.AspNetCore.Components
@@ -27,6 +30,7 @@ public event EventHandler<LocationChangedEventArgs> LocationChanged
2730
_locationChanged -= value;
2831
}
2932
}
33+
public Func<string, List<string>>? OnNavigate { get; set; }
3034

3135
private EventHandler<LocationChangedEventArgs>? _locationChanged;
3236

@@ -199,10 +203,11 @@ internal static string NormalizeBaseUri(string baseUri)
199203
/// <summary>
200204
/// Triggers the <see cref="LocationChanged"/> event with the current URI value.
201205
/// </summary>
202-
protected void NotifyLocationChanged(bool isInterceptedLink)
206+
protected async Task NotifyLocationChanged(bool isInterceptedLink)
203207
{
204208
try
205209
{
210+
await BeforeLocationChangeAsync();
206211
_locationChanged?.Invoke(this, new LocationChangedEventArgs(_uri!, isInterceptedLink));
207212
}
208213
catch (Exception ex)
@@ -211,6 +216,8 @@ protected void NotifyLocationChanged(bool isInterceptedLink)
211216
}
212217
}
213218

219+
public virtual Task BeforeLocationChangeAsync() => Task.CompletedTask;
220+
214221
private void AssertInitialized()
215222
{
216223
if (!_isInitialized)

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ static readonly ReadOnlyDictionary<string, object> _emptyParametersDictionary
5656
/// </summary>
5757
[Parameter] public RenderFragment<RouteData> Found { get; set; }
5858

59+
[Parameter] public Func<string, List<string>> OnNavigate { get; set; }
60+
5961
private RouteTable Routes { get; set; }
6062

6163
/// <inheritdoc />
@@ -69,7 +71,7 @@ public void Attach(RenderHandle renderHandle)
6971
}
7072

7173
/// <inheritdoc />
72-
public Task SetParametersAsync(ParameterView parameters)
74+
public async Task SetParametersAsync(ParameterView parameters)
7375
{
7476
parameters.SetParameterProperties(this);
7577

@@ -93,11 +95,16 @@ public Task SetParametersAsync(ParameterView parameters)
9395
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}.");
9496
}
9597

98+
if (OnNavigate != null) {
99+
NavigationManager.OnNavigate = OnNavigate;
100+
}
101+
102+
await NavigationManager.BeforeLocationChangeAsync();
103+
96104

97105
var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies);
98106
Routes = RouteTableFactory.Create(assemblies);
99107
Refresh(isNavigationIntercepted: false);
100-
return Task.CompletedTask;
101108
}
102109

103110
/// <inheritdoc />

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/BootConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface BootJsonData {
3030

3131
export interface ResourceGroups {
3232
readonly assembly: ResourceList;
33+
readonly dynamicAssembly: ResourceList;
3334
readonly pdb?: ResourceList;
3435
readonly runtime: ResourceList;
3536
readonly satelliteResources?: { [cultureName: string] : ResourceList };

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,35 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
288288
}
289289
return BINDING.js_to_mono_obj(Promise.resolve(0));
290290
}
291+
292+
window['Blazor']._internal.getDynamicAssemblies = (assembliesToLoadDotNetArray: System_Array<System_String>) : System_Object => {
293+
const assembliesToLoad = BINDING.mono_array_to_js_array<System_String, string>(assembliesToLoadDotNetArray);
294+
const dynamicAssemblies = resourceLoader.bootConfig.resources.dynamicAssembly;
295+
296+
if (dynamicAssemblies) {
297+
const resourcePromises = Promise.all(assembliesToLoad
298+
.filter(assembly => dynamicAssemblies.hasOwnProperty(assembly))
299+
.map(assembly => resourceLoader.loadResource(assembly, `_framework/_bin/${assembly}`, dynamicAssemblies[assembly], 'assembly'))
300+
.map(async resource => (await resource.response).arrayBuffer()));
301+
302+
return BINDING.js_to_mono_obj(
303+
resourcePromises.then(resourcesToLoad => {
304+
console.log(`resourcesToLoad: ${resourcesToLoad}`)
305+
if (resourcesToLoad.length) {
306+
window['Blazor']._internal.readDynamicAssemblies = () => {
307+
const array = BINDING.mono_obj_array_new(resourcesToLoad.length);
308+
for (var i = 0; i < resourcesToLoad.length; i++) {
309+
BINDING.mono_obj_array_set(array, i, BINDING.js_typed_array_to_array(new Uint8Array(resourcesToLoad[i])));
310+
}
311+
return array;
312+
};
313+
}
314+
315+
return resourcesToLoad.length;
316+
}));
317+
}
318+
return BINDING.js_to_mono_obj(Promise.resolve(0));
319+
}
291320
});
292321

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

src/Components/Web.JS/src/Platform/WebAssemblyStartOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ export interface WebAssemblyStartOptions {
1414
// This type doesn't have to align with anything in BootConfig.
1515
// Instead, this represents the public API through which certain aspects
1616
// of boot resource loading can be customized.
17-
export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'timezonedata';
17+
export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'dynamicAssembly' | 'timezonedata';

src/Components/WebAssembly/WebAssembly/src/Infrastructure/JSInteropMethods.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ public static class JSInteropMethods
2222
/// For framework use only.
2323
/// </summary>
2424
[JSInvokable(nameof(NotifyLocationChanged))]
25-
public static void NotifyLocationChanged(string uri, bool isInterceptedLink)
25+
public static Task NotifyLocationChanged(string uri, bool isInterceptedLink)
2626
{
27-
WebAssemblyNavigationManager.Instance.SetLocation(uri, isInterceptedLink);
27+
return WebAssemblyNavigationManager.Instance.SetLocation(uri, isInterceptedLink);
2828
}
2929

3030
/// <summary>

src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using Microsoft.AspNetCore.Components;
5+
using System.Collections.Generic;
6+
using System.Reflection;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Components.Routing;
69
using Interop = Microsoft.AspNetCore.Components.Web.BrowserNavigationManagerInterop;
710

811
namespace Microsoft.AspNetCore.Components.WebAssembly.Services
@@ -22,10 +25,10 @@ public WebAssemblyNavigationManager(string baseUri, string uri)
2225
Initialize(baseUri, uri);
2326
}
2427

25-
public void SetLocation(string uri, bool isInterceptedLink)
28+
public Task SetLocation(string uri, bool isInterceptedLink)
2629
{
2730
Uri = uri;
28-
NotifyLocationChanged(isInterceptedLink);
31+
return NotifyLocationChanged(isInterceptedLink);
2932
}
3033

3134
/// <inheritdoc />
@@ -38,5 +41,41 @@ protected override void NavigateToCore(string uri, bool forceLoad)
3841

3942
DefaultWebAssemblyJSRuntime.Instance.Invoke<object>(Interop.NavigateTo, uri, forceLoad);
4043
}
44+
45+
public override async Task BeforeLocationChangeAsync()
46+
{
47+
if (OnNavigate == null) {
48+
return;
49+
}
50+
51+
var assembliesToLoad = OnNavigate(Uri);
52+
53+
if (assembliesToLoad.Count == 0)
54+
{
55+
return;
56+
}
57+
58+
var count = (int)await DefaultWebAssemblyJSRuntime.Instance.InvokeUnmarshalled<string[], object, object, Task<object>>(
59+
"window.Blazor._internal.getDynamicAssemblies",
60+
assembliesToLoad.ToArray(),
61+
null,
62+
null);
63+
64+
if (count == 0)
65+
{
66+
return;
67+
}
68+
69+
var assemblies = DefaultWebAssemblyJSRuntime.Instance.InvokeUnmarshalled<object, object, object, object[]>(
70+
"window.Blazor._internal.readDynamicAssemblies",
71+
null,
72+
null,
73+
null);
74+
75+
for (var i = 0; i < assemblies.Length; i++)
76+
{
77+
Assembly.Load((byte[])assemblies[i]);
78+
}
79+
}
4180
}
4281
}

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,47 @@ public void PreventDefault_CanBlockNavigation(string navigationType, string wher
522522
}
523523
}
524524

525+
[Fact]
526+
public void CanLazyLoadOnRouteChange()
527+
{
528+
// Navigate to a page without any lazy-loaded dependencies
529+
SetUrlViaPushState("/");
530+
var app = Browser.MountTestComponent<TestRouterWithDynamicAssembly>();
531+
532+
// Ensure that we haven't requested the lazy loaded assembly
533+
Assert.False(HasLoadedAssembly("Newtonsoft.Json.dll"));
534+
535+
// Visit the route for the lazy-loaded assembly
536+
SetUrlViaPushState("/WithDynamicAssembly");
537+
538+
var button = app.FindElement(By.Id("use-package-button"));
539+
540+
// Now we should have requested the DLL
541+
Assert.True(HasLoadedAssembly("Newtonsoft.Json.dll"));
542+
543+
button.Click();
544+
545+
// We shouldn't get any errors about assemblies not being available
546+
AssertLogDoesNotContainCriticalMessages("Could not load file or assembly 'Newtonsoft.Json");
547+
}
548+
549+
[Fact]
550+
public void CanLazyLoadOnFirstVisit()
551+
{
552+
// Navigate to a page with lazy loaded assemblies for the first time
553+
SetUrlViaPushState("/WithDynamicAssembly");
554+
var app = Browser.MountTestComponent<TestRouterWithDynamicAssembly>();
555+
var button = app.FindElement(By.Id("use-package-button"));
556+
557+
// We should have requested the DLL
558+
Assert.True(HasLoadedAssembly("Newtonsoft.Json.dll"));
559+
560+
button.Click();
561+
562+
// We shouldn't get any errors about assemblies not being available
563+
AssertLogDoesNotContainCriticalMessages("Could not load file or assembly 'Newtonsoft.Json");
564+
}
565+
525566
private long BrowserScrollY
526567
{
527568
get => (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window.scrollY");
@@ -538,11 +579,36 @@ private string SetUrlViaPushState(string relativeUri)
538579
return absoluteUri.AbsoluteUri;
539580
}
540581

582+
private bool HasLoadedAssembly(string name)
583+
{
584+
var checkScript = $"return window.performance.getEntriesByType('resource').some(r => r.name.endsWith('{name}'));";
585+
var jsExecutor = (IJavaScriptExecutor)Browser;
586+
var nameRequested = jsExecutor.ExecuteScript(checkScript);
587+
if (nameRequested != null)
588+
{
589+
return (bool)nameRequested;
590+
}
591+
return false;
592+
}
593+
541594
private void AssertHighlightedLinks(params string[] linkTexts)
542595
{
543596
Browser.Equal(linkTexts, () => Browser
544597
.FindElements(By.CssSelector("a.active"))
545598
.Select(x => x.Text));
546599
}
600+
601+
private void AssertLogDoesNotContainCriticalMessages(params string[] messages)
602+
{
603+
var log = Browser.Manage().Logs.GetLog(LogType.Browser);
604+
foreach (var message in messages)
605+
{
606+
Assert.DoesNotContain(log, entry =>
607+
{
608+
return entry.Level == LogLevel.Severe
609+
&& entry.Message.Contains(message);
610+
});
611+
}
612+
}
547613
}
548614
}

src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
2121
<Reference Include="Microsoft.AspNetCore.Components.Authorization" />
2222
<Reference Include="Microsoft.Extensions.Logging.Configuration" />
23+
<Reference Include="Newtonsoft.Json" />
2324
</ItemGroup>
2425

2526
<ItemGroup>
@@ -30,4 +31,8 @@
3031
<EmbeddedResource Update="Resources.resx" GenerateSource="true" />
3132
</ItemGroup>
3233

34+
<ItemGroup>
35+
<BlazorWebAssemblyLazyLoad Include="Newtonsoft.Json.dll" />
36+
</ItemGroup>
37+
3338
</Project>

src/Components/test/testassets/BasicTestApp/Index.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
<option value="BasicTestApp.RouterTest.NavigationManagerComponent">NavigationManager Test</option>
7171
<option value="BasicTestApp.RouterTest.TestRouter">Router</option>
7272
<option value="BasicTestApp.RouterTest.TestRouterWithAdditionalAssembly">Router with additional assembly</option>
73+
<option value="BasicTestApp.RouterTest.TestRouterWithDynamicAssembly">Router with dynamic assembly</option>
7374
<option value="BasicTestApp.StringComparisonComponent">StringComparison</option>
7475
<option value="BasicTestApp.SvgComponent">SVG</option>
7576
<option value="BasicTestApp.SvgWithChildComponent">SVG with child component</option>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<li><NavLink href="/subdir/WithParameters/Name/Abc/LastName/McDef">With more parameters</NavLink></li>
2121
<li><NavLink href="/subdir/LongPage1">Long page 1</NavLink></li>
2222
<li><NavLink href="/subdir/LongPage2">Long page 2</NavLink></li>
23+
<li><NavLink href="/subdir/WithDynamicAssembly">With dynamic assembly</NavLink></li>
2324
<li><NavLink href="PreventDefaultCases">preventDefault cases</NavLink></li>
2425
<li><NavLink>Null href never matches</NavLink></li>
2526
</ul>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@using Microsoft.AspNetCore.Components.Routing
2+
3+
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" OnNavigate="@OnNavigate">
4+
<Found Context="routeData">
5+
<RouteView RouteData="@routeData" />
6+
</Found>
7+
<NotFound>
8+
<LayoutView Layout="@typeof(RouterTestLayout)">
9+
<div id="test-info">Oops, that component wasn't found!</div>
10+
</LayoutView>
11+
</NotFound>
12+
</Router>
13+
14+
@code {
15+
private List<string> OnNavigate(string uri)
16+
{
17+
if (uri.EndsWith("WithDynamicAssembly")) {
18+
return new List<string>{"Newtonsoft.Json.dll"};
19+
}
20+
return new List<string>();
21+
}
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@page "/WithDynamicAssembly"
2+
3+
@using Newtonsoft.Json
4+
5+
<p>Just a webpage that uses a lazy-loaded dependency.</p>
6+
7+
<button @onclick="UsePackage" id="use-package-button">Click Me</button>
8+
9+
@code
10+
{
11+
private void UsePackage() {
12+
JsonConvert.DeserializeObject("{ 'type': 'Test' }");
13+
}
14+
}

0 commit comments

Comments
 (0)