Add framework support for lazy-loading assemblies on route change (#23290)
* Add framework support for lazy-loading assemblies on route change * Configure lazy-loaded assemblies in WebAssemblyLazyLoadDefinition * Move tests to WebAssembly-only scenarios * Refactor RouteTableFactory and add WebAssemblyDynamicResourceLoader * Address feedback from peer review * Rename 'dynamicAssembly' to 'lazyAssembly' and address peer review * Add sample with loading state * Update Router API and assembly loading tests * Support and test cancellation and pre-rendering * Apply suggestions from code review Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com> * Spurce up API and add tests for pre-rendering scenario * Use CT instead of CTS in NavigationContext * Address feedback from peer review * Remove extra test file and update Router Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com>
This commit is contained in:
parent
37c20036b3
commit
bbc116254a
|
|
@ -556,6 +556,12 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
public bool IsNavigationIntercepted { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||
public string Location { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||
}
|
||||
public sealed partial class NavigationContext
|
||||
{
|
||||
internal NavigationContext() { }
|
||||
public System.Threading.CancellationToken CancellationToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||
public string Path { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
|
||||
}
|
||||
public partial class Router : Microsoft.AspNetCore.Components.IComponent, Microsoft.AspNetCore.Components.IHandleAfterRender, System.IDisposable
|
||||
{
|
||||
public Router() { }
|
||||
|
|
@ -566,10 +572,15 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.RouteData> Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment Navigating { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.RenderFragment NotFound { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
public Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.Routing.NavigationContext> OnNavigateAsync { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||
public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
|
||||
public void Dispose() { }
|
||||
System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides information about the current asynchronous navigation event
|
||||
/// including the target path and the cancellation token.
|
||||
/// </summary>
|
||||
public sealed class NavigationContext
|
||||
{
|
||||
internal NavigationContext(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
Path = path;
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public CancellationToken CancellationToken { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,8 @@ using System.Collections.Generic;
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Routing
|
||||
|
|
@ -29,6 +29,12 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
bool _navigationInterceptionEnabled;
|
||||
ILogger<Router> _logger;
|
||||
|
||||
private CancellationTokenSource _onNavigateCts;
|
||||
|
||||
private readonly HashSet<Assembly> _assemblies = new HashSet<Assembly>();
|
||||
|
||||
private bool _onNavigateCalled = false;
|
||||
|
||||
[Inject] private NavigationManager NavigationManager { get; set; }
|
||||
|
||||
[Inject] private INavigationInterception NavigationInterception { get; set; }
|
||||
|
|
@ -56,6 +62,16 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
/// </summary>
|
||||
[Parameter] public RenderFragment<RouteData> Found { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get or sets the content to display when asynchronous navigation is in progress.
|
||||
/// </summary>
|
||||
[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 EventCallback<NavigationContext> OnNavigateAsync { get; set; }
|
||||
|
||||
private RouteTable Routes { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -69,7 +85,7 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetParametersAsync(ParameterView parameters)
|
||||
public async Task SetParametersAsync(ParameterView parameters)
|
||||
{
|
||||
parameters.SetParameterProperties(this);
|
||||
|
||||
|
|
@ -93,17 +109,20 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}.");
|
||||
}
|
||||
|
||||
if (!_onNavigateCalled)
|
||||
{
|
||||
_onNavigateCalled = true;
|
||||
await RunOnNavigateAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute));
|
||||
}
|
||||
|
||||
var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies);
|
||||
Routes = RouteTableFactory.Create(assemblies);
|
||||
Refresh(isNavigationIntercepted: false);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
_onNavigateCts?.Dispose();
|
||||
}
|
||||
|
||||
private static string StringUntilAny(string str, char[] chars)
|
||||
|
|
@ -114,8 +133,24 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
: str.Substring(0, firstIndex);
|
||||
}
|
||||
|
||||
private void RefreshRouteTable()
|
||||
{
|
||||
var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies);
|
||||
var assembliesSet = new HashSet<Assembly>(assemblies);
|
||||
|
||||
if (!_assemblies.SetEquals(assembliesSet))
|
||||
{
|
||||
Routes = RouteTableFactory.Create(assemblies);
|
||||
_assemblies.Clear();
|
||||
_assemblies.UnionWith(assembliesSet);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void Refresh(bool isNavigationIntercepted)
|
||||
{
|
||||
RefreshRouteTable();
|
||||
|
||||
var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute);
|
||||
locationPath = StringUntilAny(locationPath, _queryOrHashStartChar);
|
||||
var context = new RouteContext(locationPath);
|
||||
|
|
@ -155,12 +190,52 @@ namespace Microsoft.AspNetCore.Components.Routing
|
|||
}
|
||||
}
|
||||
|
||||
private async Task RunOnNavigateAsync(string path)
|
||||
{
|
||||
// If this router instance does not provide an OnNavigateAsync parameter
|
||||
// then we render the component associated with the route as per usual.
|
||||
if (!OnNavigateAsync.HasDelegate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If we've already invoked a task and stored its CTS, then
|
||||
// cancel the existing task.
|
||||
_onNavigateCts?.Dispose();
|
||||
|
||||
// Create a new cancellation token source for this instance
|
||||
_onNavigateCts = new CancellationTokenSource();
|
||||
var navigateContext = new NavigationContext(path, _onNavigateCts.Token);
|
||||
|
||||
// Create a cancellation task based on the cancellation token
|
||||
// associated with the current running task.
|
||||
var cancellationTaskSource = new TaskCompletionSource();
|
||||
navigateContext.CancellationToken.Register(state =>
|
||||
((TaskCompletionSource)state).SetResult(), cancellationTaskSource);
|
||||
|
||||
var task = OnNavigateAsync.InvokeAsync(navigateContext);
|
||||
|
||||
// If the user provided a Navigating render fragment, then show it.
|
||||
if (Navigating != null && task.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
_renderHandle.Render(Navigating);
|
||||
}
|
||||
|
||||
await Task.WhenAny(task, cancellationTaskSource.Task);
|
||||
}
|
||||
|
||||
private async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted)
|
||||
{
|
||||
await RunOnNavigateAsync(path);
|
||||
Refresh(isNavigationIntercepted);
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
|
||||
{
|
||||
_locationAbsolute = args.Location;
|
||||
if (_renderHandle.IsInitialized && Routes != null)
|
||||
{
|
||||
Refresh(args.IsNavigationIntercepted);
|
||||
_ = RunOnNavigateWithRefreshAsync(NavigationManager.ToBaseRelativePath(_locationAbsolute), args.IsNavigationIntercepted);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -30,6 +30,7 @@ export interface BootJsonData {
|
|||
|
||||
export interface ResourceGroups {
|
||||
readonly assembly: ResourceList;
|
||||
readonly lazyAssembly: ResourceList;
|
||||
readonly pdb?: ResourceList;
|
||||
readonly runtime: ResourceList;
|
||||
readonly satelliteResources?: { [cultureName: string] : ResourceList };
|
||||
|
|
|
|||
|
|
@ -293,6 +293,34 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
|
|||
}
|
||||
return BINDING.js_to_mono_obj(Promise.resolve(0));
|
||||
}
|
||||
|
||||
window['Blazor']._internal.getLazyAssemblies = (assembliesToLoadDotNetArray: System_Array<System_String>) : System_Object => {
|
||||
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))
|
||||
.map(assembly => resourceLoader.loadResource(assembly, `_framework/${assembly}`, lazyAssemblies[assembly], 'assembly'))
|
||||
.map(async resource => (await resource.response).arrayBuffer()));
|
||||
|
||||
return BINDING.js_to_mono_obj(
|
||||
resourcePromises.then(resourcesToLoad => {
|
||||
if (resourcesToLoad.length) {
|
||||
window['Blazor']._internal.readLazyAssemblies = () => {
|
||||
const array = BINDING.mono_obj_array_new(resourcesToLoad.length);
|
||||
for (var i = 0; i < resourcesToLoad.length; i++) {
|
||||
BINDING.mono_obj_array_set(array, i, BINDING.js_typed_array_to_array(new Uint8Array(resourcesToLoad[i])));
|
||||
}
|
||||
return array;
|
||||
};
|
||||
}
|
||||
|
||||
return resourcesToLoad.length;
|
||||
}));
|
||||
}
|
||||
return BINDING.js_to_mono_obj(Promise.resolve(0));
|
||||
}
|
||||
});
|
||||
|
||||
module.postRun.push(() => {
|
||||
|
|
|
|||
|
|
@ -1,176 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Xunit;
|
||||
using static Microsoft.AspNetCore.Components.WebAssembly.Build.WebAssemblyRuntimePackage;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||
{
|
||||
public class BuildLazyLoadTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task Build_LazyLoadExplicitAssembly_Debug_Works()
|
||||
{
|
||||
// Arrange
|
||||
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
|
||||
project.Configuration = "Debug";
|
||||
|
||||
project.AddProjectFileContent(
|
||||
@"
|
||||
<ItemGroup>
|
||||
<BlazorWebAssemblyLazyLoad Include='RazorClassLibrary.dll' />
|
||||
</ItemGroup>
|
||||
");
|
||||
|
||||
var result = await MSBuildProcessManager.DotnetMSBuild(project);
|
||||
|
||||
var buildOutputDirectory = project.BuildOutputDirectory;
|
||||
|
||||
// Verify that a blazor.boot.json file has been created
|
||||
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
|
||||
// And that the assembly is in the output
|
||||
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "RazorClassLibrary.dll");
|
||||
|
||||
var bootJson = ReadBootJsonData(result, Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json"));
|
||||
|
||||
// And that it has been labelled as a dynamic assembly in the boot.json
|
||||
var dynamicAssemblies = bootJson.resources.dynamicAssembly;
|
||||
var assemblies = bootJson.resources.assembly;
|
||||
|
||||
Assert.NotNull(dynamicAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", dynamicAssemblies.Keys);
|
||||
Assert.DoesNotContain("RazorClassLibrary.dll", assemblies.Keys);
|
||||
|
||||
// App assembly should not be lazy loaded
|
||||
Assert.DoesNotContain("standalone.dll", dynamicAssemblies.Keys);
|
||||
Assert.Contains("standalone.dll", assemblies.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Build_LazyLoadExplicitAssembly_Release_Works()
|
||||
{
|
||||
// Arrange
|
||||
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
|
||||
project.Configuration = "Release";
|
||||
|
||||
project.AddProjectFileContent(
|
||||
@"
|
||||
<ItemGroup>
|
||||
<BlazorWebAssemblyLazyLoad Include='RazorClassLibrary.dll' />
|
||||
</ItemGroup>
|
||||
");
|
||||
|
||||
var result = await MSBuildProcessManager.DotnetMSBuild(project);
|
||||
|
||||
var buildOutputDirectory = project.BuildOutputDirectory;
|
||||
|
||||
// Verify that a blazor.boot.json file has been created
|
||||
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
|
||||
// And that the assembly is in the output
|
||||
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "RazorClassLibrary.dll");
|
||||
|
||||
var bootJson = ReadBootJsonData(result, Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json"));
|
||||
|
||||
// And that it has been labelled as a dynamic assembly in the boot.json
|
||||
var dynamicAssemblies = bootJson.resources.dynamicAssembly;
|
||||
var assemblies = bootJson.resources.assembly;
|
||||
|
||||
Assert.NotNull(dynamicAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", dynamicAssemblies.Keys);
|
||||
Assert.DoesNotContain("RazorClassLibrary.dll", assemblies.Keys);
|
||||
|
||||
// App assembly should not be lazy loaded
|
||||
Assert.DoesNotContain("standalone.dll", dynamicAssemblies.Keys);
|
||||
Assert.Contains("standalone.dll", assemblies.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Publish_LazyLoadExplicitAssembly_Debug_Works()
|
||||
{
|
||||
// Arrange
|
||||
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
|
||||
project.Configuration = "Debug";
|
||||
|
||||
project.AddProjectFileContent(
|
||||
@"
|
||||
<ItemGroup>
|
||||
<BlazorWebAssemblyLazyLoad Include='RazorClassLibrary.dll' />
|
||||
</ItemGroup>
|
||||
");
|
||||
|
||||
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish");
|
||||
|
||||
var publishDirectory = project.PublishOutputDirectory;
|
||||
|
||||
// Verify that a blazor.boot.json file has been created
|
||||
Assert.FileExists(result, publishDirectory, "wwwroot", "_framework", "blazor.boot.json");
|
||||
// And that the assembly is in the output
|
||||
Assert.FileExists(result, publishDirectory, "wwwroot", "_framework", "_bin", "RazorClassLibrary.dll");
|
||||
|
||||
var bootJson = ReadBootJsonData(result, Path.Combine(publishDirectory, "wwwroot", "_framework", "blazor.boot.json"));
|
||||
|
||||
// And that it has been labelled as a dynamic assembly in the boot.json
|
||||
var dynamicAssemblies = bootJson.resources.dynamicAssembly;
|
||||
var assemblies = bootJson.resources.assembly;
|
||||
|
||||
Assert.NotNull(dynamicAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", dynamicAssemblies.Keys);
|
||||
Assert.DoesNotContain("RazorClassLibrary.dll", assemblies.Keys);
|
||||
|
||||
// App assembly should not be lazy loaded
|
||||
Assert.DoesNotContain("standalone.dll", dynamicAssemblies.Keys);
|
||||
Assert.Contains("standalone.dll", assemblies.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Publish_LazyLoadExplicitAssembly_Release_Works()
|
||||
{
|
||||
// Arrange
|
||||
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
|
||||
project.Configuration = "Release";
|
||||
|
||||
project.AddProjectFileContent(
|
||||
@"
|
||||
<ItemGroup>
|
||||
<BlazorWebAssemblyLazyLoad Include='RazorClassLibrary.dll' />
|
||||
</ItemGroup>
|
||||
");
|
||||
|
||||
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish");
|
||||
|
||||
var publishDirectory = project.PublishOutputDirectory;
|
||||
|
||||
// Verify that a blazor.boot.json file has been created
|
||||
Assert.FileExists(result, publishDirectory, "wwwroot", "_framework", "blazor.boot.json");
|
||||
// And that the assembly is in the output
|
||||
Assert.FileExists(result, publishDirectory, "wwwroot", "_framework", "_bin", "RazorClassLibrary.dll");
|
||||
|
||||
var bootJson = ReadBootJsonData(result, Path.Combine(publishDirectory, "wwwroot", "_framework", "blazor.boot.json"));
|
||||
|
||||
// And that it has been labelled as a dynamic assembly in the boot.json
|
||||
var dynamicAssemblies = bootJson.resources.dynamicAssembly;
|
||||
var assemblies = bootJson.resources.assembly;
|
||||
|
||||
Assert.NotNull(dynamicAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", dynamicAssemblies.Keys);
|
||||
Assert.DoesNotContain("RazorClassLibrary.dll", assemblies.Keys);
|
||||
|
||||
// App assembly should not be lazy loaded
|
||||
Assert.DoesNotContain("standalone.dll", dynamicAssemblies.Keys);
|
||||
Assert.Contains("standalone.dll", assemblies.Keys);
|
||||
}
|
||||
|
||||
private static GenerateBlazorBootJson.BootJsonData ReadBootJsonData(MSBuildResult result, string path)
|
||||
{
|
||||
return JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(
|
||||
File.ReadAllText(Path.Combine(result.Project.DirectoryPath, path)),
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
|
|
@ -11,7 +10,6 @@ using Microsoft.AspNetCore.Components.Web;
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Configuration.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
|
|
@ -191,6 +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.AddLogging(builder => {
|
||||
builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a service for loading assemblies at runtime in a browser context.
|
||||
///
|
||||
/// Supports finding pre-loaded assemblies in a server or pre-rendering context.
|
||||
/// </summary>
|
||||
public 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 IServiceProvider _provider;
|
||||
|
||||
public LazyAssemblyLoader(IServiceProvider provider)
|
||||
{
|
||||
_provider = provider;
|
||||
_loadedAssemblyCache = AppDomain.CurrentDomain.GetAssemblies().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In a browser context, calling this method will fetch the assemblies requested
|
||||
/// via a network call and load them into the runtime. In a server or pre-rendered
|
||||
/// context, this method will look for the assemblies already loaded in the runtime
|
||||
/// and return them.
|
||||
/// </summary>
|
||||
/// <param name="assembliesToLoad">The names of the assemblies to load (e.g. "MyAssembly.dll")</param>
|
||||
/// <returns>A list of the loaded <see cref="Assembly"/></returns>
|
||||
public async Task<IEnumerable<Assembly>> LoadAssembliesAsync(IEnumerable<string> assembliesToLoad)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Browser))
|
||||
{
|
||||
return await LoadAssembliesInClientAsync(assembliesToLoad);
|
||||
}
|
||||
|
||||
return await LoadAssembliesInServerAsync(assembliesToLoad);
|
||||
}
|
||||
|
||||
private Task<IEnumerable<Assembly>> LoadAssembliesInServerAsync(IEnumerable<string> assembliesToLoad)
|
||||
{
|
||||
var loadedAssemblies = _loadedAssemblyCache.Where(assembly =>
|
||||
assembliesToLoad.Contains(assembly.GetName().Name + ".dll"));
|
||||
|
||||
if (loadedAssemblies.Count() != assembliesToLoad.Count())
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
return Task.FromResult(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"));
|
||||
var loadedAssemblies = new List<Assembly>();
|
||||
|
||||
var count = (int)await ((WebAssemblyJSRuntime)jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
|
||||
GetDynamicAssemblies,
|
||||
assembliesToLoad.ToArray(),
|
||||
null,
|
||||
null);
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return loadedAssemblies;
|
||||
}
|
||||
|
||||
var assemblies = ((WebAssemblyJSRuntime)jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
|
||||
ReadDynamicAssemblies,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
foreach (byte[] assembly in assemblies)
|
||||
{
|
||||
// The runtime loads assemblies into an isolated context by default. As a result,
|
||||
// assemblies that are loaded via Assembly.Load aren't available in the app's context
|
||||
// AKA the default context. To work around this, we explicitly load the assemblies
|
||||
// into the default app context.
|
||||
var loadedAssembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(assembly));
|
||||
loadedAssemblies.Add(loadedAssembly);
|
||||
_loadedAssemblyCache.Add(loadedAssembly);
|
||||
}
|
||||
|
||||
return loadedAssemblies;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +60,18 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Browser.Equal("Hello from interop call", () => Browser.FindElement(By.Id("val-set-by-interop")).GetAttribute("value"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsCompatibleWithLazyLoadWebAssembly()
|
||||
{
|
||||
Navigate("/prerendered/WithLazyAssembly");
|
||||
|
||||
var button = Browser.FindElement(By.Id("use-package-button"));
|
||||
|
||||
button.Click();
|
||||
|
||||
AssertLogDoesNotContainCriticalMessages("Could not load file or assembly 'Newtonsoft.Json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanReadUrlHashOnlyOnceConnected()
|
||||
{
|
||||
|
|
@ -121,6 +133,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Browser.FindElement(By.Id("load-boot-script")).Click();
|
||||
}
|
||||
|
||||
private void AssertLogDoesNotContainCriticalMessages(params string[] messages)
|
||||
{
|
||||
var log = Browser.Manage().Logs.GetLog(LogType.Browser);
|
||||
foreach (var message in messages)
|
||||
{
|
||||
Assert.DoesNotContain(log, entry =>
|
||||
{
|
||||
return entry.Level == LogLevel.Severe
|
||||
&& entry.Message.Contains(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void SignInAs(string userName, string roles, bool useSeparateTab = false) =>
|
||||
Browser.SignInAs(new Uri(_serverFixture.RootUri, "/prerendered/"), userName, roles, useSeparateTab);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Microsoft.AspNetCore.E2ETesting;
|
|||
using Microsoft.AspNetCore.Testing;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Interactions;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
|
|
@ -527,6 +528,32 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnNavigate_CanRenderLoadingFragment()
|
||||
{
|
||||
var app = Browser.MountTestComponent<TestRouterWithOnNavigate>();
|
||||
|
||||
SetUrlViaPushState("/LongPage1");
|
||||
|
||||
new WebDriverWait(Browser, TimeSpan.FromSeconds(2)).Until(
|
||||
driver => driver.FindElement(By.Id("loading-banner")) != null);
|
||||
|
||||
Assert.True(app.FindElement(By.Id("loading-banner")) != null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnNavigate_CanCancelCallback()
|
||||
{
|
||||
var app = Browser.MountTestComponent<TestRouterWithOnNavigate>();
|
||||
|
||||
// Navigating from one page to another should
|
||||
// cancel the previous OnNavigate Task
|
||||
SetUrlViaPushState("/LongPage2");
|
||||
SetUrlViaPushState("/LongPage1");
|
||||
|
||||
AssertDidNotLog("I'm not happening...");
|
||||
}
|
||||
|
||||
private long BrowserScrollY
|
||||
{
|
||||
get => (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window.scrollY");
|
||||
|
|
@ -543,6 +570,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
return absoluteUri.AbsoluteUri;
|
||||
}
|
||||
|
||||
private void AssertDidNotLog(params string[] messages)
|
||||
{
|
||||
var log = Browser.Manage().Logs.GetLog(LogType.Browser);
|
||||
foreach (var message in messages)
|
||||
{
|
||||
Assert.DoesNotContain(log, entry => entry.Message.Contains(message));
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertHighlightedLinks(params string[] linkTexts)
|
||||
{
|
||||
Browser.Equal(linkTexts, () => Browser
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using BasicTestApp;
|
||||
using BasicTestApp.RouterTest;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||
{
|
||||
public class WebAssemblyLazyLoadTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
|
||||
{
|
||||
public WebAssemblyLazyLoadTest(
|
||||
BrowserFixture browserFixture,
|
||||
ToggleExecutionModeServerFixture<Program> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture, output)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void InitializeAsyncCore()
|
||||
{
|
||||
Navigate(ServerPathBase, noReload: false);
|
||||
Browser.MountTestComponent<TestRouterWithLazyAssembly>();
|
||||
Browser.Exists(By.Id("blazor-error-ui"));
|
||||
|
||||
var errorUi = Browser.FindElement(By.Id("blazor-error-ui"));
|
||||
Assert.Equal("none", errorUi.GetCssValue("display"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanLazyLoadOnRouteChange()
|
||||
{
|
||||
// Navigate to a page without any lazy-loaded dependencies
|
||||
SetUrlViaPushState("/");
|
||||
var app = Browser.MountTestComponent<TestRouterWithLazyAssembly>();
|
||||
|
||||
// Ensure that we haven't requested the lazy loaded assembly
|
||||
Assert.False(HasLoadedAssembly("Newtonsoft.Json.dll"));
|
||||
|
||||
// Visit the route for the lazy-loaded assembly
|
||||
SetUrlViaPushState("/WithLazyAssembly");
|
||||
|
||||
var button = app.FindElement(By.Id("use-package-button"));
|
||||
|
||||
// Now we should have requested the DLL
|
||||
Assert.True(HasLoadedAssembly("Newtonsoft.Json.dll"));
|
||||
|
||||
button.Click();
|
||||
|
||||
// We shouldn't get any errors about assemblies not being available
|
||||
AssertLogDoesNotContainCriticalMessages("Could not load file or assembly 'Newtonsoft.Json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanLazyLoadOnFirstVisit()
|
||||
{
|
||||
// Navigate to a page with lazy loaded assemblies for the first time
|
||||
SetUrlViaPushState("/WithLazyAssembly");
|
||||
var app = Browser.MountTestComponent<TestRouterWithLazyAssembly>();
|
||||
|
||||
// Wait for the page to finish loading
|
||||
new WebDriverWait(Browser, TimeSpan.FromSeconds(2)).Until(
|
||||
driver => driver.FindElement(By.Id("use-package-button")) != null);
|
||||
|
||||
var button = app.FindElement(By.Id("use-package-button"));
|
||||
|
||||
// We should have requested the DLL
|
||||
Assert.True(HasLoadedAssembly("Newtonsoft.Json.dll"));
|
||||
|
||||
button.Click();
|
||||
|
||||
// We shouldn't get any errors about assemblies not being available
|
||||
AssertLogDoesNotContainCriticalMessages("Could not load file or assembly 'Newtonsoft.Json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanLazyLoadAssemblyWithRoutes()
|
||||
{
|
||||
// Navigate to a page without any lazy-loaded dependencies
|
||||
SetUrlViaPushState("/");
|
||||
var app = Browser.MountTestComponent<TestRouterWithLazyAssembly>();
|
||||
|
||||
// Ensure that we haven't requested the lazy loaded assembly
|
||||
Assert.False(HasLoadedAssembly("LazyTestContentPackage.dll"));
|
||||
|
||||
// Navigate to the designated route
|
||||
SetUrlViaPushState("/WithLazyLoadedRoutes");
|
||||
|
||||
// Wait for the page to finish loading
|
||||
new WebDriverWait(Browser, TimeSpan.FromSeconds(2)).Until(
|
||||
driver => driver.FindElement(By.Id("lazy-load-msg")) != null);
|
||||
|
||||
// Now the assembly has been loaded
|
||||
Assert.True(HasLoadedAssembly("LazyTestContentPackage.dll"));
|
||||
|
||||
var button = app.FindElement(By.Id("go-to-lazy-route"));
|
||||
button.Click();
|
||||
|
||||
// Navigating the lazy-loaded route should show its content
|
||||
var renderedElement = app.FindElement(By.Id("lazy-page"));
|
||||
Assert.True(renderedElement.Displayed);
|
||||
}
|
||||
|
||||
private string SetUrlViaPushState(string relativeUri)
|
||||
{
|
||||
var pathBaseWithoutHash = ServerPathBase.Split('#')[0];
|
||||
var jsExecutor = (IJavaScriptExecutor)Browser;
|
||||
var absoluteUri = new Uri(_serverFixture.RootUri, $"{pathBaseWithoutHash}{relativeUri}");
|
||||
jsExecutor.ExecuteScript($"Blazor.navigateTo('{absoluteUri.ToString().Replace("'", "\\'")}')");
|
||||
|
||||
return absoluteUri.AbsoluteUri;
|
||||
}
|
||||
|
||||
private bool HasLoadedAssembly(string name)
|
||||
{
|
||||
var checkScript = $"return window.performance.getEntriesByType('resource').some(r => r.name.endsWith('{name}'));";
|
||||
var jsExecutor = (IJavaScriptExecutor)Browser;
|
||||
var nameRequested = jsExecutor.ExecuteScript(checkScript);
|
||||
if (nameRequested != null)
|
||||
{
|
||||
return (bool)nameRequested;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AssertLogDoesNotContainCriticalMessages(params string[] messages)
|
||||
{
|
||||
var log = Browser.Manage().Logs.GetLog(LogType.Browser);
|
||||
foreach (var message in messages)
|
||||
{
|
||||
Assert.DoesNotContain(log, entry =>
|
||||
{
|
||||
return entry.Level == LogLevel.Severe
|
||||
&& entry.Message.Contains(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,14 +18,21 @@
|
|||
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
|
||||
<Reference Include="Microsoft.AspNetCore.Components.Authorization" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Configuration" />
|
||||
<Reference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TestContentPackage\TestContentPackage.csproj" />
|
||||
<ProjectReference Include="..\LazyTestContentPackage\LazyTestContentPackage.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Resources.resx" GenerateSource="true" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<BlazorWebAssemblyLazyLoad Include="Newtonsoft.Json" />
|
||||
<BlazorWebAssemblyLazyLoad Include="LazyTestContentPackage" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@
|
|||
<option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option>
|
||||
<option value="BasicTestApp.RouterTest.NavigationManagerComponent">NavigationManager Test</option>
|
||||
<option value="BasicTestApp.RouterTest.TestRouter">Router</option>
|
||||
<option value="BasicTestApp.RouterTest.TestRouterWithOnNavigate">Router with OnNavigate</option>
|
||||
<option value="BasicTestApp.RouterTest.TestRouterWithLazyAssembly">Router with dynamic assembly</option>
|
||||
<option value="BasicTestApp.RouterTest.TestRouterWithAdditionalAssembly">Router with additional assembly</option>
|
||||
<option value="BasicTestApp.StringComparisonComponent">StringComparison</option>
|
||||
<option value="BasicTestApp.SvgComponent">SVG</option>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
<li><NavLink href="/subdir/WithParameters/Name/Abc/LastName/McDef">With more parameters</NavLink></li>
|
||||
<li><NavLink href="/subdir/LongPage1">Long page 1</NavLink></li>
|
||||
<li><NavLink href="/subdir/LongPage2">Long page 2</NavLink></li>
|
||||
<li><NavLink href="/subdir/WithLazyAssembly">With lazy assembly</NavLink></li>
|
||||
<li><NavLink href="PreventDefaultCases">preventDefault cases</NavLink></li>
|
||||
<li><NavLink>Null href never matches</NavLink></li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using System.Reflection
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Services
|
||||
|
||||
@inject LazyAssemblyLoader lazyLoader
|
||||
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" AdditionalAssemblies="@lazyLoadedAssemblies" OnNavigateAsync="@OnNavigateAsync">
|
||||
<Navigating>
|
||||
<div style="padding: 20px;background-color:blue;color:white;" id="loading-banner">
|
||||
<p>Loading the requested page...</p>
|
||||
</div>
|
||||
</Navigating>
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(RouterTestLayout)">
|
||||
<div id="test-info">Oops, that component wasn't found!</div>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
||||
@code {
|
||||
private List<Assembly> lazyLoadedAssemblies = new List<Assembly>();
|
||||
|
||||
private async Task OnNavigateAsync(NavigationContext args)
|
||||
{
|
||||
Console.WriteLine($"Running OnNavigate for {args.Path}...");
|
||||
await LoadAssemblies(args.Path);
|
||||
}
|
||||
|
||||
private async Task LoadAssemblies(string uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Error when loading assemblies: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
@using Microsoft.AspNetCore.Components.Routing
|
||||
|
||||
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" OnNavigateAsync="@OnNavigateAsync">
|
||||
<Navigating>
|
||||
<div style="padding: 20px;background-color:blue;color:white;" id="loading-banner">
|
||||
<p>Loading the requested page...</p>
|
||||
</div>
|
||||
</Navigating>
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(RouterTestLayout)">
|
||||
<div id="test-info">Oops, that component wasn't found!</div>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
|
||||
@code {
|
||||
private Dictionary<string, Func<NavigationContext, Task>> preNavigateTasks = new Dictionary<string, Func<NavigationContext, Task>>()
|
||||
{
|
||||
{ "LongPage1", new Func<NavigationContext, Task>(TestLoadingPageShows) },
|
||||
{ "LongPage2", new Func<NavigationContext, Task>(TestOnNavCancel) }
|
||||
};
|
||||
|
||||
private async Task OnNavigateAsync(NavigationContext args)
|
||||
{
|
||||
Console.WriteLine($"Running OnNavigate for {args.Path}...");
|
||||
Func<NavigationContext, Task> task;
|
||||
if (preNavigateTasks.TryGetValue(args.Path, out task))
|
||||
{
|
||||
await task.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task TestLoadingPageShows(NavigationContext args)
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
}
|
||||
|
||||
public static async Task TestOnNavCancel(NavigationContext args)
|
||||
{
|
||||
await Task.Delay(2000, args.CancellationToken);
|
||||
Console.WriteLine("I'm not happening...");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
@page "/WithLazyAssembly"
|
||||
|
||||
@using System.Linq
|
||||
@using System.Reflection
|
||||
@using Newtonsoft.Json
|
||||
|
||||
<p>Just a webpage that uses a lazy-loaded dependency.</p>
|
||||
|
||||
<button @onclick="UsePackage" id="use-package-button">Click Me</button>
|
||||
|
||||
@code
|
||||
{
|
||||
private void UsePackage() {
|
||||
JsonConvert.DeserializeObject("{ 'type': 'Test' }");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
@page "/WithLazyLoadedRoutes"
|
||||
|
||||
<p id="lazy-load-msg">Click the button below to navigate to a route defined in a lazy-loaded component.</p>
|
||||
|
||||
<a href="LazyRouteInsidePackage" id="go-to-lazy-route">Click Me</a>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<div id="lazy-component">
|
||||
This component will be lazily loaded.
|
||||
</div>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||
<OutputType>library</OutputType>
|
||||
<StaticWebAssetBasePath>_content/TestContentPackage</StaticWebAssetBasePath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableTypeScriptNuGetTarget>true</EnableTypeScriptNuGetTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Components" />
|
||||
<Reference Include="Microsoft.AspNetCore.Components.Web" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
@page "/LazyRouteInsidePackage"
|
||||
|
||||
<div id="lazy-page">
|
||||
This page will be lazy-loaded.
|
||||
</div>
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
@using Microsoft.AspNetCore.Components.Web
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<base href="~/" />
|
||||
</head>
|
||||
<body>
|
||||
<app><component type="typeof(TestRouter)" render-mode="ServerPrerendered" /></app>
|
||||
<app><component type="typeof(TestRouterWithLazyAssembly)" render-mode="ServerPrerendered" /></app>
|
||||
|
||||
@*
|
||||
So that E2E tests can make assertions about both the prerendered and
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Hosting;
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace TestServer
|
||||
{
|
||||
|
|
@ -22,6 +24,7 @@ namespace TestServer
|
|||
services.AddMvc();
|
||||
services.AddServerSideBlazor();
|
||||
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
|
||||
services.AddSingleton<LazyAssemblyLoader>();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
|
|
|
|||
|
|
@ -37,15 +37,15 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
|||
var bootJson = ReadBootJsonData(result, Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json"));
|
||||
|
||||
// And that it has been labelled as a dynamic assembly in the boot.json
|
||||
var dynamicAssemblies = bootJson.resources.dynamicAssembly;
|
||||
var lazyAssemblies = bootJson.resources.lazyAssembly;
|
||||
var assemblies = bootJson.resources.assembly;
|
||||
|
||||
Assert.NotNull(dynamicAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", dynamicAssemblies.Keys);
|
||||
Assert.NotNull(lazyAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", lazyAssemblies.Keys);
|
||||
Assert.DoesNotContain("RazorClassLibrary.dll", assemblies.Keys);
|
||||
|
||||
// App assembly should not be lazy loaded
|
||||
Assert.DoesNotContain("blazorwasm.dll", dynamicAssemblies.Keys);
|
||||
Assert.DoesNotContain("blazorwasm.dll", lazyAssemblies.Keys);
|
||||
Assert.Contains("blazorwasm.dll", assemblies.Keys);
|
||||
}
|
||||
|
||||
|
|
@ -75,15 +75,15 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
|||
var bootJson = ReadBootJsonData(result, Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json"));
|
||||
|
||||
// And that it has been labelled as a dynamic assembly in the boot.json
|
||||
var dynamicAssemblies = bootJson.resources.dynamicAssembly;
|
||||
var lazyAssemblies = bootJson.resources.lazyAssembly;
|
||||
var assemblies = bootJson.resources.assembly;
|
||||
|
||||
Assert.NotNull(dynamicAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", dynamicAssemblies.Keys);
|
||||
Assert.NotNull(lazyAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", lazyAssemblies.Keys);
|
||||
Assert.DoesNotContain("RazorClassLibrary.dll", assemblies.Keys);
|
||||
|
||||
// App assembly should not be lazy loaded
|
||||
Assert.DoesNotContain("blazorwasm.dll", dynamicAssemblies.Keys);
|
||||
Assert.DoesNotContain("blazorwasm.dll", lazyAssemblies.Keys);
|
||||
Assert.Contains("blazorwasm.dll", assemblies.Keys);
|
||||
}
|
||||
|
||||
|
|
@ -113,15 +113,15 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
|||
var bootJson = ReadBootJsonData(result, Path.Combine(publishDirectory, "wwwroot", "_framework", "blazor.boot.json"));
|
||||
|
||||
// And that it has been labelled as a dynamic assembly in the boot.json
|
||||
var dynamicAssemblies = bootJson.resources.dynamicAssembly;
|
||||
var lazyAssemblies = bootJson.resources.lazyAssembly;
|
||||
var assemblies = bootJson.resources.assembly;
|
||||
|
||||
Assert.NotNull(dynamicAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", dynamicAssemblies.Keys);
|
||||
Assert.NotNull(lazyAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", lazyAssemblies.Keys);
|
||||
Assert.DoesNotContain("RazorClassLibrary.dll", assemblies.Keys);
|
||||
|
||||
// App assembly should not be lazy loaded
|
||||
Assert.DoesNotContain("blazorwasm.dll", dynamicAssemblies.Keys);
|
||||
Assert.DoesNotContain("blazorwasm.dll", lazyAssemblies.Keys);
|
||||
Assert.Contains("blazorwasm.dll", assemblies.Keys);
|
||||
}
|
||||
|
||||
|
|
@ -151,15 +151,15 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
|||
var bootJson = ReadBootJsonData(result, Path.Combine(publishDirectory, "wwwroot", "_framework", "blazor.boot.json"));
|
||||
|
||||
// And that it has been labelled as a dynamic assembly in the boot.json
|
||||
var dynamicAssemblies = bootJson.resources.dynamicAssembly;
|
||||
var lazyAssemblies = bootJson.resources.lazyAssembly;
|
||||
var assemblies = bootJson.resources.assembly;
|
||||
|
||||
Assert.NotNull(dynamicAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", dynamicAssemblies.Keys);
|
||||
Assert.NotNull(lazyAssemblies);
|
||||
Assert.Contains("RazorClassLibrary.dll", lazyAssemblies.Keys);
|
||||
Assert.DoesNotContain("RazorClassLibrary.dll", assemblies.Keys);
|
||||
|
||||
// App assembly should not be lazy loaded
|
||||
Assert.DoesNotContain("blazorwasm.dll", dynamicAssemblies.Keys);
|
||||
Assert.DoesNotContain("blazorwasm.dll", lazyAssemblies.Keys);
|
||||
Assert.Contains("blazorwasm.dll", assemblies.Keys);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,10 +76,10 @@ namespace Microsoft.AspNetCore.Razor.Tasks
|
|||
public Dictionary<string, ResourceHashesByNameDictionary> satelliteResources { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Assembly (.dll) resources that are loaded dynamically during runtime
|
||||
/// Assembly (.dll) resources that are loaded lazily during runtime
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public ResourceHashesByNameDictionary dynamicAssembly { get; set; }
|
||||
public ResourceHashesByNameDictionary lazyAssembly { get; set; }
|
||||
}
|
||||
#pragma warning restore IDE1006 // Naming Styles
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ namespace Microsoft.AspNetCore.Razor.Tasks
|
|||
|
||||
if (IsLazyLoadedAssembly(fileName))
|
||||
{
|
||||
resourceData.dynamicAssembly ??= new ResourceHashesByNameDictionary();
|
||||
resourceList = resourceData.dynamicAssembly;
|
||||
resourceData.lazyAssembly ??= new ResourceHashesByNameDictionary();
|
||||
resourceList = resourceData.lazyAssembly;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(resourceCulture))
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue