Procházet zdrojové kódy

[Blazor] Try to load the assembly when finding out a root component or a parameter in webassembly (#52331) (#52351)

# Load assembly when finding root component or parameter in WebAssembly

This change allows the system to load the assembly when a root component is defined in an RCL and rendered in WebAssembly, even if the assembly is not yet loaded into memory.

## Description

When a root component is defined in an RCL and rendered in WebAssembly, it might happen that the assembly for the component is not yet loaded into memory if the app has not used any type from the dll.
This causes the framework to fail finding the assembly when it tries to look for it in the list of loaded assemblies.

The fix is to detect this in webassembly and try to load the root component type at that point. Same with the component parameters.

Fixes #52129

## Customer Impact

A Blazor Web app may experience a failure trying to render a root component in WebAssembly, when the component is defined in a Razor Class Library. This will happen if the assembly containing the component hasn't yet been loaded on the client, and the app is trying to render that component.

## Regression?

- [ ] Yes
- [x] No

## Risk

- [ ] High
- [ ] Medium
- [x] Low

The assembly will already be downloaded by JS and is ready to be loaded.

## Verification

- [x] Manual (required)
- [x] Automated

There was manual verification that the fix addressed the issue and it was automated into an E2E test.

## Packaging changes reviewed?

- [ ] Yes
- [ ] No
- [x] N/A

There are no packaging changes associated with this fix.

## When servicing release/2.1

- [ ] Make necessary changes in eng/PatchConfig.props
Javier Calvarro Nelson před 2 roky
rodič
revize
8bf4d2de20

+ 19 - 0
AspNetCore.sln

@@ -1780,6 +1780,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Output
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OutputCaching.StackExchangeRedis", "src\Middleware\Microsoft.AspNetCore.OutputCaching.StackExchangeRedis\src\Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.csproj", "{F232B503-D412-45EE-8B31-EFD46B9FA302}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotReferencedInWasmCodePackage", "src\Components\test\testassets\NotReferencedInWasmCodePackage\NotReferencedInWasmCodePackage.csproj", "{433F91E4-E39D-4EB0-B798-2998B3969A2C}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -10717,6 +10719,22 @@ Global
 		{F232B503-D412-45EE-8B31-EFD46B9FA302}.Release|x64.Build.0 = Release|Any CPU
 		{F232B503-D412-45EE-8B31-EFD46B9FA302}.Release|x86.ActiveCfg = Release|Any CPU
 		{F232B503-D412-45EE-8B31-EFD46B9FA302}.Release|x86.Build.0 = Release|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Debug|arm64.ActiveCfg = Debug|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Debug|arm64.Build.0 = Debug|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Debug|x64.Build.0 = Debug|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Debug|x86.Build.0 = Debug|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Release|Any CPU.Build.0 = Release|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Release|arm64.ActiveCfg = Release|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Release|arm64.Build.0 = Release|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Release|x64.ActiveCfg = Release|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Release|x64.Build.0 = Release|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Release|x86.ActiveCfg = Release|Any CPU
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -11596,6 +11614,7 @@ Global
 		{CAEB7F57-28A8-451C-95D0-45FCAA3C726C} = {C445B129-0A4D-41F5-8347-6534B6B12303}
 		{A939893A-B3CD-48F6-80D3-340C8A6E275B} = {AA5ABFBC-177C-421E-B743-005E0FD1248B}
 		{F232B503-D412-45EE-8B31-EFD46B9FA302} = {AA5ABFBC-177C-421E-B743-005E0FD1248B}
+		{433F91E4-E39D-4EB0-B798-2998B3969A2C} = {6126DCE4-9692-4EE2-B240-C65743572995}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

+ 1 - 0
src/Components/ComponentsNoDeps.slnf

@@ -54,6 +54,7 @@
       "src\\Components\\test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
       "src\\Components\\test\\testassets\\GlobalizationWasmApp\\GlobalizationWasmApp.csproj",
       "src\\Components\\test\\testassets\\LazyTestContentPackage\\LazyTestContentPackage.csproj",
+      "src\\Components\\test\\testassets\\NotReferencedInWasmCodePackage\\NotReferencedInWasmCodePackage.csproj",
       "src\\Components\\test\\testassets\\TestContentPackage\\TestContentPackage.csproj"
     ]
   }

+ 18 - 2
src/Components/Shared/src/ComponentParametersTypeCache.cs

@@ -40,10 +40,26 @@ internal sealed class ComponentParametersTypeCache
 
         if (assembly == null)
         {
-            return null;
+            // It might be that the assembly is not loaded yet, this can happen if the root component is defined in a
+            // different assembly than the app and there is no reference from the app assembly to any type in the class
+            // library that has been used yet.
+            // In this case, try and load the assembly and look up the type again.
+            // We only need to do this in the browser because its a different process, in the server the assembly will already
+            // be loaded.
+            if (OperatingSystem.IsBrowser())
+            {
+                try
+                {
+                    assembly = Assembly.Load(key.Assembly);
+                }
+                catch
+                {
+                    // It's fine to ignore the exception, since we'll return null below.
+                }
+            }
         }
 
-        return assembly.GetType(key.Type, throwOnError: false, ignoreCase: false);
+        return assembly?.GetType(key.Type, throwOnError: false, ignoreCase: false);
     }
 
     private struct Key : IEquatable<Key>

+ 18 - 2
src/Components/Shared/src/RootComponentTypeCache.cs

@@ -41,10 +41,26 @@ internal sealed class RootComponentTypeCache
 
         if (assembly == null)
         {
-            return null;
+            // It might be that the assembly is not loaded yet, this can happen if the root component is defined in a
+            // different assembly than the app and there is no reference from the app assembly to any type in the class
+            // library that has been used yet.
+            // In this case, try and load the assembly and look up the type again.
+            // We only need to do this in the browser because its a different process, in the server the assembly will already
+            // be loaded.
+            if (OperatingSystem.IsBrowser())
+            {
+                try
+                {
+                    assembly = Assembly.Load(key.Assembly);
+                }
+                catch
+                {
+                    // It's fine to ignore the exception, since we'll return null below.
+                }
+            }
         }
 
-        return assembly.GetType(key.Type, throwOnError: false, ignoreCase: false);
+        return assembly?.GetType(key.Type, throwOnError: false, ignoreCase: false);
     }
 
     private readonly struct Key : IEquatable<Key>

+ 9 - 0
src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs

@@ -68,6 +68,15 @@ public class InteractivityTest : ServerTestBase<BasicTestAppServerSiteFixture<Ra
         Browser.Equal("4", () => Browser.FindElement(By.Id("count-wasm-shared")).Text);
     }
 
+    [Fact]
+    public void CanRenderInteractiveWebAssemblyComponentFromRazorClassLibraryThatIsNotExplicitlyReferenced()
+    {
+        Navigate($"{ServerPathBase}/not-explicitly-referenced-in-wasm-code");
+
+        // The element with id success is only rendered when webassembly has successfully loaded the component.
+        Browser.Exists(By.Id("success"));
+    }
+
     [Fact]
     public void CanRenderInteractiveServerAndWebAssemblyComponentsAtTheSameTime()
     {

+ 5 - 0
src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/RenderComponentWasmFromRazorClassLibrary.razor

@@ -0,0 +1,5 @@
+@page "/not-explicitly-referenced-in-wasm-code"
+@using NotReferencedInWasmCodePackage
+<h3>RenderComponentWasmFromRazorClassLibrary</h3>
+
+<NotExplicitlyLoadedFromWasmCode @rendermode="RenderMode.InteractiveWebAssembly" />

+ 1 - 0
src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj

@@ -12,6 +12,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\NotReferencedInWasmCodePackage\NotReferencedInWasmCodePackage.csproj" />
     <ProjectReference Include="..\TestContentPackage\TestContentPackage.csproj" />
   </ItemGroup>
 

+ 0 - 3
src/Components/test/testassets/Components.WasmMinimal/Program.cs

@@ -1,12 +1,9 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Reflection;
 using Components.TestServer.Services;
 using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
 
-Assembly.Load(nameof(TestContentPackage));
-
 var builder = WebAssemblyHostBuilder.CreateDefault(args);
 builder.Services.AddSingleton<AsyncOperationService>();
 

+ 16 - 0
src/Components/test/testassets/NotReferencedInWasmCodePackage/NotExplicitlyLoadedFromWasmCode.razor

@@ -0,0 +1,16 @@
+<h3>NotExplicitlyLoadedFromWasmCode</h3>
+
+<p>Represents a component that wasn't explicitly loaded from webassembly code by a direct type reference.
+</p>
+
+<p>Instead, this component is only rendered in webassembly mode from the server and not loaded into the application domain until we explicitly load it via a call to Assembly.Load when we try to render the root component.
+</p>
+
+@if(OperatingSystem.IsBrowser())
+{
+    <p id="success">Running in webassembly mode.</p>
+}
+else
+{
+    <p>Running in server mode.</p>
+}

+ 14 - 0
src/Components/test/testassets/NotReferencedInWasmCodePackage/NotReferencedInWasmCodePackage.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk.Razor">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <OutputType>library</OutputType>
+    <StaticWebAssetBasePath>_content/NotReferencedInWasmCodePackage</StaticWebAssetBasePath>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Components" />
+    <Reference Include="Microsoft.AspNetCore.Components.Web" />
+  </ItemGroup>
+
+</Project>