diff --git a/AspNetCore.sln b/AspNetCore.sln
index aea9643f478f981430198697cef78ca9019c2a76..2f12c219cd0deb87fe56cae4fc8c26192662d49a 100644
--- a/AspNetCore.sln
+++ b/AspNetCore.sln
@@ -1427,6 +1427,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.Web.Extensions.Tests", "src\Components\Web.Extensions\test\Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj", "{157605CB-5170-4C1A-980F-4BAE42DB60DE}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sdk", "Sdk", "{FED4267E-E5E4-49C5-98DB-8B3F203596EE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly", "src\Components\WebAssembly\Sdk\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj", "{6B2734BF-C61D-4889-ABBF-456A4075D59B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tests", "src\Components\WebAssembly\Sdk\test\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj", "{83371889-9A3E-4D16-AE77-EB4F83BC6374}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests", "src\Components\WebAssembly\Sdk\integrationtests\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj", "{525EBCB4-A870-470B-BC90-845306C337D1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tools", "src\Components\WebAssembly\Sdk\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj", "{175E5CD8-92D4-46BB-882E-3A930D3302D4}"
+EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{6126DCE4-9692-4EE2-B240-C65743572995}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicTestApp", "src\Components\test\testassets\BasicTestApp\BasicTestApp.csproj", "{46FB7E93-1294-4068-B80A-D4864F78277A}"
@@ -6743,6 +6753,54 @@ Global
 		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x64.Build.0 = Release|Any CPU
 		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.ActiveCfg = Release|Any CPU
 		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.Build.0 = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x64.Build.0 = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x86.Build.0 = Debug|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x64.ActiveCfg = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x64.Build.0 = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x86.ActiveCfg = Release|Any CPU
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x86.Build.0 = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x64.Build.0 = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x86.Build.0 = Debug|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|Any CPU.Build.0 = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x64.ActiveCfg = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x64.Build.0 = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x86.ActiveCfg = Release|Any CPU
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x86.Build.0 = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x64.Build.0 = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x86.Build.0 = Debug|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|Any CPU.Build.0 = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|x64.ActiveCfg = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|x64.Build.0 = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|x86.ActiveCfg = Release|Any CPU
+		{525EBCB4-A870-470B-BC90-845306C337D1}.Release|x86.Build.0 = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x64.Build.0 = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x86.Build.0 = Debug|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x64.ActiveCfg = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x64.Build.0 = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.ActiveCfg = Release|Any CPU
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.Build.0 = Release|Any CPU
 		{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -7530,6 +7588,11 @@ Global
 		{F71FE795-9923-461B-9809-BB1821A276D0} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
 		{8294A74F-7DAA-4B69-BC56-7634D93C9693} = {F71FE795-9923-461B-9809-BB1821A276D0}
 		{157605CB-5170-4C1A-980F-4BAE42DB60DE} = {F71FE795-9923-461B-9809-BB1821A276D0}
+		{FED4267E-E5E4-49C5-98DB-8B3F203596EE} = {562D5067-8CD8-4F19-BCBB-873204932C61}
+		{6B2734BF-C61D-4889-ABBF-456A4075D59B} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
+		{83371889-9A3E-4D16-AE77-EB4F83BC6374} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
+		{525EBCB4-A870-470B-BC90-845306C337D1} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
+		{175E5CD8-92D4-46BB-882E-3A930D3302D4} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
 		{6126DCE4-9692-4EE2-B240-C65743572995} = {0508E463-0269-40C9-B5C2-3B600FB2A28B}
 		{46FB7E93-1294-4068-B80A-D4864F78277A} = {6126DCE4-9692-4EE2-B240-C65743572995}
 		{25FA84DB-EEA7-4068-8E2D-F3D48B281C16} = {6126DCE4-9692-4EE2-B240-C65743572995}
diff --git a/eng/Build.props b/eng/Build.props
index 56b4917250d150b4d20c053ad50fcd9e7fcf63f6..3740546dd641018a5cd819b9d08f3fe631e94e58 100644
--- a/eng/Build.props
+++ b/eng/Build.props
@@ -172,6 +172,7 @@
                           @(ProjectToBuild);
                           @(ProjectToExclude);
                           $(RepoRoot)src\Razor\test\testassets\**\*.*proj;
+                          $(RepoRoot)src\Components\WebAssembly\Sdk\testassets\**\*.*proj;
                           $(RepoRoot)**\node_modules\**\*;
                           $(RepoRoot)**\bin\**\*;
                           $(RepoRoot)**\obj\**\*;"
diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props
index a0e89782caeeb3f292da48a893b1cefbf3b074fd..6dbfca4646bdd90fabdd0e10140852287365ab67 100644
--- a/eng/ProjectReferences.props
+++ b/eng/ProjectReferences.props
@@ -145,6 +145,7 @@
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Web.Extensions" ProjectPath="$(RepoRoot)src\Components\Web.Extensions\src\Microsoft.AspNetCore.Components.Web.Extensions.csproj"  />
     <ProjectReferenceProvider Include="Microsoft.Authentication.WebAssembly.Msal" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Authentication.Msal\src\Microsoft.Authentication.WebAssembly.Msal.csproj"  />
     <ProjectReferenceProvider Include="Microsoft.JSInterop.WebAssembly" ProjectPath="$(RepoRoot)src\Components\WebAssembly\JSInterop\src\Microsoft.JSInterop.WebAssembly.csproj"  />
+    <ProjectReferenceProvider Include="Microsoft.NET.Sdk.BlazorWebAssembly" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Sdk\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj"  />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly.Server" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Server\src\Microsoft.AspNetCore.Components.WebAssembly.Server.csproj"  />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" ProjectPath="$(RepoRoot)src\Components\WebAssembly\WebAssembly.Authentication\src\Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj"  />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly" ProjectPath="$(RepoRoot)src\Components\WebAssembly\WebAssembly\src\Microsoft.AspNetCore.Components.WebAssembly.csproj"  />
diff --git a/src/Components/Components.slnf b/src/Components/Components.slnf
index c202f7c1c69ba1b663e1059b6f9609ddf827ed8a..637ec1a3845896cb970d59e95ebec98aee088bc4 100644
--- a/src/Components/Components.slnf
+++ b/src/Components/Components.slnf
@@ -110,6 +110,9 @@
       "src\\Components\\WebAssembly\\WebAssembly.Authentication\\test\\Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests.csproj",
       "src\\Components\\WebAssembly\\testassets\\Wasm.Authentication.Client\\Wasm.Authentication.Client.csproj",
       "src\\Components\\WebAssembly\\testassets\\Wasm.Authentication.Shared\\Wasm.Authentication.Shared.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\src\\Microsoft.NET.Sdk.BlazorWebAssembly.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\test\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\integrationtests\\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj",
       "src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj"
     ]
   }
diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs
index fa4f421ab291d23303a0f5c7ab26a95ea7a06908..7d6b778d291f977dbb01d34d6f1864e74d1bf62f 100644
--- a/src/Components/Components/src/Routing/Router.cs
+++ b/src/Components/Components/src/Routing/Router.cs
@@ -4,6 +4,7 @@
 #nullable disable warnings
 
 using System;
+using System.Runtime.ExceptionServices;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
@@ -72,7 +73,7 @@ namespace Microsoft.AspNetCore.Components.Routing
         /// <summary>
         /// Gets or sets a handler that should be called before navigating to a new page.
         /// </summary>
-        [Parameter] public EventCallback<NavigationContext> OnNavigateAsync { get; set; }
+        [Parameter] public Func<NavigationContext, Task> OnNavigateAsync { get; set; }
 
         private RouteTable Routes { get; set; }
 
@@ -194,51 +195,58 @@ namespace Microsoft.AspNetCore.Components.Routing
 
         private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNavigate)
         {
-            // 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)
+            if (OnNavigateAsync == null)
             {
                 return true;
             }
 
-            // If we've already invoked a task and stored its CTS, then
-            // cancel that existing CTS.
+            // Cancel the CTS instead of disposing it, since disposing does not
+            // actually cancel and can cause unintended Object Disposed Exceptions.
+            // This effectivelly cancels the previously running task and completes it.
             _onNavigateCts?.Cancel();
-            // Then make sure that the task has been completed cancelled or
-            // completed before continuing with the execution of this current task.
+            // Then make sure that the task has been completely cancelled or completed
+            // before starting the next one. This avoid race conditions where the cancellation
+            // for the previous task was set but not fully completed by the time we get to this
+            // invocation.
             await previousOnNavigate;
 
-            // 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 cancellationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
-            navigateContext.CancellationToken.Register(state =>
-                ((TaskCompletionSource)state).SetResult(), cancellationTcs);
-
-            var task = OnNavigateAsync.InvokeAsync(navigateContext);
-
-            // If the user provided a Navigating render fragment, then show it.
-            if (Navigating != null && task.Status != TaskStatus.RanToCompletion)
+            try
+            {
+                if (Navigating != null)
+                {
+                    _renderHandle.Render(Navigating);
+                }
+                await OnNavigateAsync(navigateContext);
+                return true;
+            }
+            catch (OperationCanceledException e)
+            {
+                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;
+                }
+            }
+            catch (Exception e)
             {
-                _renderHandle.Render(Navigating);
+                _renderHandle.Render(builder => ExceptionDispatchInfo.Capture(e).Throw());
+                return false;
             }
 
-            var completedTask = await Task.WhenAny(task, cancellationTcs.Task);
-            return task == completedTask;
+            return false;
         }
 
         internal async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted)
         {
             // We cache the Task representing the previously invoked RunOnNavigateWithRefreshAsync
-            // that is stored
+            // that is stored. Then we create a new one that represents our current invocation and store it
+            // globally for the next invocation. This allows us to check inside `RunOnNavigateAsync` if the
+            // previous OnNavigateAsync task has fully completed before starting the next one.
             var previousTask = _previousOnNavigateTask;
-            // Then we create a new one that represents our current invocation and store it
-            // globally for the next invocation. Note to the developer, if the WASM runtime
-            // support multi-threading then we'll need to implement the appropriate locks
-            // here to ensure that the cached previous task is overwritten incorrectly.
             var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             _previousOnNavigateTask = tcs.Task;
             try
diff --git a/src/Components/Components/test/Routing/RouterTest.cs b/src/Components/Components/test/Routing/RouterTest.cs
index 7e8a78b382476070a37350e6cfc0c225e65d38b7..62569d5c569c5334e3f4d0b04fe9b334ed3111a2 100644
--- a/src/Components/Components/test/Routing/RouterTest.cs
+++ b/src/Components/Components/test/Routing/RouterTest.cs
@@ -2,103 +2,253 @@
 // 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.Linq;
 using System.Reflection;
 using System.Threading;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.Routing;
 using Microsoft.AspNetCore.Components.Test.Helpers;
-using Microsoft.Extensions.DependencyModel;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
 using Moq;
 using Xunit;
+using Microsoft.AspNetCore.Components;
 
 namespace Microsoft.AspNetCore.Components.Test.Routing
 {
     public class RouterTest
     {
+        private readonly Router _router;
+        private readonly TestRenderer _renderer;
+
+        public RouterTest()
+        {
+            var services = new ServiceCollection();
+            services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
+            services.AddSingleton<NavigationManager, TestNavigationManager>();
+            services.AddSingleton<INavigationInterception, TestNavigationInterception>();
+            var serviceProvider = services.BuildServiceProvider();
+
+            _renderer = new TestRenderer(serviceProvider);
+            _renderer.ShouldHandleExceptions = true;
+            _router = (Router)_renderer.InstantiateComponent<Router>();
+            _router.AppAssembly = Assembly.GetExecutingAssembly();
+            _router.Found = routeData => (builder) => builder.AddContent(0, "Rendering route...");
+            _renderer.AssignRootComponentId(_router);
+        }
+
         [Fact]
         public async Task CanRunOnNavigateAsync()
         {
             // Arrange
-            var router = CreateMockRouter();
             var called = false;
             async Task OnNavigateAsync(NavigationContext args)
             {
                 await Task.CompletedTask;
                 called = true;
             }
-            router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
+            _router.OnNavigateAsync = OnNavigateAsync;
 
             // Act
-            await router.Object.RunOnNavigateWithRefreshAsync("http://example.com/jan", false);
+            await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
 
             // Assert
             Assert.True(called);
         }
 
         [Fact]
-        public async Task CanCancelPreviousOnNavigateAsync()
+        public async Task CanHandleSingleFailedOnNavigateAsync()
+        {
+            // Arrange
+            var called = false;
+            async Task OnNavigateAsync(NavigationContext args)
+            {
+                called = true;
+                await Task.CompletedTask;
+                throw new Exception("This is an uncaught exception.");
+            }
+            _router.OnNavigateAsync = OnNavigateAsync;
+
+            // Act
+            await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
+
+            // Assert
+            Assert.True(called);
+            Assert.Single(_renderer.HandledExceptions);
+            var unhandledException = _renderer.HandledExceptions[0];
+            Assert.Equal("This is an uncaught exception.", unhandledException.Message);
+        }
+
+        [Fact]
+        public async Task CanceledFailedOnNavigateAsyncDoesNothing()
+        {
+            // Arrange
+            var onNavigateInvoked = 0;
+            async Task OnNavigateAsync(NavigationContext args)
+            {
+                onNavigateInvoked += 1;
+                if (args.Path.EndsWith("jan"))
+                {
+                    await Task.Delay(Timeout.Infinite, args.CancellationToken);
+                    throw new Exception("This is an uncaught exception.");
+                }
+            }
+            var refreshCalled = false;
+            _renderer.OnUpdateDisplay = (renderBatch) =>
+            {
+                if (!refreshCalled)
+                {
+                    refreshCalled = true;
+                    return;
+                }
+                Assert.True(false, "OnUpdateDisplay called more than once.");
+            };
+            _router.OnNavigateAsync = OnNavigateAsync;
+
+            // Act
+            var janTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
+            var febTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false));
+
+            await janTask;
+            await febTask;
+
+            // Assert that we render the second route component and don't throw an exception
+            Assert.Empty(_renderer.HandledExceptions);
+            Assert.Equal(2, onNavigateInvoked);
+        }
+
+        [Fact]
+        public async Task CanHandleSingleCancelledOnNavigateAsync()
+        {
+            // Arrange
+            async Task OnNavigateAsync(NavigationContext args)
+            {
+                var tcs = new TaskCompletionSource<int>();
+                tcs.TrySetCanceled();
+                await tcs.Task;
+            }
+            _renderer.OnUpdateDisplay = (renderBatch) => Assert.True(false, "OnUpdateDisplay called more than once.");
+            _router.OnNavigateAsync = OnNavigateAsync;
+
+            // Act
+            await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
+
+            // Assert
+            Assert.Single(_renderer.HandledExceptions);
+            var unhandledException = _renderer.HandledExceptions[0];
+            Assert.Equal("OnNavigateAsync can only be cancelled via NavigateContext.CancellationToken.", unhandledException.Message);
+        }
+
+        [Fact]
+        public async Task AlreadyCanceledOnNavigateAsyncDoesNothing()
+        {
+            // Arrange
+            var triggerCancel = new TaskCompletionSource();
+            async Task OnNavigateAsync(NavigationContext args)
+            {
+                if (args.Path.EndsWith("jan"))
+                {
+                    var tcs = new TaskCompletionSource();
+                    await triggerCancel.Task;
+                    tcs.TrySetCanceled();
+                    await tcs.Task;
+                }
+            }
+            var refreshCalled = false;
+            _renderer.OnUpdateDisplay = (renderBatch) =>
+            {
+                if (!refreshCalled)
+                {
+                    Assert.True(true);
+                    return;
+                }
+                Assert.True(false, "OnUpdateDisplay called more than once.");
+            };
+            _router.OnNavigateAsync = OnNavigateAsync;
+
+            // Act (start the operations then await them)
+            var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
+            var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false));
+            triggerCancel.TrySetResult();
+
+            await jan;
+            await feb;
+        }
+
+        [Fact]
+        public void CanCancelPreviousOnNavigateAsync()
         {
             // Arrange
-            var router = CreateMockRouter();
             var cancelled = "";
             async Task OnNavigateAsync(NavigationContext args)
             {
                 await Task.CompletedTask;
                 args.CancellationToken.Register(() => cancelled = args.Path);
             };
-            router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
+            _router.OnNavigateAsync = OnNavigateAsync;
 
             // Act
-            await router.Object.RunOnNavigateWithRefreshAsync("jan", false);
-            await router.Object.RunOnNavigateWithRefreshAsync("feb", false);
+            _ = _router.RunOnNavigateWithRefreshAsync("jan", false);
+            _ = _router.RunOnNavigateWithRefreshAsync("feb", false);
 
             // Assert
             var expected = "jan";
-            Assert.Equal(cancelled, expected);
+            Assert.Equal(expected, cancelled);
         }
 
         [Fact]
         public async Task RefreshesOnceOnCancelledOnNavigateAsync()
         {
             // Arrange
-            var router = CreateMockRouter();
             async Task OnNavigateAsync(NavigationContext args)
             {
                 if (args.Path.EndsWith("jan"))
                 {
-                    await Task.Delay(Timeout.Infinite);
+                    await Task.Delay(Timeout.Infinite, args.CancellationToken);
+                }
+            };
+            var refreshCalled = false;
+            _renderer.OnUpdateDisplay = (renderBatch) =>
+            {
+                if (!refreshCalled)
+                {
+                    Assert.True(true);
+                    return;
                 }
+                Assert.True(false, "OnUpdateDisplay called more than once.");
             };
-            router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync);
+            _router.OnNavigateAsync = OnNavigateAsync;
 
             // Act
-            var janTask = router.Object.RunOnNavigateWithRefreshAsync("jan", false);
-            var febTask = router.Object.RunOnNavigateWithRefreshAsync("feb", false);
+            var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false));
+            var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false));
 
-            var janTaskException = await Record.ExceptionAsync(() => janTask);
-            var febTaskException = await Record.ExceptionAsync(() => febTask);
-
-            // Assert neither execution threw an exception
-            Assert.Null(janTaskException);
-            Assert.Null(febTaskException);
-            // Assert refresh should've only been called once for the second route
-            router.Verify(x => x.Refresh(false), Times.Once());
+            await jan;
+            await feb;
         }
 
-        private Mock<Router> CreateMockRouter()
+        internal class TestNavigationManager : NavigationManager
         {
-            var router = new Mock<Router>() { CallBase = true };
-            router.Setup(x => x.Refresh(It.IsAny<bool>())).Verifiable();
-            return router;
+            public TestNavigationManager() =>
+                Initialize("https://www.example.com/subdir/", "https://www.example.com/subdir/jan");
+
+            protected override void NavigateToCore(string uri, bool forceLoad) => throw new NotImplementedException();
         }
 
-        [Route("jan")]
-        private class JanComponent : ComponentBase { }
+        internal sealed class TestNavigationInterception : INavigationInterception
+        {
+            public static readonly TestNavigationInterception Instance = new TestNavigationInterception();
+
+            public Task EnableNavigationInterceptionAsync()
+            {
+                return Task.CompletedTask;
+            }
+        }
 
         [Route("feb")]
-        private class FebComponent : ComponentBase { }
+        public class FebComponent : ComponentBase { }
+
+        [Route("jan")]
+        public class JanComponent : ComponentBase { }
     }
 }
diff --git a/src/Components/ComponentsNoDeps.slnf b/src/Components/ComponentsNoDeps.slnf
index 6dbf031fd31fcfddd2fc877481aafda6cc5127af..dd08970d02a727c53d7f756a5ec88e01ab0a0ab1 100644
--- a/src/Components/ComponentsNoDeps.slnf
+++ b/src/Components/ComponentsNoDeps.slnf
@@ -31,6 +31,10 @@
       "src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj",
       "src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Server\\HostedInAspNet.Server.csproj",
       "src\\Components\\WebAssembly\\testassets\\StandaloneApp\\StandaloneApp.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\src\\Microsoft.NET.Sdk.BlazorWebAssembly.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\test\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\tools\\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj",
+      "src\\Components\\WebAssembly\\Sdk\\integrationtests\\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj",
       "src\\Components\\Web\\src\\Microsoft.AspNetCore.Components.Web.csproj",
       "src\\Components\\Web\\test\\Microsoft.AspNetCore.Components.Web.Tests.csproj",
       "src\\Components\\benchmarkapps\\Wasm.Performance\\Driver\\Wasm.Performance.Driver.csproj",
diff --git a/src/Components/Directory.Build.props b/src/Components/Directory.Build.props
index 1704a1b070a8e260ab73933c7bb3313fc0db6d96..b1ea764edd5c572387001f038db379ca5c942b6b 100644
--- a/src/Components/Directory.Build.props
+++ b/src/Components/Directory.Build.props
@@ -1,14 +1,6 @@
 <Project>
   <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
 
-  <PropertyGroup>
-    <!-- Workaround for https://github.com/dotnet/aspnetcore/issues/5486 which requires the bin and obj directory be in the project directory -->
-    <BaseIntermediateOutputPath />
-    <IntermediateOutputPath />
-    <BaseOutputPath />
-    <OutputPath />
-  </PropertyGroup>
-
   <PropertyGroup>
     <EnableTypeScriptNuGetTarget>true</EnableTypeScriptNuGetTarget>
   </PropertyGroup>
diff --git a/src/Components/Directory.Build.targets b/src/Components/Directory.Build.targets
index 69204d61a66078921e771e76d62ebf2f27f7fee1..f3143253af19347287e8ea350f54357669e55456 100644
--- a/src/Components/Directory.Build.targets
+++ b/src/Components/Directory.Build.targets
@@ -3,7 +3,7 @@
     <BlazorWebAssemblyJSPath>$(RepoRoot)src\Components\Web.JS\dist\$(Configuration)\blazor.webassembly.js</BlazorWebAssemblyJSPath>
     <BlazorWebAssemblyJSMapPath>$(BlazorWebAssemblyJSPath).map</BlazorWebAssemblyJSMapPath>
 
-    <_BlazorDevServerPath>$(RepoRoot)src/Components/WebAssembly/DevServer/src/bin/$(Configuration)/$(DefaultNetCoreTargetFramework)/blazor-devserver.dll</_BlazorDevServerPath>
+    <_BlazorDevServerPath>$(ArtifactsDir)/bin/Microsoft.AspNetCore.Components.WebAssembly.DevServer/$(Configuration)/$(DefaultNetCoreTargetFramework)/blazor-devserver.dll</_BlazorDevServerPath>
     <RunCommand>dotnet</RunCommand>
     <RunArguments>exec &quot;$(_BlazorDevServerPath)&quot; serve --applicationpath &quot;$(TargetPath)&quot; $(AdditionalRunArguments)</RunArguments>
   </PropertyGroup>
diff --git a/src/Components/WebAssembly/Sdk/integrationtests/Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj b/src/Components/WebAssembly/Sdk/integrationtests/Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..4255ce7c73eb80b65e6b4639cbb4c4e899bef536
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/integrationtests/Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj
@@ -0,0 +1,78 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <!--
+      There's not much value in multi-targeting here, this doesn't run much .NET code, it tests MSBuild.
+
+      This is also a partial workaround for https://github.com/Microsoft/msbuild/issues/2661 - this project
+      has netcoreapp dependencies that need to be built first.
+    -->
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <PreserveCompilationContext>true</PreserveCompilationContext>
+
+    <!-- Tests do not work on Helix yet -->
+    <BuildHelixPayload>false</BuildHelixPayload>
+    <TestAppsRoot>$(MSBuildProjectDirectory)\..\testassets\</TestAppsRoot>
+  </PropertyGroup>
+
+  <Import Project="$(SharedSourceRoot)MSBuild.Testing\MSBuild.Testing.targets" />
+
+  <ItemGroup>
+    <None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.Build.Utilities.Core" />
+    <Reference Include="Microsoft.Extensions.DependencyModel" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="$(RepoRoot)src\Razor\test\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.csproj" />
+
+    <ProjectReference Include="..\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj">
+      <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+      <SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
+    </ProjectReference>
+
+    <ProjectReference Include="$(RepoRoot)src\Razor\Microsoft.NET.Sdk.Razor\src\Microsoft.NET.Sdk.Razor.csproj">
+      <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
+      <SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
+    </ProjectReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="..\src\BootJsonData.cs" LinkBase="Wasm" />
+    <Compile Include="..\src\AssetsManifestFile.cs" LinkBase="Wasm" />
+    <Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" />
+  </ItemGroup>
+
+  <Target Name="GenerateTestData" BeforeTargets="GetAssemblyAttributes">
+    <Exec Condition="'$(OS)' == 'Windows_NT'" Command="&quot;$(NuGetPackageRoot)vswhere\$(VSWhereVersion)\tools\vswhere.exe&quot; -latest -prerelease -property installationPath -requires Microsoft.Component.MSBuild" ConsoleToMsBuild="true" StandardErrorImportance="high">
+      <Output TaskParameter="ConsoleOutput" PropertyName="_VSInstallDir" />
+    </Exec>
+    <Error Condition="'$(OS)' == 'Windows_NT' and '$(_VSInstallDir)'=='' and '$(Test)' == 'true'" Text="Visual Studio not found on Windows." />
+
+    <PropertyGroup>
+      <_DesktopMSBuildPath Condition="'$(OS)' == 'Windows_NT' and Exists('$(_VSInstallDir)\MSBuild\Current\Bin\msbuild.exe')">$(_VSInstallDir)\MSBuild\Current\Bin\msbuild.exe</_DesktopMSBuildPath>
+      <_DesktopMSBuildPath Condition="'$(OS)' == 'Windows_NT' and Exists('$(_VSInstallDir)\MSBuild\15.0\Bin\msbuild.exe')">$(_VSInstallDir)\MSBuild\15.0\Bin\msbuild.exe</_DesktopMSBuildPath>
+    </PropertyGroup>
+
+    <Error Condition="'$(OS)' == 'Windows_NT' and '$(_DesktopMSBuildPath)'=='' and '$(Test)' == 'true'" Text="MSBuild.exe not found on Windows." />
+
+    <ItemGroup>
+      <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+        <_Parameter1>DesktopMSBuildPath</_Parameter1>
+        <_Parameter2>$(_DesktopMSBuildPath)</_Parameter2>
+      </AssemblyAttribute>
+    </ItemGroup>
+  </Target>
+
+  <Target Name="RestoreTestProjects" BeforeTargets="Restore;Build" Condition="'$(DotNetBuildFromSource)' != 'true'">
+    <MSBuild Projects="..\testassets\RestoreBlazorWasmTestProjects\RestoreBlazorWasmTestProjects.csproj" Targets="Restore" Properties="MicrosoftNetCompilersToolsetPackageVersion=$(MicrosoftNetCompilersToolsetPackageVersion);RepoRoot=$(RepoRoot)" />
+  </Target>
+
+  <Target Name="EnsureLogFolder" AfterTargets="Build">
+    <MakeDir Directories="$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'log', '$(_BuildConfig)'))" />
+  </Target>
+
+</Project>
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/ServiceWorkerAssert.cs b/src/Components/WebAssembly/Sdk/integrationtests/ServiceWorkerAssert.cs
similarity index 98%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/ServiceWorkerAssert.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/ServiceWorkerAssert.cs
index 7585b5b76ca6f05731fb7e2f104894cfde1ffcc9..8597d104805ae0b4c4f1bf8df4d3c10721f99ea1 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/ServiceWorkerAssert.cs
+++ b/src/Components/WebAssembly/Sdk/integrationtests/ServiceWorkerAssert.cs
@@ -6,7 +6,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Text.Json;
-using Microsoft.AspNetCore.Razor.Tasks;
+using Microsoft.NET.Sdk.BlazorWebAssembly;
 
 namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
 {
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIncrementalismTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIncrementalismTest.cs
similarity index 99%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIncrementalismTest.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIncrementalismTest.cs
index f91dd03863e43c087e7d537b105febeda3a4779b..218567fcc543dcc3d89556699922174ab937fb90 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIncrementalismTest.cs
+++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIncrementalismTest.cs
@@ -4,7 +4,7 @@
 using System.IO;
 using System.Text.Json;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Razor.Tasks;
+using Microsoft.NET.Sdk.BlazorWebAssembly;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIntegrationTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs
similarity index 99%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIntegrationTest.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs
index 12fbbd038048a724a06f7c451bdb8d546888f48c..c521c6be7e9a104f2b92bc1d7f3ddc11ea2e5985 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIntegrationTest.cs
+++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs
@@ -4,7 +4,7 @@
 using System.IO;
 using System.Text.Json;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Razor.Tasks;
+using Microsoft.NET.Sdk.BlazorWebAssembly;
 using Microsoft.AspNetCore.Testing;
 using Xunit;
 
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildLazyLoadTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildLazyLoadTest.cs
similarity index 99%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildLazyLoadTest.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmBuildLazyLoadTest.cs
index e8f1a02d40f4f1009069b90443422cea30ed8277..424caeda2d8088c88a4fd48eed1d119b4991bd40 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildLazyLoadTest.cs
+++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildLazyLoadTest.cs
@@ -4,7 +4,7 @@
 using System.IO;
 using System.Text.Json;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Razor.Tasks;
+using Microsoft.NET.Sdk.BlazorWebAssembly;
 using Xunit;
 
 namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmCompressionTests.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmCompressionTests.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmCompressionTests.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmCompressionTests.cs
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPublishIntegrationTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs
similarity index 98%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPublishIntegrationTest.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs
index 45c878ea5aa93d389bce6b4f5d929e84123a9bd7..1108d0bff94e2f5675ebf4937934a7e033eb4d9f 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPublishIntegrationTest.cs
+++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs
@@ -5,7 +5,7 @@ using System.IO;
 using System.IO.Compression;
 using System.Text.Json;
 using System.Threading.Tasks;
-using Microsoft.AspNetCore.Razor.Tasks;
+using Microsoft.NET.Sdk.BlazorWebAssembly;
 using Microsoft.AspNetCore.Testing;
 using Xunit;
 using static Microsoft.AspNetCore.Razor.Design.IntegrationTests.ServiceWorkerAssert;
@@ -608,6 +608,17 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
                 assetsManifestPath: "custom-service-worker-assets.js");
         }
 
+        [Fact]
+        public async Task Publish_HostedApp_WithRidSpecifiedInCLI_Works()
+        {
+            // Arrange
+            using var project = ProjectDirectory.Create("blazorhosted-rid", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", });
+            project.RuntimeIdentifier = "linux-x64";
+            var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:RuntimeIdentifier=linux-x64");
+
+            AssertRIDPublishOuput(project, result);
+        }
+
         [Fact]
         public async Task Publish_HostedApp_WithRid_Works()
         {
@@ -616,6 +627,11 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
             project.RuntimeIdentifier = "linux-x64";
             var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish");
 
+            AssertRIDPublishOuput(project, result);
+        }
+
+        private static void AssertRIDPublishOuput(ProjectDirectory project, MSBuildResult result)
+        {
             Assert.BuildPassed(result);
 
             var publishDirectory = project.PublishOutputDirectory;
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPwaManifestTests.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmPwaManifestTests.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPwaManifestTests.cs
rename to src/Components/WebAssembly/Sdk/integrationtests/WasmPwaManifestTests.cs
diff --git a/src/Components/WebAssembly/Sdk/integrationtests/xunit.runner.json b/src/Components/WebAssembly/Sdk/integrationtests/xunit.runner.json
new file mode 100644
index 0000000000000000000000000000000000000000..d00e4ae907442030f89d8b5a4a9a7ab6b5c5ba88
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/integrationtests/xunit.runner.json
@@ -0,0 +1,5 @@
+{
+  "methodDisplay": "method",
+  "shadowCopy": false,
+  "maxParallelThreads": -1
+}
\ No newline at end of file
diff --git a/src/Components/WebAssembly/Sdk/src/AssetsManifestFile.cs b/src/Components/WebAssembly/Sdk/src/AssetsManifestFile.cs
new file mode 100644
index 0000000000000000000000000000000000000000..872b17aae220aaee34715feca27a0eb0d99a8a98
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/AssetsManifestFile.cs
@@ -0,0 +1,33 @@
+// 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.
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+#pragma warning disable IDE1006 // Naming Styles
+    public class AssetsManifestFile
+    {
+        /// <summary>
+        /// Gets or sets a version string.
+        /// </summary>
+        public string version { get; set; }
+
+        /// <summary>
+        /// Gets or sets the assets. Keys are URLs; values are base-64-formatted SHA256 content hashes.
+        /// </summary>
+        public AssetsManifestFileEntry[] assets { get; set; }
+    }
+
+    public class AssetsManifestFileEntry
+    {
+        /// <summary>
+        /// Gets or sets the asset URL. Normally this will be relative to the application's base href.
+        /// </summary>
+        public string url { get; set; }
+
+        /// <summary>
+        /// Gets or sets the file content hash. This should be the base-64-formatted SHA256 value.
+        /// </summary>
+        public string hash { get; set; }
+    }
+#pragma warning restore IDE1006 // Naming Styles
+}
diff --git a/src/Components/WebAssembly/Sdk/src/BlazorReadSatelliteAssemblyFile.cs b/src/Components/WebAssembly/Sdk/src/BlazorReadSatelliteAssemblyFile.cs
new file mode 100644
index 0000000000000000000000000000000000000000..f6bf0c88f8d09c6af5c5b82b806f97d03d466e6b
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/BlazorReadSatelliteAssemblyFile.cs
@@ -0,0 +1,38 @@
+// 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.Linq;
+using System.Xml.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class BlazorReadSatelliteAssemblyFile : Task
+    {
+        [Output]
+        public ITaskItem[] SatelliteAssembly { get; set; }
+
+        [Required]
+        public ITaskItem ReadFile { get; set; }
+
+        public override bool Execute()
+        {
+            var document = XDocument.Load(ReadFile.ItemSpec);
+            SatelliteAssembly = document.Root
+                .Elements()
+                .Select(e =>
+                {
+                    // <Assembly Name="..." Culture="..." DestinationSubDirectory="..." />
+
+                    var taskItem = new TaskItem(e.Attribute("Name").Value);
+                    taskItem.SetMetadata("Culture", e.Attribute("Culture").Value);
+                    taskItem.SetMetadata("DestinationSubDirectory", e.Attribute("DestinationSubDirectory").Value);
+
+                    return taskItem;
+                }).ToArray();
+
+            return true;
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/BlazorWriteSatelliteAssemblyFile.cs b/src/Components/WebAssembly/Sdk/src/BlazorWriteSatelliteAssemblyFile.cs
new file mode 100644
index 0000000000000000000000000000000000000000..92fd8d33e874a0230a79aaa500d1f513966f96c7
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/BlazorWriteSatelliteAssemblyFile.cs
@@ -0,0 +1,53 @@
+// 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.IO;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class BlazorWriteSatelliteAssemblyFile : Task
+    {
+        [Required]
+        public ITaskItem[] SatelliteAssembly { get; set; }
+
+        [Required]
+        public ITaskItem WriteFile { get; set; }
+
+        public override bool Execute()
+        {
+            using var fileStream = File.Create(WriteFile.ItemSpec);
+            WriteSatelliteAssemblyFile(fileStream);
+            return true;
+        }
+
+        internal void WriteSatelliteAssemblyFile(Stream stream)
+        {
+            var root = new XElement("SatelliteAssembly");
+
+            foreach (var item in SatelliteAssembly)
+            {
+                // <Assembly Name="..." Culture="..." DestinationSubDirectory="..." />
+
+                root.Add(new XElement("Assembly",
+                    new XAttribute("Name", item.ItemSpec),
+                    new XAttribute("Culture", item.GetMetadata("Culture")),
+                    new XAttribute("DestinationSubDirectory", item.GetMetadata("DestinationSubDirectory"))));
+            }
+
+            var xmlWriterSettings = new XmlWriterSettings
+            {
+                Indent = true,
+                OmitXmlDeclaration = true
+            };
+
+            using var writer = XmlWriter.Create(stream, xmlWriterSettings);
+            var xDocument = new XDocument(root);
+
+            xDocument.Save(writer);
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/BootJsonData.cs b/src/Components/WebAssembly/Sdk/src/BootJsonData.cs
new file mode 100644
index 0000000000000000000000000000000000000000..b3a2dbd3884763b94d77faa006b55730594867be
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/BootJsonData.cs
@@ -0,0 +1,85 @@
+// 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.Collections.Generic;
+using System.Runtime.Serialization;
+using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+#pragma warning disable IDE1006 // Naming Styles
+    /// <summary>
+    /// Defines the structure of a Blazor boot JSON file
+    /// </summary>
+    public class BootJsonData
+    {
+        /// <summary>
+        /// Gets the name of the assembly with the application entry point
+        /// </summary>
+        public string entryAssembly { get; set; }
+
+        /// <summary>
+        /// Gets the set of resources needed to boot the application. This includes the transitive
+        /// closure of .NET assemblies (including the entrypoint assembly), the dotnet.wasm file,
+        /// and any PDBs to be loaded.
+        ///
+        /// Within <see cref="ResourceHashesByNameDictionary"/>, dictionary keys are resource names,
+        /// and values are SHA-256 hashes formatted in prefixed base-64 style (e.g., 'sha256-abcdefg...')
+        /// as used for subresource integrity checking.
+        /// </summary>
+        public ResourcesData resources { get; set; } = new ResourcesData();
+
+        /// <summary>
+        /// Gets a value that determines whether to enable caching of the <see cref="resources"/>
+        /// inside a CacheStorage instance within the browser.
+        /// </summary>
+        public bool cacheBootResources { get; set; }
+
+        /// <summary>
+        /// Gets a value that determines if this is a debug build.
+        /// </summary>
+        public bool debugBuild { get; set; }
+
+        /// <summary>
+        /// Gets a value that determines if the linker is enabled.
+        /// </summary>
+        public bool linkerEnabled { get; set; }
+
+        /// <summary>
+        /// Config files for the application
+        /// </summary>
+        public List<string> config { get; set; }
+    }
+
+    public class ResourcesData
+    {
+        /// <summary>
+        /// .NET Wasm runtime resources (dotnet.wasm, dotnet.js) etc.
+        /// </summary>
+        public ResourceHashesByNameDictionary runtime { get; set; } = new ResourceHashesByNameDictionary();
+
+        /// <summary>
+        /// "assembly" (.dll) resources
+        /// </summary>
+        public ResourceHashesByNameDictionary assembly { get; set; } = new ResourceHashesByNameDictionary();
+
+        /// <summary>
+        /// "debug" (.pdb) resources
+        /// </summary>
+        [DataMember(EmitDefaultValue = false)]
+        public ResourceHashesByNameDictionary pdb { get; set; }
+
+        /// <summary>
+        /// localization (.satellite resx) resources
+        /// </summary>
+        [DataMember(EmitDefaultValue = false)]
+        public Dictionary<string, ResourceHashesByNameDictionary> satelliteResources { get; set; }
+
+        /// <summary>
+        /// Assembly (.dll) resources that are loaded lazily during runtime
+        /// </summary>
+        [DataMember(EmitDefaultValue = false)]
+        public ResourceHashesByNameDictionary lazyAssembly { get; set; }
+    }
+#pragma warning restore IDE1006 // Naming Styles
+}
diff --git a/src/Components/WebAssembly/Sdk/src/BrotliCompress.cs b/src/Components/WebAssembly/Sdk/src/BrotliCompress.cs
new file mode 100644
index 0000000000000000000000000000000000000000..89b3004a5edb691befa0ad1443200a877557d015
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/BrotliCompress.cs
@@ -0,0 +1,125 @@
+// 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.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class BrotliCompress : ToolTask
+    {
+        private static readonly char[] InvalidPathChars = Path.GetInvalidFileNameChars();
+        private string _dotnetPath;
+
+        [Required]
+        public ITaskItem[] FilesToCompress { get; set; }
+
+        [Output]
+        public ITaskItem[] CompressedFiles { get; set; }
+
+        [Required]
+        public string OutputDirectory { get; set; }
+
+        public string CompressionLevel { get; set; }
+
+        public bool SkipIfOutputIsNewer { get; set; }
+
+        [Required]
+        public string ToolAssembly { get; set; }
+
+        protected override string ToolName => Path.GetDirectoryName(DotNetPath);
+
+        private string DotNetPath
+        {
+            get
+            {
+                if (!string.IsNullOrEmpty(_dotnetPath))
+                {
+                    return _dotnetPath;
+                }
+
+                _dotnetPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
+                if (string.IsNullOrEmpty(_dotnetPath))
+                {
+                    throw new InvalidOperationException("DOTNET_HOST_PATH is not set");
+                }
+
+                return _dotnetPath;
+            }
+        }
+
+        protected override string GenerateCommandLineCommands() => ToolAssembly;
+
+        protected override string GenerateResponseFileCommands()
+        {
+            var builder = new StringBuilder();
+
+
+            builder.AppendLine("brotli");
+
+            if (!string.IsNullOrEmpty(CompressionLevel))
+            {
+                builder.AppendLine("-c");
+                builder.AppendLine(CompressionLevel);
+            }
+
+            CompressedFiles = new ITaskItem[FilesToCompress.Length];
+
+            for (var i = 0; i < FilesToCompress.Length; i++)
+            {
+                var input = FilesToCompress[i];
+                var inputFullPath = input.GetMetadata("FullPath");
+                var relativePath = input.GetMetadata("RelativePath");
+                var outputRelativePath = Path.Combine(OutputDirectory, CalculateTargetPath(inputFullPath, ".br"));
+                var outputFullPath = Path.GetFullPath(outputRelativePath);
+
+                var outputItem = new TaskItem(outputRelativePath);
+                outputItem.SetMetadata("RelativePath", relativePath + ".br");
+                CompressedFiles[i] = outputItem;
+
+                if (SkipIfOutputIsNewer && File.Exists(outputFullPath) && File.GetLastWriteTimeUtc(inputFullPath) < File.GetLastWriteTimeUtc(outputFullPath))
+                {
+                    Log.LogMessage(MessageImportance.Low, $"Skipping compression for '{input.ItemSpec}' because '{outputRelativePath}' is newer than '{input.ItemSpec}'.");
+                    continue;
+                }
+
+                builder.AppendLine("-s");
+                builder.AppendLine(inputFullPath);
+
+                builder.AppendLine("-o");
+                builder.AppendLine(outputFullPath);
+            }
+
+            return builder.ToString();
+        }
+
+        internal static string CalculateTargetPath(string relativePath, string extension)
+        {
+            // RelativePath can be long and if used as-is to write the output, might result in long path issues on Windows.
+            // Instead we'll calculate a fixed length path by hashing the input file name. This uses SHA1 similar to the Hash task in MSBuild
+            // since it has no crytographic significance.
+            using var hash = SHA1.Create();
+            var bytes = Encoding.UTF8.GetBytes(relativePath);
+            var hashString = Convert.ToBase64String(hash.ComputeHash(bytes));
+
+            var builder = new StringBuilder();
+
+            for (var i = 0; i < 8; i++)
+            {
+                var c = hashString[i];
+                builder.Append(InvalidPathChars.Contains(c) ? '+' : c);
+            }
+
+            builder.Append(extension);
+            return builder.ToString();
+        }
+
+        protected override string GenerateFullPathToTool() => DotNetPath;
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/CreateBlazorTrimmerRootDescriptorFile.cs b/src/Components/WebAssembly/Sdk/src/CreateBlazorTrimmerRootDescriptorFile.cs
new file mode 100644
index 0000000000000000000000000000000000000000..f14ffad2219ab4c7bd7418cb794c3b98151e9897
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/CreateBlazorTrimmerRootDescriptorFile.cs
@@ -0,0 +1,79 @@
+// 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.Xml;
+using System.Xml.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    // Based on https://github.com/mono/linker/blob/3b329b9481e300bcf4fb88a2eebf8cb5ef8b323b/src/ILLink.Tasks/CreateRootDescriptorFile.cs
+    public class CreateBlazorTrimmerRootDescriptorFile : Task
+    {
+        [Required]
+        public ITaskItem[] Assemblies { get; set; }
+
+        [Required]
+        public ITaskItem TrimmerFile { get; set; }
+
+        public override bool Execute()
+        {
+            var rootDescriptor = CreateRootDescriptorContents();
+            if (File.Exists(TrimmerFile.ItemSpec))
+            {
+                var existing = File.ReadAllText(TrimmerFile.ItemSpec);
+
+                if (string.Equals(rootDescriptor, existing, StringComparison.Ordinal))
+                {
+                    Log.LogMessage(MessageImportance.Low, "Skipping write to file {0} because contents would not change.", TrimmerFile.ItemSpec);
+                    // Avoid writing if the file contents are identical. This is required for build incrementalism.
+                    return !Log.HasLoggedErrors;
+                }
+            }
+
+            File.WriteAllText(TrimmerFile.ItemSpec, rootDescriptor);
+            return !Log.HasLoggedErrors;
+        }
+
+        internal string CreateRootDescriptorContents()
+        {
+            var roots = new XElement("linker");
+            foreach (var assembly in Assemblies.OrderBy(a => a.ItemSpec))
+            {
+                // NOTE: Descriptor files don't include the file extension
+                // in the assemblyName.
+                var assemblyName = assembly.GetMetadata("FileName");
+                var typePreserved = assembly.GetMetadata("Preserve");
+                var typeRequired = assembly.GetMetadata("Required");
+
+                var attributes = new List<XAttribute>
+                {
+                    new XAttribute("fullname", "*"),
+                    new XAttribute("required", typeRequired),
+                };
+
+                if (!string.IsNullOrEmpty(typePreserved))
+                {
+                    attributes.Add(new XAttribute("preserve", typePreserved));
+                }
+
+                roots.Add(new XElement("assembly",
+                    new XAttribute("fullname", assemblyName),
+                    new XElement("type", attributes)));
+            }
+
+            var xmlWriterSettings = new XmlWriterSettings
+            {
+                Indent = true,
+                OmitXmlDeclaration = true
+            };
+
+            return new XDocument(roots).Root.ToString();
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/GZipCompress.cs b/src/Components/WebAssembly/Sdk/src/GZipCompress.cs
new file mode 100644
index 0000000000000000000000000000000000000000..4b8a0c542a1b9a467c6fdec48651794d59f981cb
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/GZipCompress.cs
@@ -0,0 +1,70 @@
+// 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.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class GZipCompress : Task
+    {
+        [Required]
+        public ITaskItem[] FilesToCompress { get; set; }
+
+        [Output]
+        public ITaskItem[] CompressedFiles { get; set; }
+
+        [Required]
+        public string OutputDirectory { get; set; }
+
+        public override bool Execute()
+        {
+            CompressedFiles = new ITaskItem[FilesToCompress.Length];
+
+            Directory.CreateDirectory(OutputDirectory);
+
+            System.Threading.Tasks.Parallel.For(0, FilesToCompress.Length, i =>
+            {
+                var file = FilesToCompress[i];
+                var inputPath = file.ItemSpec;
+                var relativePath = file.GetMetadata("RelativePath");
+                var outputRelativePath = Path.Combine(
+                    OutputDirectory,
+                    BrotliCompress.CalculateTargetPath(relativePath, ".gz"));
+
+                var outputItem = new TaskItem(outputRelativePath);
+                outputItem.SetMetadata("RelativePath", relativePath + ".gz");
+                CompressedFiles[i] = outputItem;
+
+                if (File.Exists(outputRelativePath) && File.GetLastWriteTimeUtc(inputPath) < File.GetLastWriteTimeUtc(outputRelativePath))
+                {
+                    // Incrementalism. If input source doesn't exist or it exists and is not newer than the expected output, do nothing.
+                    Log.LogMessage(MessageImportance.Low, $"Skipping '{inputPath}' because '{outputRelativePath}' is newer than '{inputPath}'.");
+                    return;
+                }
+
+                try
+                {
+                    using var sourceStream = File.OpenRead(inputPath);
+                    using var fileStream = File.Create(outputRelativePath);
+                    using var stream = new GZipStream(fileStream, CompressionLevel.Optimal);
+
+                    sourceStream.CopyTo(stream);
+                }
+                catch (Exception e)
+                {
+                    Log.LogErrorFromException(e);
+                    return;
+                }
+            });
+
+            return !Log.HasLoggedErrors;
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/GenerateBlazorWebAssemblyBootJson.cs b/src/Components/WebAssembly/Sdk/src/GenerateBlazorWebAssemblyBootJson.cs
new file mode 100644
index 0000000000000000000000000000000000000000..81cb6ecb96dbf7c7a567978018fcdae779f9e515
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/GenerateBlazorWebAssemblyBootJson.cs
@@ -0,0 +1,155 @@
+// 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.Serialization.Json;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class GenerateBlazorWebAssemblyBootJson : Task
+    {
+        [Required]
+        public string AssemblyPath { get; set; }
+
+        [Required]
+        public ITaskItem[] Resources { get; set; }
+
+        [Required]
+        public bool DebugBuild { get; set; }
+
+        [Required]
+        public bool LinkerEnabled { get; set; }
+
+        [Required]
+        public bool CacheBootResources { get; set; }
+
+        public ITaskItem[] ConfigurationFiles { get; set; }
+
+        [Required]
+        public string OutputPath { get; set; }
+
+        public ITaskItem[] LazyLoadedAssemblies { get; set; }
+
+        public override bool Execute()
+        {
+            using var fileStream = File.Create(OutputPath);
+            var entryAssemblyName = AssemblyName.GetAssemblyName(AssemblyPath).Name;
+
+            try
+            {
+                WriteBootJson(fileStream, entryAssemblyName);
+            }
+            catch (Exception ex)
+            {
+                Log.LogErrorFromException(ex);
+            }
+
+            return !Log.HasLoggedErrors;
+        }
+
+        // Internal for tests
+        public void WriteBootJson(Stream output, string entryAssemblyName)
+        {
+            var result = new BootJsonData
+            {
+                entryAssembly = entryAssemblyName,
+                cacheBootResources = CacheBootResources,
+                debugBuild = DebugBuild,
+                linkerEnabled = LinkerEnabled,
+                resources = new ResourcesData(),
+                config = new List<string>(),
+            };
+
+            // Build a two-level dictionary of the form:
+            // - assembly:
+            //   - UriPath (e.g., "System.Text.Json.dll")
+            //     - ContentHash (e.g., "4548fa2e9cf52986")
+            // - runtime:
+            //   - UriPath (e.g., "dotnet.js")
+            //     - ContentHash (e.g., "3448f339acf512448")
+            if (Resources != null)
+            {
+                var resourceData = result.resources;
+                foreach (var resource in Resources)
+                {
+                    ResourceHashesByNameDictionary resourceList;
+
+                    var fileName = resource.GetMetadata("FileName");
+                    var extension = resource.GetMetadata("Extension");
+                    var resourceCulture = resource.GetMetadata("Culture");
+                    var assetType = resource.GetMetadata("AssetType");
+                    var resourceName = $"{fileName}{extension}";
+
+                    if (IsLazyLoadedAssembly(fileName))
+                    {
+                        resourceData.lazyAssembly ??= new ResourceHashesByNameDictionary();
+                        resourceList = resourceData.lazyAssembly;
+                    }
+                    else if (!string.IsNullOrEmpty(resourceCulture))
+                    {
+                        resourceData.satelliteResources ??= new Dictionary<string, ResourceHashesByNameDictionary>(StringComparer.OrdinalIgnoreCase);
+                        resourceName = resourceCulture + "/" + resourceName;
+
+                        if (!resourceData.satelliteResources.TryGetValue(resourceCulture, out resourceList))
+                        {
+                            resourceList = new ResourceHashesByNameDictionary();
+                            resourceData.satelliteResources.Add(resourceCulture, resourceList);
+                        }
+                    }
+                    else if (string.Equals(extension, ".pdb", StringComparison.OrdinalIgnoreCase))
+                    {
+                        resourceData.pdb ??= new ResourceHashesByNameDictionary();
+                        resourceList = resourceData.pdb;
+                    }
+                    else if (string.Equals(extension, ".dll", StringComparison.OrdinalIgnoreCase))
+                    {
+                        resourceList = resourceData.assembly;
+                    }
+                    else if (string.Equals(assetType, "native", StringComparison.OrdinalIgnoreCase))
+                    {
+                        resourceList = resourceData.runtime;
+                    }
+                    else
+                    {
+                        // This should include items such as XML doc files, which do not need to be recorded in the manifest.
+                        continue;
+                    }
+
+                    if (!resourceList.ContainsKey(resourceName))
+                    {
+                        resourceList.Add(resourceName, $"sha256-{resource.GetMetadata("FileHash")}");
+                    }
+                }
+            }
+
+            if (ConfigurationFiles != null)
+            {
+                foreach (var configFile in ConfigurationFiles)
+                {
+                    result.config.Add(Path.GetFileName(configFile.ItemSpec));
+                }
+            }
+
+            var serializer = new DataContractJsonSerializer(typeof(BootJsonData), new DataContractJsonSerializerSettings
+            {
+                UseSimpleDictionaryFormat = true
+            });
+
+            using var writer = JsonReaderWriterFactory.CreateJsonWriter(output, Encoding.UTF8, ownsStream: false, indent: true);
+            serializer.WriteObject(writer, result);
+        }
+
+        private bool IsLazyLoadedAssembly(string fileName)
+        {
+            return LazyLoadedAssemblies != null && LazyLoadedAssemblies.Any(a => a.ItemSpec == fileName);
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/GenerateServiceWorkerAssetsManifest.cs b/src/Components/WebAssembly/Sdk/src/GenerateServiceWorkerAssetsManifest.cs
new file mode 100644
index 0000000000000000000000000000000000000000..08c75a927af75c98350f9ed65f60d80b35f406cd
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/GenerateServiceWorkerAssetsManifest.cs
@@ -0,0 +1,97 @@
+// 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.IO;
+using System.Linq;
+using System.Runtime.Serialization.Json;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public partial class GenerateServiceWorkerAssetsManifest : Task
+    {
+        [Required]
+        public ITaskItem[] Assets { get; set; }
+
+        public string Version { get; set; }
+
+        [Required]
+        public string OutputPath { get; set; }
+
+        [Output]
+        public string CalculatedVersion { get; set; }
+
+        public override bool Execute()
+        {
+            using var fileStream = File.Create(OutputPath);
+            CalculatedVersion = GenerateAssetManifest(fileStream);
+
+            return true;
+        }
+
+        internal string GenerateAssetManifest(Stream stream)
+        {
+            var assets = new AssetsManifestFileEntry[Assets.Length];
+            System.Threading.Tasks.Parallel.For(0, assets.Length, i =>
+            {
+                var item = Assets[i];
+                var hash = item.GetMetadata("FileHash");
+                var url = item.GetMetadata("AssetUrl");
+
+                if (string.IsNullOrEmpty(hash))
+                {
+                    // Some files that are part of the service worker manifest may not have their hashes previously
+                    // calcualted. Calculate them at this time.
+                    using var sha = SHA256.Create();
+                    using var file = File.OpenRead(item.ItemSpec);
+                    var bytes = sha.ComputeHash(file);
+
+                    hash = Convert.ToBase64String(bytes);
+                }
+
+                assets[i] = new AssetsManifestFileEntry
+                {
+                    hash = "sha256-" + hash,
+                    url = url,
+                };
+            });
+
+            var version = Version;
+            if (string.IsNullOrEmpty(version))
+            {
+                // If a version isn't specified (which is likely the most common case), construct a Version by combining
+                // the file names + hashes of all the inputs.
+
+                var combinedHash = string.Join(
+                    Environment.NewLine,
+                    assets.OrderBy(f => f.url, StringComparer.Ordinal).Select(f => f.hash));
+
+                using var sha = SHA256.Create();
+                var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(combinedHash));
+                version = Convert.ToBase64String(bytes).Substring(0, 8);
+            }
+
+            var data = new AssetsManifestFile
+            {
+                version = version,
+                assets = assets,
+            };
+
+            using var streamWriter = new StreamWriter(stream, Encoding.UTF8, bufferSize: 50, leaveOpen: true);
+            streamWriter.Write("self.assetsManifest = ");
+            streamWriter.Flush();
+
+            using var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(stream, Encoding.UTF8, ownsStream: false, indent: true);
+            new DataContractJsonSerializer(typeof(AssetsManifestFile)).WriteObject(jsonWriter, data);
+            jsonWriter.Flush();
+
+            streamWriter.WriteLine(";");
+
+            return version;
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.csproj b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..adf7330a3feede765baf2dfe3bd044a0431965f5
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.csproj
@@ -0,0 +1,89 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <Description>MSBuild support for building Blazor WebAssembly apps.</Description>
+    <TargetFrameworks>$(DefaultNetCoreTargetFramework);net46</TargetFrameworks>
+
+    <TargetName>Microsoft.NET.Sdk.BlazorWebAssembly.Tasks</TargetName>
+    <NuspecFile>$(MSBuildProjectName).nuspec</NuspecFile>
+    <Serviceable>true</Serviceable>
+    <SdkOutputPath>$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\sdk-output\</SdkOutputPath>
+
+    <!-- Allow assemblies outside of lib in the package -->
+    <NoWarn>$(NoWarn);NU5100</NoWarn>
+    <!-- Need to build this project in source build -->
+    <ExcludeFromSourceBuild>false</ExcludeFromSourceBuild>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.Build.Framework" />
+    <Reference Include="Microsoft.Build.Utilities.Core" />
+
+    <ProjectReference
+      Include="..\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj"
+      Targets="Publish"
+      ReferenceOutputAssembly="false"
+      IsImplicityDefined="false"
+      SkipGetTargetFrameworkProperties="true"
+      UndefineProperties="TargetFramework;TargetFrameworks;RuntimeIdentifier;PublishDir" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Content Include="_._" CopyToOutputDirectory="PreserveNewest" />
+  </ItemGroup>
+
+  <Target Name="LayoutDependencies" BeforeTargets="Build"
+    Condition="'$(IsInnerBuild)' != 'true' AND '$(NoBuild)' != 'true'">
+    <!-- Layout tasks, compiler, and extensions in the sdk-output folder. The entire folder structure gets packaged as-is into the SDK -->
+
+    <PropertyGroup Condition="'$(ContinuousIntegrationBuild)' != 'true'">
+      <_ContinueOnError>true</_ContinueOnError>
+      <_Retries>1</_Retries>
+    </PropertyGroup>
+
+    <PropertyGroup Condition="'$(ContinuousIntegrationBuild)' == 'true'">
+      <_ContinueOnError>false</_ContinueOnError>
+      <_Retries>10</_Retries>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_WebAssemblyToolsOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly.Tools\$(Configuration)\$(DefaultNetCoreTargetFramework)\publish\Microsoft.*" />
+    </ItemGroup>
+
+    <Error
+      Text="WebAssembly SDK tools outputs were not found in $(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly.Tools\$(Configuration)\$(DefaultNetCoreTargetFramework)\publish"
+      Condition="'@(_WebAssemblyToolsOutput->Count())' == '0'" />
+
+    <Copy
+      SourceFiles="@(_WebAssemblyToolsOutput)"
+      DestinationFolder="$(SdkOutputPath)tools\$(DefaultNetCoreTargetFramework)\"
+      SkipUnchangedFiles="true"
+      Retries="$(_Retries)"
+      ContinueOnError="$(_ContinueOnError)" />
+
+    <ItemGroup>
+      <ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\net46*\Microsoft.NET.Sdk.BlazorWebAssembly.*" />
+      <ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\$(DefaultNetCoreTargetFramework)*\Microsoft.NET.Sdk.BlazorWebAssembly.*" />
+    </ItemGroup>
+
+    <Copy SourceFiles="@(ProjectOutput)" DestinationFiles="$(SdkOutputPath)tasks\%(RecursiveDir)%(FileName)%(Extension)" SkipUnchangedFiles="true" Retries="$(_Retries)" ContinueOnError="$(_ContinueOnError)">
+      <Output TaskParameter="CopiedFiles" ItemName="FileWrites" />
+    </Copy>
+
+    <Message Text="Blazor WebAssembly SDK output -&gt; $(SdkOutputPath)" Importance="High" />
+  </Target>
+
+  <Target Name="PopulateNuspec" BeforeTargets="InitializeStandardNuspecProperties" DependsOnTargets="LayoutDependencies">
+    <PropertyGroup>
+      <PackageTags>$(PackageTags.Replace(';',' '))</PackageTags>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <NuspecProperty Include="outputPath=$(OutputPath)\sdk-output" />
+    </ItemGroup>
+  </Target>
+
+  <!-- Workarounds to allow publishing to work when the SDK is referenced as a project.  -->
+  <Target Name="GetTargetPath" />
+  <Target Name="GetCopyToPublishDirectoryItems" />
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.nuspec b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.nuspec
new file mode 100644
index 0000000000000000000000000000000000000000..6d74a1931cca5bd02c843a03d8876e85f16834ef
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.nuspec
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
+  <metadata>
+    $CommonMetadataElements$
+    <dependencies>
+      <group targetFramework=".NET5.0" />
+    </dependencies>
+  </metadata>
+
+  <files>
+    $CommonFileElements$
+    <file src="Sdk\*" target="Sdk" />
+    <file src="build\**" target="build" />
+    <file src="targets\**" target="targets" />
+    <file src="_._" target="lib\net5.0\_._" />
+
+    <file src="$outputPath$\**" target="\" />
+  </files>
+</package>
diff --git a/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.props b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.props
new file mode 100644
index 0000000000000000000000000000000000000000..3f6870441f48028ecae226e762e97d6c88a0d490
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.props
@@ -0,0 +1,22 @@
+<!--
+***********************************************************************************************
+Sdk.props
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0" TreatAsLocalProperty="RuntimeIdentifier">
+  <PropertyGroup>
+    <UsingMicrosoftNETSdkBlazorWebAssembly>true</UsingMicrosoftNETSdkBlazorWebAssembly>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <_BlazorWebAssemblyPropsFile Condition="'$(_BlazorWebAssemblyPropsFile)' == ''">$(MSBuildThisFileDirectory)..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.props</_BlazorWebAssemblyPropsFile>
+  </PropertyGroup>
+
+  <Import Project="$(_BlazorWebAssemblyPropsFile)" />
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.targets b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.targets
new file mode 100644
index 0000000000000000000000000000000000000000..616c56fb8a0b060e3972526176036f0cd0c35e10
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.targets
@@ -0,0 +1,20 @@
+<!--
+***********************************************************************************************
+Sdk.targets
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0">
+
+  <PropertyGroup>
+    <_BlazorWebAssemblyTargetsFile Condition="'$(_BlazorWebAssemblyTargetsFile)' == ''">$(MSBuildThisFileDirectory)..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets</_BlazorWebAssemblyTargetsFile>
+  </PropertyGroup>
+
+  <Import Project="$(_BlazorWebAssemblyTargetsFile)" />
+
+</Project>
diff --git a/src/Razor/test/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js b/src/Components/WebAssembly/Sdk/src/_._
similarity index 100%
rename from src/Razor/test/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js
rename to src/Components/WebAssembly/Sdk/src/_._
diff --git a/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.props b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.props
new file mode 100644
index 0000000000000000000000000000000000000000..27e3fde3e00048ebf61c45aadda356bd0f35caf7
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.props
@@ -0,0 +1,16 @@
+<!--
+***********************************************************************************************
+Microsoft.NET.Sdk.BlazorWebAssembly.props
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0">
+  <PropertyGroup>
+    <_BlazorWebAssemblyPropsFile>$(MSBuildThisFileDirectory)..\..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.props</_BlazorWebAssemblyPropsFile>
+  </PropertyGroup>
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.targets b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.targets
new file mode 100644
index 0000000000000000000000000000000000000000..8091b3d876f795eb899deef51912c0d540f1d25d
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.targets
@@ -0,0 +1,16 @@
+<!--
+***********************************************************************************************
+Microsoft.NET.Sdk.BlazorWebAssembly.props
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0">
+  <PropertyGroup>
+    <_BlazorWebAssemblyTargetsFile>$(MSBuildThisFileDirectory)..\..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets</_BlazorWebAssemblyTargetsFile>
+  </PropertyGroup>
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/targets/BlazorWasm.web.config b/src/Components/WebAssembly/Sdk/src/targets/BlazorWasm.web.config
new file mode 100644
index 0000000000000000000000000000000000000000..7f9995d792c3df47a6b92c4d598e44a2715be78c
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/targets/BlazorWasm.web.config
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+  <system.webServer>
+    <staticContent>
+      <remove fileExtension=".dat" />
+      <remove fileExtension=".dll" />
+      <remove fileExtension=".json" />
+      <remove fileExtension=".wasm" />
+      <remove fileExtension=".woff" />
+      <remove fileExtension=".woff2" />
+      <mimeMap fileExtension=".dll" mimeType="application/octet-stream" />
+      <mimeMap fileExtension=".dat" mimeType="application/octet-stream" />
+      <mimeMap fileExtension=".json" mimeType="application/json" />
+      <mimeMap fileExtension=".wasm" mimeType="application/wasm" />
+      <mimeMap fileExtension=".woff" mimeType="application/font-woff" />
+      <mimeMap fileExtension=".woff2" mimeType="application/font-woff" />
+    </staticContent>
+    <httpCompression>
+      <dynamicTypes>
+        <add mimeType="application/octet-stream" enabled="true" />
+        <add mimeType="application/wasm" enabled="true" />
+      </dynamicTypes>
+    </httpCompression>
+    <rewrite>
+      <rules>
+        <rule name="Serve subdir">
+          <match url=".*" />
+          <action type="Rewrite" url="wwwroot\{R:0}" />
+        </rule>
+        <rule name="SPA fallback routing" stopProcessing="true">
+          <match url=".*" />
+          <conditions logicalGrouping="MatchAll">
+            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
+          </conditions>
+          <action type="Rewrite" url="wwwroot\" />
+        </rule>
+      </rules>
+    </rewrite>
+  </system.webServer>
+</configuration>
diff --git a/src/Components/WebAssembly/Sdk/src/targets/LinkerWorkaround.xml b/src/Components/WebAssembly/Sdk/src/targets/LinkerWorkaround.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c61bc7a3cca2d6c976e9c578a8aa4d0960333695
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/targets/LinkerWorkaround.xml
@@ -0,0 +1,15 @@
+<linker>
+
+  <!-- This file specifies which parts of the BCL or Blazor packages must not be stripped
+  by the IL linker even if they are not referenced by user code. The file format is
+  described at https://github.com/mono/linker/blob/master/src/linker/README.md#syntax-of-xml-descriptor -->
+
+  <assembly fullname="System">
+    <!-- Without this, [Required(typeof(bool), "true", "true", ErrorMessage = "...")] fails -->
+    <type fullname="System.ComponentModel.BooleanConverter" />
+
+    <!-- TypeConverters are only used through reflection. These are two built-in TypeConverters that are useful. -->
+    <type fullname="System.ComponentModel.GuidConverter" />
+    <type fullname="System.ComponentModel.TimeSpanConverter" />
+  </assembly>
+</linker>
diff --git a/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props
new file mode 100644
index 0000000000000000000000000000000000000000..03b94ad5666bf9b21695a4befdeeb092f60c964f
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props
@@ -0,0 +1,36 @@
+<!--
+***********************************************************************************************
+Microsoft.NET.Sdk.BlazorWebAssembly.props
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0" TreatAsLocalProperty="RuntimeIdentifier">
+  <PropertyGroup>
+    <!-- Blazor WASM projects are always browser-wasm -->
+    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
+
+    <!-- Avoid having the rid show up in output paths -->
+    <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
+
+    <OutputType>exe</OutputType>
+
+    <IsPackable>false</IsPackable>
+
+    <WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <!-- Determines if this Sdk is responsible for importing Microsoft.NET.Sdk.Razor. Temporary workaround until we can create a SDK. -->
+    <_RazorSdkImportsMicrosoftNetSdkRazor Condition="'$(UsingMicrosoftNETSdkRazor)' != 'true'">true</_RazorSdkImportsMicrosoftNetSdkRazor>
+  </PropertyGroup>
+
+  <Import Sdk="Microsoft.NET.Sdk.Razor" Project="Sdk.props" Condition="'$(_RazorSdkImportsMicrosoftNetSdkRazor)' == 'true'" />
+  <Import Sdk="Microsoft.NET.Sdk.Web.ProjectSystem" Project="Sdk.props" />
+  <Import Sdk="Microsoft.NET.Sdk.Publish" Project="Sdk.props" />
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets
new file mode 100644
index 0000000000000000000000000000000000000000..a11bc00625c2037367952498632c027696657965
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets
@@ -0,0 +1,582 @@
+<!--
+***********************************************************************************************
+Microsoft.NET.Sdk.BlazorWebAssembly.targets
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+<Project ToolsVersion="14.0">
+
+  <PropertyGroup>
+    <EnableDefaultContentItems Condition=" '$(EnableDefaultContentItems)' == '' ">true</EnableDefaultContentItems>
+  </PropertyGroup>
+
+  <Import Sdk="Microsoft.NET.Sdk.Razor" Project="Sdk.targets" Condition="'$(_RazorSdkImportsMicrosoftNetSdkRazor)' == 'true'" />
+  <Import Sdk="Microsoft.NET.Sdk.Web.ProjectSystem" Project="Sdk.targets" />
+  <Import Sdk="Microsoft.NET.Sdk.Publish" Project="Sdk.targets" />
+
+  <!--
+    Targets supporting Razor MSBuild integration. Contain support for generating C# code using Razor
+    and including the generated code in the project lifecycle, including compiling, publishing and producing
+    nuget packages.
+  -->
+
+  <!--
+    This is a hook to import a set of targets before the Blazor targets. By default this is unused.
+  -->
+  <Import Project="$(CustomBeforeBlazorWebAssemblySdkTargets)" Condition="'$(CustomBeforeBlazorWebAssemblySdkTargets)' != '' and Exists('$(CustomBeforeBlazorWebAssemblySdkTargets)')"/>
+
+  <PropertyGroup>
+    <!-- Paths to tools, tasks, and extensions are calculated relative to the BlazorWebAssemblySdkDirectoryRoot. This can be modified to test a local build. -->
+    <BlazorWebAssemblySdkDirectoryRoot Condition="'$(BlazorWebAssemblySdkDirectoryRoot)'==''">$(MSBuildThisFileDirectory)..\..\</BlazorWebAssemblySdkDirectoryRoot>
+    <_BlazorWebAssemblySdkTasksTFM Condition=" '$(MSBuildRuntimeType)' == 'Core'">net5.0</_BlazorWebAssemblySdkTasksTFM>
+    <_BlazorWebAssemblySdkTasksTFM Condition=" '$(MSBuildRuntimeType)' != 'Core'">net46</_BlazorWebAssemblySdkTasksTFM>
+    <_BlazorWebAssemblySdkTasksAssembly>$(BlazorWebAssemblySdkDirectoryRoot)tasks\$(_BlazorWebAssemblySdkTasksTFM)\Microsoft.NET.Sdk.BlazorWebAssembly.Tasks.dll</_BlazorWebAssemblySdkTasksAssembly>
+    <_BlazorWebAssemblySdkToolAssembly>$(BlazorWebAssemblySdkDirectoryRoot)tools\net5.0\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.dll</_BlazorWebAssemblySdkToolAssembly>
+  </PropertyGroup>
+
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.GenerateBlazorWebAssemblyBootJson" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.BlazorWriteSatelliteAssemblyFile" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.BlazorReadSatelliteAssemblyFile" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.BrotliCompress" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.GzipCompress" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.CreateBlazorTrimmerRootDescriptorFile" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+
+  <PropertyGroup>
+    <SelfContained>true</SelfContained>
+    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+
+    <!-- Trimmer defaults -->
+    <PublishTrimmed Condition="'$(PublishTrimmed)' == ''">true</PublishTrimmed>
+    <TrimMode Condition="'$(TrimMode)' == ''">link</TrimMode>
+
+    <StaticWebAssetBasePath Condition="'$(StaticWebAssetBasePath)' == ''">/</StaticWebAssetBasePath>
+    <BlazorCacheBootResources Condition="'$(BlazorCacheBootResources)' == ''">true</BlazorCacheBootResources>
+
+    <!-- Turn off parts of the build that do not apply to WASM projects -->
+    <GenerateDependencyFile>false</GenerateDependencyFile>
+    <GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
+    <PreserveCompilationContext>false</PreserveCompilationContext>
+    <PreserveCompilationReferences>false</PreserveCompilationReferences>
+    <IsWebConfigTransformDisabled>true</IsWebConfigTransformDisabled>
+
+    <!-- Internal properties -->
+    <_BlazorOutputPath>wwwroot\_framework\</_BlazorOutputPath>
+
+  </PropertyGroup>
+
+  <Import Project="Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets" />
+
+  <Target Name="_ScrambleDotnetJsFileName" AfterTargets="ResolveRuntimePackAssets">
+    <!--
+      We want the dotnet.js file output to have a version to better work with caching. We'll append the runtime version to the file name as soon as file has been discovered.
+    -->
+    <PropertyGroup>
+      <_DotNetJsVersion>$(BundledNETCoreAppPackageVersion)</_DotNetJsVersion>
+      <_DotNetJsVersion Condition="'$(RuntimeFrameworkVersion)' != ''">$(RuntimeFrameworkVersion)</_DotNetJsVersion>
+      <_BlazorDotnetJsFileName>dotnet.$(_DotNetJsVersion).js</_BlazorDotnetJsFileName>
+      <_BlazorDotNetJsFilePath>$(IntermediateOutputPath)$(_BlazorDotnetJsFileName)</_BlazorDotNetJsFilePath>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_DotNetJsItem Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.DestinationSubPath)' == 'dotnet.js' AND '%(ReferenceCopyLocalPaths.AssetType)' == 'native'" />
+    </ItemGroup>
+
+    <Copy
+      SourceFiles="@(_DotNetJsItem)"
+      DestinationFiles="$(_BlazorDotNetJsFilePath)"
+      SkipUnchangedFiles="true"
+      OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" />
+
+    <ItemGroup Condition="'@(_DotNetJsItem->Count())' != '0'">
+      <ReferenceCopyLocalPaths
+        Include="$(_BlazorDotNetJsFilePath)"
+        AssetType="native"
+        CopyLocal="true"
+        DestinationSubPath="$(_BlazorDotnetJsFileName)" />
+
+      <ReferenceCopyLocalPaths Remove="@(_DotNetJsItem)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_ResolveBlazorWasmOutputs" DependsOnTargets="ResolveReferences;PrepareResourceNames;ComputeIntermediateSatelliteAssemblies">
+    <!--
+      Calculates the outputs and the paths for Blazor WASM. This target is invoked frequently and should perform minimal work.
+    -->
+
+    <PropertyGroup>
+      <_BlazorSatelliteAssemblyCacheFile>$(IntermediateOutputPath)blazor.satelliteasm.props</_BlazorSatelliteAssemblyCacheFile>
+      <!-- Workaround for https://github.com/dotnet/sdk/issues/12114-->
+      <PublishDir Condition="'$(AppendRuntimeIdentifierToOutputPath)' != 'true' AND '$(PublishDir)' == '$(OutputPath)$(RuntimeIdentifier)\$(PublishDirName)\'">$(OutputPath)$(PublishDirName)\</PublishDir>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_BlazorJSFile Include="$(BlazorWebAssemblyJSPath)" />
+      <_BlazorJSFile Include="$(BlazorWebAssemblyJSMapPath)" Condition="Exists('$(BlazorWebAssemblyJSMapPath)')" />
+
+      <_BlazorConfigFile Include="wwwroot\appsettings*.json" />
+
+      <!-- Clear out temporary build artifacts that the runtime packages -->
+      <ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.a'" />
+
+      <!--
+        ReferenceCopyLocalPaths includes satellite assemblies from referenced projects but are inexpicably missing
+        any metadata that might allow them to be differentiated. We'll explicitly add those
+        to _BlazorOutputWithTargetPath so that satellite assemblies from packages, the current project and referenced project
+        are all treated the same.
+       -->
+
+      <_BlazorCopyLocalPath
+        Include="@(ReferenceCopyLocalPaths)"
+        Exclude="@(ReferenceSatellitePaths)"/>
+
+      <_BlazorCopyLocalPath Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
+        <DestinationSubDirectory>%(IntermediateSatelliteAssembliesWithTargetPath.Culture)\</DestinationSubDirectory>
+      </_BlazorCopyLocalPath>
+
+      <_BlazorOutputWithTargetPath Include="
+          @(_BlazorCopyLocalPath);
+          @(IntermediateAssembly);
+          @(_DebugSymbolsIntermediatePath);
+          @(_BlazorJSFile)" />
+
+      <_BlazorOutputWithTargetPath Include="@(ReferenceSatellitePaths)">
+        <Culture>$([System.String]::Copy('%(ReferenceSatellitePaths.DestinationSubDirectory)').Trim('\').Trim('/'))</Culture>
+      </_BlazorOutputWithTargetPath>
+    </ItemGroup>
+
+    <!--
+      BuildingProject=false is typically set for referenced projects when building inside VisualStudio.
+
+      When building with BuildingProject=false, satellite assemblies do not get resolved (the ones for the current project and the one for
+      referenced project). Satellite assemblies from packages get resolved.
+      To workaround this, we'll cache metadata during a regular build, and rehydrate from it when BuildingProject=false.
+    -->
+    <BlazorReadSatelliteAssemblyFile
+        ReadFile="$(_BlazorSatelliteAssemblyCacheFile)"
+        Condition="'$(BuildingProject)' != 'true' AND EXISTS('$(_BlazorSatelliteAssemblyCacheFile)')">
+      <Output TaskParameter="SatelliteAssembly" ItemName="_BlazorReadSatelliteAssembly" />
+    </BlazorReadSatelliteAssemblyFile>
+
+    <ItemGroup>
+      <!-- We've imported a previously Cacheed file. Let's turn in to a _BlazorOutputWithTargetPath -->
+      <_BlazorOutputWithTargetPath
+        Include="@(_BlazorReadSatelliteAssembly)"
+        Exclude="@(_BlazorOutputWithTargetPath)"
+        Condition="'@(_BlazorReadSatelliteAssembly->Count())' != '0'" />
+
+      <!-- Calculate the target path -->
+      <_BlazorOutputWithTargetPath
+        TargetPath="$(_BlazorOutputPath)%(_BlazorOutputWithTargetPath.DestinationSubDirectory)%(FileName)%(Extension)"
+        Condition="'%(__BlazorOutputWithTargetPath.TargetPath)' == ''" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_ProcessBlazorWasmOutputs" DependsOnTargets="_ResolveBlazorWasmOutputs">
+    <PropertyGroup>
+      <_BlazorBuildGZipCompressDirectory>$(IntermediateOutputPath)build-gz\</_BlazorBuildGZipCompressDirectory>
+    </PropertyGroup>
+
+    <!--
+      Compress referenced binaries using GZip during build. This skips files such as the project's assemblies
+      that change from build to build. Runtime assets contribute to the bulk of the download size. Compressing it
+      has the most benefit while avoiding any ongoing costs to the dev inner loop.
+    -->
+    <ItemGroup>
+      <_GzipFileToCompressForBuild
+        Include="@(ReferenceCopyLocalPaths)"
+        RelativePath="$(_BlazorOutputPath)%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)"
+        Condition="'%(Extension)' == '.dll' or '%(ReferenceCopyLocalPaths.AssetType)' == 'native'" />
+    </ItemGroup>
+
+    <GZipCompress
+      FilesToCompress="@(_GzipFileToCompressForBuild)"
+      OutputDirectory="$(_BlazorBuildGZipCompressDirectory)">
+
+      <Output TaskParameter="CompressedFiles" ItemName="_BlazorBuildGZipCompressedFile" />
+      <Output TaskParameter="CompressedFiles" ItemName="FileWrites" />
+    </GZipCompress>
+
+    <ItemGroup>
+      <_BlazorWriteSatelliteAssembly Include="@(_BlazorOutputWithTargetPath->HasMetadata('Culture'))" />
+
+      <!-- Retarget ReferenceCopyLocalPaths to copy to the wwwroot directory -->
+      <ReferenceCopyLocalPaths DestinationSubDirectory="$(_BlazorOutputPath)%(ReferenceCopyLocalPaths.DestinationSubDirectory)" />
+    </ItemGroup>
+
+    <!-- A missing blazor.webassembly.js is our packaging error. Produce an error so it's discovered early. -->
+    <Error
+      Text="Unable to find BlazorWebAssembly JS files. This usually indicates a packaging error."
+      Code="RAZORSDK1007"
+      Condition="'@(_BlazorJSFile->Count())' == '0'" />
+
+    <!--
+      When building with BuildingProject=false, satellite assemblies do not get resolved (the ones for the current project and the one for
+      referenced project). BuildingProject=false is typically set for referenced projects when building inside VisualStudio.
+      To workaround this, we'll cache metadata during a regular build, and rehydrate from it when BuildingProject=false.
+    -->
+
+    <BlazorWriteSatelliteAssemblyFile
+      SatelliteAssembly="@(_BlazorWriteSatelliteAssembly)"
+      WriteFile="$(_BlazorSatelliteAssemblyCacheFile)"
+      Condition="'$(BuildingProject)' == 'true' AND '@(_BlazorWriteSatelliteAssembly->Count())' != '0'" />
+
+    <Delete
+      Files="$(_BlazorSatelliteAssemblyCacheFile)"
+      Condition="'$(BuildingProject)' == 'true' AND '@(_BlazorWriteSatelliteAssembly->Count())' == '0' and EXISTS('$(_BlazorSatelliteAssemblyCacheFile)')" />
+
+    <ItemGroup>
+      <FileWrites Include="$(_BlazorSatelliteAssemblyCacheFile)" Condition="Exists('$(_BlazorSatelliteAssemblyCacheFile)')" />
+    </ItemGroup>
+
+    <GetFileHash Files="@(_BlazorOutputWithTargetPath)" Algorithm="SHA256" HashEncoding="base64">
+      <Output TaskParameter="Items" ItemName="_BlazorOutputWithHash" />
+    </GetFileHash>
+  </Target>
+
+  <PropertyGroup>
+    <PrepareForRunDependsOn>
+      _BlazorWasmPrepareForRun;
+      $(PrepareForRunDependsOn)
+    </PrepareForRunDependsOn>
+
+    <GetCurrentProjectStaticWebAssetsDependsOn>
+      $(GetCurrentProjectStaticWebAssetsDependsOn);
+      _BlazorWasmPrepareForRun;
+    </GetCurrentProjectStaticWebAssetsDependsOn>
+  </PropertyGroup>
+
+  <Target Name="_BlazorWasmPrepareForRun" DependsOnTargets="_ProcessBlazorWasmOutputs" BeforeTargets="_RazorPrepareForRun" AfterTargets="GetCurrentProjectStaticWebAssets">
+    <PropertyGroup>
+      <_BlazorBuildBootJsonPath>$(IntermediateOutputPath)blazor.boot.json</_BlazorBuildBootJsonPath>
+    </PropertyGroup>
+
+    <GenerateBlazorWebAssemblyBootJson
+      AssemblyPath="@(IntermediateAssembly)"
+      Resources="@(_BlazorOutputWithHash)"
+      DebugBuild="true"
+      LinkerEnabled="false"
+      CacheBootResources="$(BlazorCacheBootResources)"
+      OutputPath="$(_BlazorBuildBootJsonPath)"
+      ConfigurationFiles="@(_BlazorConfigFile)"
+      LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)" />
+
+    <ItemGroup>
+      <FileWrites Include="$(OutDir)$(_BlazorOutputPath)blazor.boot.json" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <_BlazorWebAssemblyStaticWebAsset Include="$(_BlazorBuildBootJsonPath)">
+        <SourceId>$(PackageId)</SourceId>
+        <SourceType></SourceType>
+        <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
+        <BasePath>$(StaticWebAssetBasePath)</BasePath>
+        <RelativePath>_framework/blazor.boot.json</RelativePath>
+        <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+      </_BlazorWebAssemblyStaticWebAsset>
+
+      <_BlazorWebAssemblyStaticWebAsset Include="@(_BlazorOutputWithHash)">
+        <SourceId>$(PackageId)</SourceId>
+        <SourceType></SourceType>
+        <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
+        <BasePath>$(StaticWebAssetBasePath)</BasePath>
+        <RelativePath>$([System.String]::Copy('%(_BlazorOutputWithHash.TargetPath)').Replace('\','/').Substring(8))</RelativePath>
+        <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+      </_BlazorWebAssemblyStaticWebAsset>
+
+      <_BlazorWebAssemblyStaticWebAsset Include="@(_BlazorBuildGZipCompressedFile)">
+        <SourceId>$(PackageId)</SourceId>
+        <SourceType></SourceType>
+        <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
+        <BasePath>$(StaticWebAssetBasePath)</BasePath>
+        <RelativePath>$([System.String]::Copy('%(_BlazorBuildGZipCompressedFile.RelativePath)').Replace('\','/').Substring(8))</RelativePath>
+        <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+      </_BlazorWebAssemblyStaticWebAsset>
+
+      <StaticWebAsset Include="@(_BlazorWebAssemblyStaticWebAsset)" />
+      <_ExternalStaticWebAsset Include="@(_BlazorWebAssemblyStaticWebAsset)" SourceType="Generated" />
+    </ItemGroup>
+  </Target>
+
+  <!-- Mimics the behavior of CopyFilesToOutputDirectory. We simply copy relevant build outputs to the wwwroot directory -->
+  <Target Name="_BlazorCopyFilesToOutputDirectory" AfterTargets="CopyFilesToOutputDirectory">
+    <Copy
+        SourceFiles="@(IntermediateAssembly)"
+        DestinationFolder="$(OutDir)$(_BlazorOutputPath)"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)"
+        Condition="'$(CopyBuildOutputToOutputDirectory)' == 'true' and '$(SkipCopyBuildProduct)' != 'true'">
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+
+    <Message Importance="High" Text="$(MSBuildProjectName) (Blazor output) -&gt; $(TargetDir)wwwroot" Condition="'$(CopyBuildOutputToOutputDirectory)' == 'true' and '$(SkipCopyBuildProduct)'!='true'" />
+
+    <Copy
+        SourceFiles="@(_DebugSymbolsIntermediatePath)"
+        DestinationFolder="$(OutDir)$(_BlazorOutputPath)"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)"
+        Condition="'$(_DebugSymbolsProduced)'=='true' and '$(SkipCopyingSymbolsToOutputDirectory)' != 'true' and '$(CopyOutputSymbolsToOutputDirectory)'=='true'">
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+
+    <Copy
+        SourceFiles="@(IntermediateSatelliteAssembliesWithTargetPath)"
+        DestinationFiles="@(IntermediateSatelliteAssembliesWithTargetPath->'$(OutDir)$(_BlazorOutputPath)%(Culture)\$(TargetName).resources.dll')"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)"
+        Condition="'@(IntermediateSatelliteAssembliesWithTargetPath)' != ''" >
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+
+    <Copy
+        SourceFiles="@(_BlazorJSFile);$(_BlazorBuildBootJsonPath)"
+        DestinationFolder="$(OutDir)$(_BlazorOutputPath)"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)">
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+
+    <Copy
+        SourceFiles="@(_BlazorBuildGZipCompressedFile)"
+        DestinationFiles="@(_BlazorBuildGZipCompressedFile->'$(OutDir)%(RelativePath)')"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)">
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+  </Target>
+
+  <Target Name="_BlazorWasmPrepareForLink" BeforeTargets="PrepareForILLink">
+    <PropertyGroup>
+      <_BlazorTypeGranularTrimmerDescriptorFile>$(IntermediateOutputPath)typegranularity.trimmerdescriptor.xml</_BlazorTypeGranularTrimmerDescriptorFile>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_BlazorTypeGranularAssembly
+          Include="@(ManagedAssemblyToLink)"
+          Condition="'%(Extension)' == '.dll' AND ($([System.String]::Copy('%(Filename)').StartsWith('Microsoft.AspNetCore.')) or $([System.String]::Copy('%(Filename)').StartsWith('Microsoft.Extensions.')))">
+        <Required>false</Required>
+        <Preserve>all</Preserve>
+      </_BlazorTypeGranularAssembly>
+
+      <ManagedAssemblyToLink
+        IsTrimmable="true"
+        Condition="'%(Extension)' == '.dll' AND ($([System.String]::Copy('%(Filename)').StartsWith('Microsoft.AspNetCore.')) or $([System.String]::Copy('%(Filename)').StartsWith('Microsoft.Extensions.')))" />
+    </ItemGroup>
+
+    <CreateBlazorTrimmerRootDescriptorFile
+      Assemblies="@(_BlazorTypeGranularAssembly)"
+      TrimmerFile="$(_BlazorTypeGranularTrimmerDescriptorFile)" />
+
+    <ItemGroup>
+      <TrimmerRootDescriptor Include="$(_BlazorTypeGranularTrimmerDescriptorFile)" />
+      <TrimmerRootDescriptor Include="$(MSBuildThisFileDirectory)LinkerWorkaround.xml" />
+
+      <FileWrites Include="$(_BlazorTypeGranularTrimmerDescriptorFile)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_ProcessPublishFilesForBlazor" DependsOnTargets="_ResolveBlazorWasmOutputs" AfterTargets="ILLink">
+
+    <!--
+      ResolvedFileToPublish.Culture is missing for satellite assemblies from project references.
+      Since we need the culture to correctly generate blazor.boot.json, we cross-reference the culture we calculate as part of _ResolveBlazorWasmOutputs
+    -->
+    <JoinItems Left="@(ResolvedFileToPublish)"
+               Right="@(_BlazorOutputWithTargetPath->HasMetadata('Culture'))"
+               LeftMetadata="*"
+               RightMetadata="Culture"
+               ItemSpecToUse="Left">
+      <Output TaskParameter="JoinResult" ItemName="_ResolvedSatelliteToPublish" />
+    </JoinItems>
+
+    <ItemGroup>
+      <ResolvedFileToPublish Remove="@(_ResolvedSatelliteToPublish)" />
+      <ResolvedFileToPublish Include="@(_ResolvedSatelliteToPublish)" />
+
+      <ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'%(Extension)' == '.a'" />
+
+      <!-- Remove dotnet.js from publish output -->
+      <ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.RelativePath)' == 'dotnet.js'" />
+
+      <!-- Retarget so that items are published to the wwwroot directory -->
+      <ResolvedFileToPublish
+        RelativePath="$(_BlazorOutputPath)%(ResolvedFileToPublish.RelativePath)"
+        Condition="'%(ResolvedFileToPublish.RelativePath)' != 'web.config' AND !$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').StartsWith('wwwroot/'))" />
+
+      <!-- Remove pdbs from the publish output -->
+      <ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'$(BlazorWebAssemblyEnableDebugging)' != 'true' AND '%(Extension)' == '.pdb'" />
+    </ItemGroup>
+
+    <ItemGroup Condition="'@(ResolvedFileToPublish->AnyHaveMetadataValue('RelativePath', 'web.config'))' != 'true'">
+      <ResolvedFileToPublish
+         Include="$(MSBuildThisFileDirectory)BlazorWasm.web.config"
+         ExcludeFromSingleFile="true"
+         CopyToPublishDirectory="PreserveNewest"
+         RelativePath="web.config" />
+    </ItemGroup>
+
+    <!-- Generate the publish boot json -->
+    <ItemGroup>
+      <_BlazorPublishBootResource
+        Include="@(ResolvedFileToPublish)"
+        Condition="$([System.String]::Copy('%(RelativePath)').Replace('\','/').StartsWith('wwwroot/_framework')) AND '%(Extension)' != '.a'" />
+    </ItemGroup>
+
+    <GetFileHash Files="@(_BlazorPublishBootResource)" Algorithm="SHA256" HashEncoding="base64">
+      <Output TaskParameter="Items" ItemName="_BlazorPublishBootResourceWithHash" />
+    </GetFileHash>
+
+    <GenerateBlazorWebAssemblyBootJson
+      AssemblyPath="@(IntermediateAssembly)"
+      Resources="@(_BlazorPublishBootResourceWithHash)"
+      DebugBuild="false"
+      LinkerEnabled="$(PublishTrimmed)"
+      CacheBootResources="$(BlazorCacheBootResources)"
+      OutputPath="$(IntermediateOutputPath)blazor.publish.boot.json"
+      ConfigurationFiles="@(_BlazorConfigFile)"
+      LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)" />
+
+    <ItemGroup>
+      <ResolvedFileToPublish
+        Include="$(IntermediateOutputPath)blazor.publish.boot.json"
+        RelativePath="$(_BlazorOutputPath)blazor.boot.json" />
+
+      <ResolvedFileToPublish
+        Include="@(_BlazorJSFile)"
+        RelativePath="$(_BlazorOutputPath)%(FileName)%(Extension)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_BlazorCompressPublishFiles" AfterTargets="_ProcessPublishFilesForBlazor" Condition="'$(BlazorEnableCompression)' != 'false'">
+    <PropertyGroup>
+      <_CompressedFileOutputPath>$(IntermediateOutputPath)compress\</_CompressedFileOutputPath>
+      <_BlazorWebAssemblyBrotliIncremental>true</_BlazorWebAssemblyBrotliIncremental>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_FileToCompress
+        Include="@(ResolvedFileToPublish)"
+        Condition="$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').StartsWith('wwwroot/'))" />
+    </ItemGroup>
+
+    <Message Text="Compressing Blazor WebAssembly publish artifacts. This may take a while..." Importance="High" />
+
+    <MakeDir Directories="$(_CompressedFileOutputPath)" Condition="!Exists('$(_CompressedFileOutputPath)')" />
+
+    <PropertyGroup Condition="'$(DOTNET_HOST_PATH)' == ''">
+      <_DotNetHostDirectory>$(NetCoreRoot)</_DotNetHostDirectory>
+      <_DotNetHostFileName>dotnet</_DotNetHostFileName>
+      <_DotNetHostFileName Condition="'$(OS)' == 'Windows_NT'">dotnet.exe</_DotNetHostFileName>
+    </PropertyGroup>
+
+    <BrotliCompress
+      OutputDirectory="$(_CompressedFileOutputPath)"
+      FilesToCompress="@(_FileToCompress)"
+      CompressionLevel="$(_BlazorBrotliCompressionLevel)"
+      SkipIfOutputIsNewer="$(_BlazorWebAssemblyBrotliIncremental)"
+      ToolAssembly="$(_BlazorWebAssemblySdkToolAssembly)"
+      ToolExe="$(_DotNetHostFileName)"
+      ToolPath="$(_DotNetHostDirectory)">
+
+      <Output TaskParameter="CompressedFiles" ItemName="_BrotliCompressedFile" />
+      <Output TaskParameter="CompressedFiles" ItemName="FileWrites" />
+    </BrotliCompress>
+
+    <GZipCompress
+      OutputDirectory="$(_CompressedFileOutputPath)"
+      FilesToCompress="@(_FileToCompress)">
+
+      <Output TaskParameter="CompressedFiles" ItemName="_BlazorPublishGZipCompressedFile" />
+      <Output TaskParameter="CompressedFiles" ItemName="FileWrites" />
+    </GZipCompress>
+
+    <ItemGroup>
+      <ResolvedFileToPublish Include="@(_BrotliCompressedFile)" />
+      <ResolvedFileToPublish Include="@(_BlazorPublishGZipCompressedFile)" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_SetupPublishSemaphore" BeforeTargets="PrepareForPublish">
+    <PropertyGroup>
+      <!--
+        Add marker that indicates Blazor WASM is doing a publish. This is used to identify when GetCopyToPublishDirectoryItems
+        is invoked as a result of a P2P reference.
+      -->
+      <_PublishingBlazorWasmProject>true</_PublishingBlazorWasmProject>
+    </PropertyGroup>
+  </Target>
+
+  <Target Name="_GetBlazorWasmFilesForPublishInner"
+    DependsOnTargets="_ResolveBlazorWasmOutputs;ComputeFilesToPublish"
+    Returns="@(ResolvedFileToPublish)" />
+
+  <Target Name="_GetBlazorWasmFilesForPublish" BeforeTargets="GetCopyToPublishDirectoryItems">
+    <MSBuild
+      Projects="$(MSBuildProjectFullPath)"
+      Targets="_GetBlazorWasmFilesForPublishInner"
+      Properties="BuildProjectReferences=false;ResolveAssemblyReferencesFindRelatedSatellites=true;_PublishingBlazorWasmProject=true"
+      RemoveProperties="NoBuild;RuntimeIdentifier"
+      BuildInParallel="$(BuildInParallel)"
+      Condition="'$(_PublishingBlazorWasmProject)' != 'true'">
+
+      <Output TaskParameter="TargetOutputs" ItemName="_ResolvedFileToPublish" />
+    </MSBuild>
+
+    <ItemGroup>
+      <AllPublishItemsFullPathWithTargetPath Include="@(_ResolvedFileToPublish->'%(FullPath)')">
+        <TargetPath>%(RelativePath)</TargetPath>
+        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+      </AllPublishItemsFullPathWithTargetPath>
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_BlazorApplyLinkPreferencesToContent" BeforeTargets="AssignTargetPaths;ResolveCurrentProjectStaticWebAssetsInputs;ResolveStaticWebAssetsInputs" Returns="@(Content)">
+    <ItemGroup>
+      <Content
+        Condition="'%(Content.Link)' != '' AND '%(Content.CopyToPublishDirectory)' == '' AND $([System.String]::Copy('%(Content.Link)').Replace('\','/').StartsWith('wwwroot/'))"
+        CopyToPublishDirectory="PreserveNewest" />
+
+    </ItemGroup>
+  </Target>
+
+  <!--
+    This is a hook to import a set of targets after the Blazor WebAssembly targets. By default this is unused.
+  -->
+  <Import Project="$(CustomAfterBlazorWebAssemblySdkTargets)" Condition="'$(CustomAfterBlazorWebAssemblySdkTargets)' != '' and Exists('$(CustomAfterBlazorWebAssemblySdkTargets)')"/>
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets
new file mode 100644
index 0000000000000000000000000000000000000000..5202eb338d40c91814e5b24c2aeeebebd9d0a45b
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets
@@ -0,0 +1,169 @@
+<!--
+***********************************************************************************************
+Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets
+
+WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
+          created a backup copy.  Incorrect changes to this file will make it
+          impossible to load or build your projects from the command-line or the IDE.
+
+Copyright (c) .NET Foundation. All rights reserved.
+***********************************************************************************************
+-->
+
+<Project>
+  <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.GenerateServiceWorkerAssetsManifest" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" />
+
+  <Target Name="_ComputeServiceWorkerAssets" BeforeTargets="ResolveStaticWebAssetsInputs">
+
+    <PropertyGroup>
+      <_ServiceWorkerAssetsManifestIntermediateOutputPath Condition="'$([System.IO.Path]::IsPathRooted($(BaseIntermediateOutputPath)))' == 'true'">obj\$(Configuration)\$(TargetFramework)\$(ServiceWorkerAssetsManifest)</_ServiceWorkerAssetsManifestIntermediateOutputPath>
+      <_ServiceWorkerAssetsManifestIntermediateOutputPath Condition="'$([System.IO.Path]::IsPathRooted($(BaseIntermediateOutputPath)))' != 'true'">$(IntermediateOutputPath)$(ServiceWorkerAssetsManifest)</_ServiceWorkerAssetsManifestIntermediateOutputPath>
+      <_ServiceWorkerAssetsManifestFullPath>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/$(_ServiceWorkerAssetsManifestIntermediateOutputPath)'))</_ServiceWorkerAssetsManifestFullPath>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_ManifestStaticWebAsset Include="$(_ServiceWorkerAssetsManifestFullPath)">
+        <SourceType></SourceType>
+        <SourceId>$(PackageId)</SourceId>
+        <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
+        <BasePath>$(StaticWebAssetBasePath)</BasePath>
+        <RelativePath>$(ServiceWorkerAssetsManifest)</RelativePath>
+        <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+      </_ManifestStaticWebAsset>
+
+      <!-- Figure out where we're getting the content for each @(ServiceWorker) entry, depending on whether there's a PublishedContent value -->
+      <_ServiceWorkerIntermediateFile Include="@(ServiceWorker->'$(IntermediateOutputPath)serviceworkers\%(Identity)')">
+        <ContentSourcePath Condition="'%(_ServiceWorker.PublishedContent)' != ''">%(ServiceWorker.PublishedContent)</ContentSourcePath>
+        <ContentSourcePath Condition="'%(_ServiceWorker.PublishedContent)' == ''">%(ServiceWorker.Identity)</ContentSourcePath>
+        <OriginalPath>%(ServiceWorker.Identity)</OriginalPath>
+        <TargetOutputPath>%(ServiceWorker.Identity)</TargetOutputPath>
+        <TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').Replace('\','/').StartsWith('wwwroot/'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath>
+      </_ServiceWorkerIntermediateFile>
+
+      <_ServiceWorkerStaticWebAsset Include="%(_ServiceWorkerIntermediateFile.FullPath)">
+        <SourceType></SourceType>
+        <SourceId>$(PackageId)</SourceId>
+        <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
+        <BasePath>$(StaticWebAssetBasePath)</BasePath>
+        <RelativePath>%(TargetOutputPath)</RelativePath>
+        <CopyToPublishDirectory>Never</CopyToPublishDirectory>
+      </_ServiceWorkerStaticWebAsset>
+
+      <StaticWebAsset Include="
+          @(_ManifestStaticWebAsset);
+          @(_ServiceWorkerStaticWebAsset)" />
+    </ItemGroup>
+
+  </Target>
+
+  <Target Name="_WriteServiceWorkerAssetsManifest"
+    DependsOnTargets="_ComputeServiceWorkerAssets;ResolveStaticWebAssetsInputs">
+
+    <ItemGroup>
+      <_ServiceWorkItem Include="@(StaticWebAsset)" Exclude="$(_ServiceWorkerAssetsManifestFullPath);@(_ServiceWorkerStaticWebAsset)">
+        <AssetUrl>$([System.String]::Copy('$([System.String]::Copy('%(StaticWebAsset.BasePath)').TrimEnd('/'))/%(StaticWebAsset.RelativePath)').Replace('\','/').TrimStart('/'))</AssetUrl>
+      </_ServiceWorkItem>
+    </ItemGroup>
+
+    <GenerateServiceWorkerAssetsManifest
+      Version="$(ServiceWorkerAssetsManifestVersion)"
+      Assets="@(_ServiceWorkItem)"
+      OutputPath="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)">
+      <Output TaskParameter="CalculatedVersion" PropertyName="_ServiceWorkerAssetsManifestVersion" />
+    </GenerateServiceWorkerAssetsManifest>
+
+    <Copy
+      SourceFiles="%(_ServiceWorkerIntermediateFile.ContentSourcePath)"
+      DestinationFiles="%(_ServiceWorkerIntermediateFile.Identity)" />
+
+    <WriteLinesToFile
+      File="%(_ServiceWorkerIntermediateFile.Identity)"
+      Lines="/* Manifest version: $(_ServiceWorkerAssetsManifestVersion) */"
+      Condition="'$(_ServiceWorkerAssetsManifestVersion)' != ''" />
+
+    <ItemGroup>
+      <FileWrites Include="@(_ServiceWorkerIntermediateFile)" />
+      <FileWrites Include="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)" />
+    </ItemGroup>
+
+  </Target>
+
+  <Target Name="_BlazorStaticAssetsCopyFilesToOutputDirectory" AfterTargets="CopyFilesToOutputDirectory" DependsOnTargets="_WriteServiceWorkerAssetsManifest">
+    <Copy
+        SourceFiles="@(_ManifestStaticWebAsset);@(_ServiceWorkerStaticWebAsset)"
+        DestinationFiles="$(OutDir)wwwroot\%(RelativePath)"
+        SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
+        OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
+        Retries="$(CopyRetryCount)"
+        RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
+        UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
+        UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
+        ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)"
+        Condition="Exists('%(Identity)')">
+
+      <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
+    </Copy>
+  </Target>
+
+  <Target Name="_OmitServiceWorkerContent"
+    BeforeTargets="AssignTargetPaths;ResolveCurrentProjectStaticWebAssetsInputs">
+
+    <ItemGroup>
+      <!-- Don't emit the service worker source files to the output -->
+      <Content Remove="@(ServiceWorker)" />
+      <Content Remove="@(ServiceWorker->'%(PublishedContent)')" />
+    </ItemGroup>
+  </Target>
+
+  <Target Name="_GenerateServiceWorkerFileForPublish"
+    BeforeTargets="_BlazorCompressPublishFiles"
+    AfterTargets="_ProcessPublishFilesForBlazor">
+
+    <PropertyGroup>
+      <_ServiceWorkerAssetsManifestPublishIntermediateOutputPath>$(IntermediateOutputPath)publish-$(ServiceWorkerAssetsManifest)</_ServiceWorkerAssetsManifestPublishIntermediateOutputPath>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <_ServiceWorkerIntermediatePublishFile Include="$(IntermediateOutputPath)serviceworkers\%(FileName).publish%(Extension)">
+        <ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' != ''">%(ServiceWorker.PublishedContent)</ContentSourcePath>
+        <ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' == ''">%(ServiceWorker.Identity)</ContentSourcePath>
+        <RelativePath>%(ServiceWorker.Identity)</RelativePath>
+      </_ServiceWorkerIntermediatePublishFile>
+
+      <_ServiceWorkerPublishFile Include="@(ResolvedFileToPublish)" Condition="$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').StartsWith('wwwroot/'))">
+        <AssetUrl>$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').TrimStart('/'))</AssetUrl>
+        <AssetUrl>$([System.String]::Copy('%(RelativePath)').Replace('\','/').Substring(8))</AssetUrl>
+      </_ServiceWorkerPublishFile>
+    </ItemGroup>
+
+    <GenerateServiceWorkerAssetsManifest
+      Version="$(ServiceWorkerAssetsManifestVersion)"
+      Assets="@(_ServiceWorkerPublishFile)"
+      OutputPath="$(_ServiceWorkerAssetsManifestPublishIntermediateOutputPath)">
+
+      <Output TaskParameter="CalculatedVersion" PropertyName="_ServiceWorkerPublishAssetsManifestVersion" />
+    </GenerateServiceWorkerAssetsManifest>
+
+    <Copy SourceFiles="%(_ServiceWorkerIntermediatePublishFile.ContentSourcePath)"
+      DestinationFiles="%(_ServiceWorkerIntermediatePublishFile.Identity)" />
+
+    <WriteLinesToFile
+      File="%(_ServiceWorkerIntermediatePublishFile.Identity)"
+      Lines="/* Manifest version: $(_ServiceWorkerPublishAssetsManifestVersion) */" />
+
+    <ItemGroup>
+      <ResolvedFileToPublish
+        Include="@(_ServiceWorkerIntermediatePublishFile)"
+        CopyToPublishDirectory="PreserveNewest"
+        RelativePath="%(_ServiceWorkerIntermediatePublishFile.RelativePath)"
+        ExcludeFromSingleFile="true" />
+
+      <ResolvedFileToPublish
+        Include="$(_ServiceWorkerAssetsManifestPublishIntermediateOutputPath)"
+        CopyToPublishDirectory="PreserveNewest"
+        RelativePath="wwwroot\$(ServiceWorkerAssetsManifest)"
+        ExcludeFromSingleFile="true" />
+    </ItemGroup>
+  </Target>
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/test/BlazorReadSatelliteAssemblyFileTest.cs b/src/Components/WebAssembly/Sdk/test/BlazorReadSatelliteAssemblyFileTest.cs
new file mode 100644
index 0000000000000000000000000000000000000000..b95a6154b663fcc5526c72c0243479a06560234b
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/test/BlazorReadSatelliteAssemblyFileTest.cs
@@ -0,0 +1,68 @@
+// 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.Collections.Generic;
+using System.IO;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Moq;
+using Xunit;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class BlazorReadSatelliteAssemblyFileTest
+    {
+        [Fact]
+        public void WritesAndReadsRoundTrip()
+        {
+            // Arrange/Act
+            var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+
+            var writer = new BlazorWriteSatelliteAssemblyFile
+            {
+                BuildEngine = Mock.Of<IBuildEngine>(),
+                WriteFile = new TaskItem(tempFile),
+                SatelliteAssembly = new[]
+                {
+                    new TaskItem("Resources.fr.dll", new Dictionary<string, string>
+                    {
+                        ["Culture"] = "fr",
+                        ["DestinationSubDirectory"] = "fr\\",
+                    }),
+                    new TaskItem("Resources.ja-jp.dll", new Dictionary<string, string>
+                    {
+                        ["Culture"] = "ja-jp",
+                        ["DestinationSubDirectory"] = "ja-jp\\",
+                    }),
+                },
+            };
+
+            var reader = new BlazorReadSatelliteAssemblyFile
+            {
+                BuildEngine = Mock.Of<IBuildEngine>(),
+                ReadFile = new TaskItem(tempFile),
+            };
+
+            writer.Execute();
+
+            Assert.True(File.Exists(tempFile), "Write should have succeeded.");
+
+            reader.Execute();
+
+            Assert.Collection(
+                reader.SatelliteAssembly,
+                assembly =>
+                {
+                    Assert.Equal("Resources.fr.dll", assembly.ItemSpec);
+                    Assert.Equal("fr", assembly.GetMetadata("Culture"));
+                    Assert.Equal("fr\\", assembly.GetMetadata("DestinationSubDirectory"));
+                },
+                assembly =>
+                {
+                    Assert.Equal("Resources.ja-jp.dll", assembly.ItemSpec);
+                    Assert.Equal("ja-jp", assembly.GetMetadata("Culture"));
+                    Assert.Equal("ja-jp\\", assembly.GetMetadata("DestinationSubDirectory"));
+                });
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/test/GenerateBlazorBootJsonTest.cs b/src/Components/WebAssembly/Sdk/test/GenerateBlazorBootJsonTest.cs
new file mode 100644
index 0000000000000000000000000000000000000000..139f22f27fbc42d4d7b8d42c32ce153680f161b7
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/test/GenerateBlazorBootJsonTest.cs
@@ -0,0 +1,195 @@
+// 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.IO;
+using System.Linq;
+using System.Runtime.Serialization.Json;
+using Microsoft.Build.Framework;
+using Moq;
+using Xunit;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly
+{
+    public class GenerateBlazorWebAssemblyBootJsonTest
+    {
+        [Fact]
+        public void GroupsResourcesByType()
+        {
+            // Arrange
+            var taskInstance = new GenerateBlazorWebAssemblyBootJson
+            {
+                AssemblyPath = "MyApp.Entrypoint.dll",
+                Resources = new[]
+                {
+                    CreateResourceTaskItem(
+                        ("FileName", "My.Assembly1"),
+                        ("Extension", ".dll"),
+                        ("FileHash", "abcdefghikjlmnopqrstuvwxyz")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "My.Assembly2"),
+                        ("Extension", ".dll"),
+                        ("FileHash", "012345678901234567890123456789")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "SomePdb"),
+                        ("Extension", ".pdb"),
+                        ("FileHash", "pdbhashpdbhashpdbhash")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "My.Assembly1"),
+                        ("Extension", ".pdb"),
+                        ("FileHash", "pdbdefghikjlmnopqrstuvwxyz")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "some-runtime-file"),
+                        ("FileHash", "runtimehashruntimehash"),
+                        ("AssetType", "native")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "satellite-assembly1"),
+                        ("Extension", ".dll"),
+                        ("FileHash", "hashsatelliteassembly1"),
+                        ("Culture", "en-GB")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "satellite-assembly2"),
+                        ("Extension", ".dll"),
+                        ("FileHash", "hashsatelliteassembly2"),
+                        ("Culture", "fr")),
+
+                    CreateResourceTaskItem(
+                        ("FileName", "satellite-assembly3"),
+                        ("Extension", ".dll"),
+                        ("FileHash", "hashsatelliteassembly3"),
+                        ("Culture", "en-GB")),
+                }
+            };
+
+            using var stream = new MemoryStream();
+
+            // Act
+            taskInstance.WriteBootJson(stream, "MyEntrypointAssembly");
+
+            // Assert
+            var parsedContent = ParseBootData(stream);
+            Assert.Equal("MyEntrypointAssembly", parsedContent.entryAssembly);
+
+            var resources = parsedContent.resources.assembly;
+            Assert.Equal(2, resources.Count);
+            Assert.Equal("sha256-abcdefghikjlmnopqrstuvwxyz", resources["My.Assembly1.dll"]);
+            Assert.Equal("sha256-012345678901234567890123456789", resources["My.Assembly2.dll"]);
+
+            resources = parsedContent.resources.pdb;
+            Assert.Equal(2, resources.Count);
+            Assert.Equal("sha256-pdbhashpdbhashpdbhash", resources["SomePdb.pdb"]);
+            Assert.Equal("sha256-pdbdefghikjlmnopqrstuvwxyz", resources["My.Assembly1.pdb"]);
+
+            resources = parsedContent.resources.runtime;
+            Assert.Single(resources);
+            Assert.Equal("sha256-runtimehashruntimehash", resources["some-runtime-file"]);
+
+            var satelliteResources = parsedContent.resources.satelliteResources;
+            Assert.Collection(
+                satelliteResources.OrderBy(kvp => kvp.Key),
+                kvp =>
+                {
+                    Assert.Equal("en-GB", kvp.Key);
+                    Assert.Collection(
+                        kvp.Value.OrderBy(item => item.Key),
+                        item =>
+                        {
+                            Assert.Equal("en-GB/satellite-assembly1.dll", item.Key);
+                            Assert.Equal("sha256-hashsatelliteassembly1", item.Value);
+                        },
+                        item =>
+                        {
+                            Assert.Equal("en-GB/satellite-assembly3.dll", item.Key);
+                            Assert.Equal("sha256-hashsatelliteassembly3", item.Value);
+                        });
+                },
+                kvp =>
+                {
+                    Assert.Equal("fr", kvp.Key);
+                    Assert.Collection(
+                        kvp.Value.OrderBy(item => item.Key),
+                        item =>
+                        {
+                            Assert.Equal("fr/satellite-assembly2.dll", item.Key);
+                            Assert.Equal("sha256-hashsatelliteassembly2", item.Value);
+                        });
+                });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void CanSpecifyCacheBootResources(bool flagValue)
+        {
+            // Arrange
+            var taskInstance = new GenerateBlazorWebAssemblyBootJson { CacheBootResources = flagValue };
+            using var stream = new MemoryStream();
+
+            // Act
+            taskInstance.WriteBootJson(stream, "MyEntrypointAssembly");
+
+            // Assert
+            var parsedContent = ParseBootData(stream);
+            Assert.Equal(flagValue, parsedContent.cacheBootResources);
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void CanSpecifyDebugBuild(bool flagValue)
+        {
+            // Arrange
+            var taskInstance = new GenerateBlazorWebAssemblyBootJson { DebugBuild = flagValue };
+            using var stream = new MemoryStream();
+
+            // Act
+            taskInstance.WriteBootJson(stream, "MyEntrypointAssembly");
+
+            // Assert
+            var parsedContent = ParseBootData(stream);
+            Assert.Equal(flagValue, parsedContent.debugBuild);
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void CanSpecifyLinkerEnabled(bool flagValue)
+        {
+            // Arrange
+            var taskInstance = new GenerateBlazorWebAssemblyBootJson { LinkerEnabled = flagValue };
+            using var stream = new MemoryStream();
+
+            // Act
+            taskInstance.WriteBootJson(stream, "MyEntrypointAssembly");
+
+            // Assert
+            var parsedContent = ParseBootData(stream);
+            Assert.Equal(flagValue, parsedContent.linkerEnabled);
+        }
+
+        private static BootJsonData ParseBootData(Stream stream)
+        {
+            stream.Position = 0;
+            var serializer = new DataContractJsonSerializer(
+                typeof(BootJsonData),
+                new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true });
+            return (BootJsonData)serializer.ReadObject(stream);
+        }
+
+        private static ITaskItem CreateResourceTaskItem(params (string key, string value)[] values)
+        {
+            var mock = new Mock<ITaskItem>();
+
+            foreach (var (key, value) in values)
+            {
+                mock.Setup(m => m.GetMetadata(key)).Returns(value);
+            }
+            return mock.Object;
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj b/src/Components/WebAssembly/Sdk/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..5d30b4f7814a743b872fb82eada24d1b159cb60c
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.Build.Utilities.Core" />
+    <Reference Include="Microsoft.NET.Sdk.BlazorWebAssembly" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/testassets/Directory.Build.props b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.props
new file mode 100644
index 0000000000000000000000000000000000000000..6d0949542f9f929e151c02e2f1c93d1e13a639e2
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.props
@@ -0,0 +1,50 @@
+<Project>
+  <Import Project="Before.Directory.Build.props" Condition="Exists('Before.Directory.Build.props')" />
+
+  <PropertyGroup>
+    <!--
+      In the case that a user is building a sample directly the MicrosoftNetCompilersToolsetPackagerVersion will not be provided.
+      We'll fall back to whatever the current SDK provides in regards to Roslyn's Microsoft.Net.Compilers.Toolset.
+    -->
+    <BuildingTestAppsIndependently>false</BuildingTestAppsIndependently>
+    <BuildingTestAppsIndependently Condition="'$(MicrosoftNetCompilersToolsetPackageVersion)' == ''">true</BuildingTestAppsIndependently>
+
+    <!-- Do not resolve Reference ItemGroup since it has a different semantic meaning in Razor test apps -->
+    <EnableCustomReferenceResolution>false</EnableCustomReferenceResolution>
+
+    <DefaultNetCoreTargetFramework>net5.0</DefaultNetCoreTargetFramework>
+
+    <RepoRoot Condition="'$(RepoRoot)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, 'AspNetCore.sln'))\</RepoRoot>
+
+    <RazorSdkCurrentVersionProps>$(RepoRoot)src\Razor\Microsoft.NET.Sdk.Razor\src\build\netstandard2.0\Sdk.Razor.CurrentVersion.props</RazorSdkCurrentVersionProps>
+    <RazorSdkCurrentVersionTargets>$(RepoRoot)src\Razor\Microsoft.NET.Sdk.Razor\src\build\netstandard2.0\Sdk.Razor.CurrentVersion.targets</RazorSdkCurrentVersionTargets>
+    <RazorSdkArtifactsDirectory>$(RepoRoot)artifacts\bin\Microsoft.NET.Sdk.Razor\</RazorSdkArtifactsDirectory>
+    <BlazorWebAssemblySdkArtifactsDirectory>$(RepoRoot)artifacts\bin\Microsoft.NET.Sdk.BlazorWebAssembly\</BlazorWebAssemblySdkArtifactsDirectory>
+    <BlazorWebAssemblyJSPath>$(MSBuildThisFileDirectory)blazor.webassembly.js</BlazorWebAssemblyJSPath>
+  </PropertyGroup>
+
+  <Import Project="$(RepoRoot)eng\Versions.props" />
+
+  <PropertyGroup>
+    <!-- Reset version prefix to 1.0.0 for test projects -->
+    <VersionPrefix>1.0.0</VersionPrefix>
+
+    <!-- Turn down the compression level for brotli -->
+    <_BlazorBrotliCompressionLevel>NoCompression</_BlazorBrotliCompressionLevel>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <!-- Have the SDK treat the MvcShim as an MVC assembly -->
+    <_MvcAssemblyName Include="Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib" />
+  </ItemGroup>
+
+  <ItemGroup Condition="$(BuildingTestAppsIndependently) == 'false'">
+    <PackageReference Include="Microsoft.Net.Compilers.Toolset"
+        Version="$(MicrosoftNetCompilersToolsetPackageVersion)"
+        PrivateAssets="all"
+        IsImplicitlyDefined="true" />
+  </ItemGroup>
+
+  <Import Project="After.Directory.Build.props" Condition="Exists('After.Directory.Build.props')" />
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/testassets/Directory.Build.targets b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.targets
new file mode 100644
index 0000000000000000000000000000000000000000..45a2ee1e5650483a3e9ba65f5dc9e2b68edbba03
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.targets
@@ -0,0 +1,7 @@
+<Project>
+  <PropertyGroup>
+    <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)' ">$(MicrosoftNETCoreAppRuntimeVersion)</RuntimeFrameworkVersion>
+    <!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use -->
+    <NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>
+  </PropertyGroup>
+</Project>
diff --git a/src/Razor/test/testassets/LinkBaseToWebRoot/js/LinkedScript.js b/src/Components/WebAssembly/Sdk/testassets/LinkBaseToWebRoot/js/LinkedScript.js
similarity index 100%
rename from src/Razor/test/testassets/LinkBaseToWebRoot/js/LinkedScript.js
rename to src/Components/WebAssembly/Sdk/testassets/LinkBaseToWebRoot/js/LinkedScript.js
diff --git a/src/Components/WebAssembly/Sdk/testassets/RestoreBlazorWasmTestProjects/RestoreBlazorWasmTestProjects.csproj b/src/Components/WebAssembly/Sdk/testassets/RestoreBlazorWasmTestProjects/RestoreBlazorWasmTestProjects.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..f4debf808803f70899bfbfed78c6c896f0b54929
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/testassets/RestoreBlazorWasmTestProjects/RestoreBlazorWasmTestProjects.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\blazorhosted\blazorhosted.csproj" />
+    <ProjectReference Include="..\blazorhosted-rid\blazorhosted-rid.csproj" />
+    <ProjectReference Include="..\blazorwasm\blazorwasm.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/testassets/blazor.webassembly.js b/src/Components/WebAssembly/Sdk/testassets/blazor.webassembly.js
new file mode 100644
index 0000000000000000000000000000000000000000..84362ca046b156d71d84fa5356ae9afbcb282919
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/testassets/blazor.webassembly.js
@@ -0,0 +1 @@
+Test file
\ No newline at end of file
diff --git a/src/Razor/test/testassets/blazorhosted-rid/Program.cs b/src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/Program.cs
similarity index 100%
rename from src/Razor/test/testassets/blazorhosted-rid/Program.cs
rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/Program.cs
diff --git a/src/Razor/test/testassets/blazorhosted-rid/blazorhosted-rid.csproj b/src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/blazorhosted-rid.csproj
similarity index 100%
rename from src/Razor/test/testassets/blazorhosted-rid/blazorhosted-rid.csproj
rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/blazorhosted-rid.csproj
diff --git a/src/Razor/test/testassets/blazorhosted/Program.cs b/src/Components/WebAssembly/Sdk/testassets/blazorhosted/Program.cs
similarity index 100%
rename from src/Razor/test/testassets/blazorhosted/Program.cs
rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted/Program.cs
diff --git a/src/Razor/test/testassets/blazorhosted/blazorhosted.csproj b/src/Components/WebAssembly/Sdk/testassets/blazorhosted/blazorhosted.csproj
similarity index 100%
rename from src/Razor/test/testassets/blazorhosted/blazorhosted.csproj
rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted/blazorhosted.csproj
diff --git a/src/Razor/test/testassets/blazorwasm/App.razor b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/App.razor
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/App.razor
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/App.razor
diff --git a/src/Razor/test/testassets/blazorwasm/LinkToWebRoot/css/app.css b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/LinkToWebRoot/css/app.css
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/LinkToWebRoot/css/app.css
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/LinkToWebRoot/css/app.css
diff --git a/src/Razor/test/testassets/blazorwasm/Pages/Index.razor b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/Pages/Index.razor
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/Pages/Index.razor
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/Pages/Index.razor
diff --git a/src/Razor/test/testassets/blazorwasm/Program.cs b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/Program.cs
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/Program.cs
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/Program.cs
diff --git a/src/Razor/test/testassets/blazorwasm/Resources.ja.resx.txt b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/Resources.ja.resx.txt
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/Resources.ja.resx.txt
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/Resources.ja.resx.txt
diff --git a/src/Razor/test/testassets/blazorwasm/_Imports.razor b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/_Imports.razor
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/_Imports.razor
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/_Imports.razor
diff --git a/src/Razor/test/testassets/blazorwasm/blazorwasm.csproj b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/blazorwasm.csproj
similarity index 88%
rename from src/Razor/test/testassets/blazorwasm/blazorwasm.csproj
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/blazorwasm.csproj
index 36a511e7c77fa6d890c9953eb542c7db61b8b5e4..c8906b1bd290604f6ba88d752a6cb8f7e56f6edc 100644
--- a/src/Razor/test/testassets/blazorwasm/blazorwasm.csproj
+++ b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/blazorwasm.csproj
@@ -1,10 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk.Razor">
+
+  <Import Project="$(RepoRoot)src\Components\WebAssembly\Sdk\src\Sdk\Sdk.props" />
+
   <PropertyGroup>
     <TargetFramework>net5.0</TargetFramework>
-    <UseBlazorWebAssembly>true</UseBlazorWebAssembly>
     <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
-    <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
     <RazorSdkDirectoryRoot>$(RazorSdkArtifactsDirectory)$(Configuration)\sdk-output\</RazorSdkDirectoryRoot>
+    <BlazorWebAssemblySdkDirectoryRoot>$(BlazorWebAssemblySdkArtifactsDirectory)$(Configuration)\sdk-output\</BlazorWebAssemblySdkDirectoryRoot>
     <ServiceWorkerAssetsManifest>custom-service-worker-assets.js</ServiceWorkerAssetsManifest>
   </PropertyGroup>
 
@@ -50,4 +52,6 @@
     <ServiceWorker Include="wwwroot\serviceworkers\my-service-worker.js" PublishedContent="wwwroot\serviceworkers\my-prod-service-worker.js" />
   </ItemGroup>
 
+  <Import Project="$(RepoRoot)src\Components\WebAssembly\Sdk\src\Sdk\Sdk.targets" />
+
 </Project>
diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/Fake-License.txt b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/Fake-License.txt
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/wwwroot/Fake-License.txt
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/Fake-License.txt
diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/css/app.css b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/css/app.css
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/wwwroot/css/app.css
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/css/app.css
diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/index.html b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/index.html
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/wwwroot/index.html
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/index.html
diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js
diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js
similarity index 100%
rename from src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js
rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js
diff --git a/src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Class1.cs b/src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Class1.cs
similarity index 100%
rename from src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Class1.cs
rename to src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Class1.cs
diff --git a/src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx b/src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx
similarity index 100%
rename from src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx
rename to src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx
diff --git a/src/Razor/test/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj b/src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj
similarity index 100%
rename from src/Razor/test/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj
rename to src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj
diff --git a/src/Razor/test/testassets/razorclasslibrary/Class1.cs b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/Class1.cs
similarity index 100%
rename from src/Razor/test/testassets/razorclasslibrary/Class1.cs
rename to src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/Class1.cs
diff --git a/src/Razor/test/testassets/razorclasslibrary/RazorClassLibrary.csproj b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/RazorClassLibrary.csproj
similarity index 100%
rename from src/Razor/test/testassets/razorclasslibrary/RazorClassLibrary.csproj
rename to src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/RazorClassLibrary.csproj
diff --git a/src/Razor/test/testassets/razorclasslibrary/wwwroot/styles.css b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/styles.css
similarity index 100%
rename from src/Razor/test/testassets/razorclasslibrary/wwwroot/styles.css
rename to src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/styles.css
diff --git a/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/Components/WebAssembly/Sdk/tools/Application.cs b/src/Components/WebAssembly/Sdk/tools/Application.cs
new file mode 100644
index 0000000000000000000000000000000000000000..d9d66d10bc8b678e425b9af5a82628b272e9ea96
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/Application.cs
@@ -0,0 +1,98 @@
+// 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.Reflection;
+using System.Threading;
+using Microsoft.Extensions.CommandLineUtils;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools
+{
+    internal class Application : CommandLineApplication
+    {
+        public Application(
+            CancellationToken cancellationToken,
+            TextWriter output = null,
+            TextWriter error = null)
+        {
+            CancellationToken = cancellationToken;
+            Out = output ?? Out;
+            Error = error ?? Error;
+
+            Name = "BlazorWebAssembly.Tools";
+            FullName = "Microsoft Blazor WebAssembly SDK tool";
+            Description = "CLI for Blazor WebAssembly operations.";
+            ShortVersionGetter = GetInformationalVersion;
+
+            HelpOption("-?|-h|--help");
+
+            Commands.Add(new BrotliCompressCommand(this));
+        }
+
+        public CancellationToken CancellationToken { get; }
+
+        public new int Execute(params string[] args)
+        {
+            try
+            {
+                return base.Execute(ExpandResponseFiles(args));
+            }
+            catch (AggregateException ex) when (ex.InnerException != null)
+            {
+                foreach (var innerException in ex.Flatten().InnerExceptions)
+                {
+                    Error.WriteLine(innerException.Message);
+                    Error.WriteLine(innerException.StackTrace);
+                }
+                return 1;
+            }
+            catch (CommandParsingException ex)
+            {
+                // Don't show a call stack when we have unneeded arguments, just print the error message.
+                // The code that throws this exception will print help, so no need to do it here.
+                Error.WriteLine(ex.Message);
+                return 1;
+            }
+            catch (OperationCanceledException)
+            {
+                // This is a cancellation, not a failure.
+                Error.WriteLine("Cancelled");
+                return 1;
+            }
+            catch (Exception ex)
+            {
+                Error.WriteLine(ex.Message);
+                Error.WriteLine(ex.StackTrace);
+                return 1;
+            }
+        }
+
+        private string GetInformationalVersion()
+        {
+            var assembly = typeof(Application).GetTypeInfo().Assembly;
+            var attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
+            return attribute.InformationalVersion;
+        }
+
+        private static string[] ExpandResponseFiles(string[] args)
+        {
+            var expandedArgs = new List<string>();
+            foreach (var arg in args)
+            {
+                if (!arg.StartsWith("@", StringComparison.Ordinal))
+                {
+                    expandedArgs.Add(arg);
+                }
+                else
+                {
+                    var fileName = arg.Substring(1);
+                    expandedArgs.AddRange(File.ReadLines(fileName));
+                }
+            }
+
+            return expandedArgs.ToArray();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Components/WebAssembly/Sdk/tools/BrotliCompressCommand.cs b/src/Components/WebAssembly/Sdk/tools/BrotliCompressCommand.cs
new file mode 100644
index 0000000000000000000000000000000000000000..136c2fc660eee25617d528ca1745428c766e126b
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/BrotliCompressCommand.cs
@@ -0,0 +1,85 @@
+// 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.IO;
+using System.IO.Compression;
+using System.Threading.Tasks;
+using Microsoft.Extensions.CommandLineUtils;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools
+{
+    internal class BrotliCompressCommand : CommandLineApplication
+    {
+        public BrotliCompressCommand(Application parent)
+            : base(throwOnUnexpectedArg: true)
+        {
+            base.Parent = parent;
+            Name = "brotli";
+            Sources = Option("-s", "files to compress", CommandOptionType.MultipleValue);
+            Outputs = Option("-o", "Output file path", CommandOptionType.MultipleValue);
+            CompressionLevelOption = Option("-c", "Compression level", CommandOptionType.SingleValue);
+
+            Invoke = () => Execute().GetAwaiter().GetResult();
+        }
+
+        public CommandOption Sources { get; }
+
+        public CommandOption Outputs { get; }
+
+        public CommandOption CompressionLevelOption { get; }
+
+        public CompressionLevel CompressionLevel { get; private set; } = CompressionLevel.Optimal;
+
+        private Task<int> Execute()
+        {
+            if (!ValidateArguments())
+            {
+                ShowHelp();
+                return Task.FromResult(1);
+            }
+
+            return ExecuteCoreAsync();
+        }
+
+        private bool ValidateArguments()
+        {
+            if (Sources.Values.Count != Outputs.Values.Count)
+            {
+                Error.WriteLine($"{Sources.Description} has {Sources.Values.Count}, but {Outputs.Description} has {Outputs.Values.Count} values.");
+                return false;
+            }
+
+            if (CompressionLevelOption.HasValue())
+            {
+                if (!Enum.TryParse<CompressionLevel>(CompressionLevelOption.Value(), out var value))
+                {
+                    Error.WriteLine($"Invalid option {CompressionLevelOption.Value()} for {CompressionLevelOption.Template}.");
+                    return false;
+                }
+
+                CompressionLevel = value;
+            }
+
+            return true;
+        }
+
+        private Task<int> ExecuteCoreAsync()
+        {
+            Parallel.For(0, Sources.Values.Count, i =>
+            {
+                var source = Sources.Values[i];
+                var output = Outputs.Values[i];
+
+                using var sourceStream = File.OpenRead(source);
+                using var fileStream = new FileStream(output, FileMode.Create);
+
+                using var stream = new BrotliStream(fileStream, CompressionLevel);
+
+                sourceStream.CopyTo(stream);
+            });
+
+            return Task.FromResult(0);
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/tools/DebugMode.cs b/src/Components/WebAssembly/Sdk/tools/DebugMode.cs
new file mode 100644
index 0000000000000000000000000000000000000000..816bb4a783555cc75e835592bc3d57a406e41ed7
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/DebugMode.cs
@@ -0,0 +1,27 @@
+// 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.Linq;
+using System.Threading;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools
+{
+    internal static class DebugMode
+    {
+        public static void HandleDebugSwitch(ref string[] args)
+        {
+            if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase))
+            {
+                args = args.Skip(1).ToArray();
+
+                Console.WriteLine("Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id);
+                while (!Debugger.IsAttached)
+                {
+                    Thread.Sleep(TimeSpan.FromSeconds(3));
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Components/WebAssembly/Sdk/tools/Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj b/src/Components/WebAssembly/Sdk/tools/Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..6ac7a26c1924308fabd759e1b519b60ef12524ea
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net5.0</TargetFramework>
+
+    <IsPackable>false</IsPackable>
+    <IsShipping>false</IsShipping>
+    <DisablePubternalApiCheck>true</DisablePubternalApiCheck>
+
+    <UseAppHost>false</UseAppHost>
+    <RuntimeIdentifier />
+
+    <!-- Need to build this project in source build -->
+    <ExcludeFromSourceBuild>false</ExcludeFromSourceBuild>
+  </PropertyGroup>
+
+  <ItemGroup>
+     <Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/Components/WebAssembly/Sdk/tools/Program.cs b/src/Components/WebAssembly/Sdk/tools/Program.cs
new file mode 100644
index 0000000000000000000000000000000000000000..b00093323d09fa7b8205ab9888675d994de224b2
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/Program.cs
@@ -0,0 +1,30 @@
+// 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.IO;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+
+namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools
+{
+    internal static class Program
+    {
+        public static int Main(string[] args)
+        {
+            DebugMode.HandleDebugSwitch(ref args);
+
+            var cancel = new CancellationTokenSource();
+            Console.CancelKeyPress += (sender, e) => { cancel.Cancel(); };
+
+            var application = new Application(
+                cancel.Token,
+                Console.Out,
+                Console.Error);
+
+            application.Commands.Add(new BrotliCompressCommand(application));
+
+            return application.Execute(args);
+        }
+    }
+}
diff --git a/src/Components/WebAssembly/Sdk/tools/runtimeconfig.template.json b/src/Components/WebAssembly/Sdk/tools/runtimeconfig.template.json
new file mode 100644
index 0000000000000000000000000000000000000000..2c73f398906918d3b5d5f39686fcf55fe1372218
--- /dev/null
+++ b/src/Components/WebAssembly/Sdk/tools/runtimeconfig.template.json
@@ -0,0 +1,3 @@
+{
+    "rollForwardOnNoCandidateFx": 2
+}
\ No newline at end of file
diff --git a/src/Components/test/E2ETest/Tests/RoutingTest.cs b/src/Components/test/E2ETest/Tests/RoutingTest.cs
index 2b0c708097d7971ce231d74420b2cbcd6b92c96b..074e57d3720a8dc418ad29a0a6c5688975bd4cf9 100644
--- a/src/Components/test/E2ETest/Tests/RoutingTest.cs
+++ b/src/Components/test/E2ETest/Tests/RoutingTest.cs
@@ -565,6 +565,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
             AssertDidNotLog("I'm not happening...");
         }
 
+        [Fact]
+        public void OnNavigate_CanRenderUIForExceptions()
+        {
+            var app = Browser.MountTestComponent<TestRouterWithOnNavigate>();
+
+            // Navigating from one page to another should
+            // cancel the previous OnNavigate Task
+            SetUrlViaPushState("/Other");
+
+            var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10));
+            Assert.NotNull(errorUiElem);
+        }
+
         private long BrowserScrollY
         {
             get => (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window.scrollY");
diff --git a/src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithOnNavigate.razor b/src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithOnNavigate.razor
index 4d51ea9e2555d4d857806870b5455003b8d5d53a..5baa82fc005fba993357e48c724ab32e3ea00106 100644
--- a/src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithOnNavigate.razor
+++ b/src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithOnNavigate.razor
@@ -20,7 +20,8 @@
     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) }
+        { "LongPage2", new Func<NavigationContext, Task>(TestOnNavCancel) },
+        { "Other", new Func<NavigationContext, Task>(TestOnNavException) }
     };
 
     private async Task OnNavigateAsync(NavigationContext args)
@@ -43,4 +44,10 @@
         await Task.Delay(2000, args.CancellationToken);
         Console.WriteLine("I'm not happening...");
     }
+
+    public static async Task TestOnNavException(NavigationContext args)
+    {
+        await Task.CompletedTask;
+        throw new Exception("This is an uncaught exception.");
+    }
 }
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj
index a1e160322eb73603cca5b227a855bd1e4bcbedb6..2832be128d11c3f6748079ce85cfe6053e0a8c84 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj
+++ b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <!--
@@ -13,8 +13,11 @@
 
     <!-- Tests do not work on Helix yet -->
     <BuildHelixPayload>false</BuildHelixPayload>
+    <TestAppsRoot>$(MSBuildProjectDirectory)\..\..\test\testassets\</TestAppsRoot>
   </PropertyGroup>
 
+  <Import Project="$(SharedSourceRoot)MSBuild.Testing\MSBuild.Testing.targets" />
+
   <ItemGroup>
     <None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
   </ItemGroup>
@@ -24,49 +27,6 @@
     <Reference Include="Microsoft.Extensions.DependencyModel" />
   </ItemGroup>
 
-  <ItemGroup>
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>Testing.AdditionalRestoreSources</_Parameter1>
-      <_Parameter2>$(MSBuildThisFileDirectory)..\testassets\PregeneratedPackages</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>ArtifactsLogDir</_Parameter1>
-      <_Parameter2>$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'log', '$(_BuildConfig)'))</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>ProcDumpToolPath</_Parameter1>
-      <_Parameter2>$(ProcDumpPath)</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>Testing.RepoRoot</_Parameter1>
-      <_Parameter2>$(RepoRoot)</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>MicrosoftNETCoreAppRuntimeVersion</_Parameter1>
-      <_Parameter2>$(MicrosoftNETCoreAppRuntimeVersion)</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>DefaultNetCoreTargetFramework</_Parameter1>
-      <_Parameter2>$(DefaultNetCoreTargetFramework)</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>MicrosoftNetCompilersToolsetPackageVersion</_Parameter1>
-      <_Parameter2>$(MicrosoftNetCompilersToolsetPackageVersion)</_Parameter2>
-    </AssemblyAttribute>
-
-    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
-      <_Parameter1>RazorSdkDirectoryRoot</_Parameter1>
-      <_Parameter2>$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\sdk-output\</_Parameter2>
-    </AssemblyAttribute>
-
-  </ItemGroup>
-
   <ItemGroup>
     <Reference Include="rzc" />
     <ProjectReference Include="..\..\test\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.csproj" />
diff --git a/src/Shared/E2ETesting/E2ETesting.targets b/src/Shared/E2ETesting/E2ETesting.targets
index d0fa6cb856ec4934762587337bd8997bf0715db5..76ced2cce99343ce40dccbcea959f47142f3428c 100644
--- a/src/Shared/E2ETesting/E2ETesting.targets
+++ b/src/Shared/E2ETesting/E2ETesting.targets
@@ -66,14 +66,9 @@
       <_DefaultProjectRoot>$([System.IO.Path]::GetFullPath($(_DefaultProjectFilter)))</_DefaultProjectRoot>
     </PropertyGroup>
     <ItemGroup>
-      <_ContentRootProjectReferencesUnfiltered
-        Include="@(ReferencePath)"
-        Condition="'%(ReferencePath.ReferenceSourceTarget)' == 'ProjectReference'" />
-      <_ContentRootProjectReferencesFilter
-        Include="@(_ContentRootProjectReferencesUnfiltered->StartsWith('$(_DefaultProjectRoot)'))" />
       <_ContentRootProjectReferences
-        Include="@(_ContentRootProjectReferencesFilter)"
-        Condition="'%(Identity)' == 'True'" />
+        Include="@(ReferencePath)"
+        Condition="'%(ReferencePath.ReferenceSourceTarget)' == 'ProjectReference' AND $([System.String]::Copy(%(ReferencePath.MSBuildSourceProjectFile)).StartsWith('$(_DefaultProjectRoot)'))" />
     </ItemGroup>
   </Target>
 
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Assert.cs b/src/Shared/MSBuild.Testing/Assert.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Assert.cs
rename to src/Shared/MSBuild.Testing/Assert.cs
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildVariables.cs b/src/Shared/MSBuild.Testing/BuildVariables.cs
similarity index 83%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildVariables.cs
rename to src/Shared/MSBuild.Testing/BuildVariables.cs
index 797d244625016737352c4a09c028487ff579b9a2..b659ba918c7afeada9ed60751afe58e65d2fe90a 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildVariables.cs
+++ b/src/Shared/MSBuild.Testing/BuildVariables.cs
@@ -19,8 +19,12 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
 
         public static string RazorSdkDirectoryRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "RazorSdkDirectoryRoot").Value;
 
+        public static string BlazorWebAssemblySdkDirectoryRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "BlazorWebAssemblySdkDirectoryRoot").Value;
+
         public static string RepoRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "Testing.RepoRoot").Value;
-        
+
         public static string DefaultNetCoreTargetFramework => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "DefaultNetCoreTargetFramework").Value;
+
+        public static string TestAppsRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "TestAppsRoot").Value;
     }
 }
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/FIleThumbPrint.cs b/src/Shared/MSBuild.Testing/FIleThumbPrint.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/FIleThumbPrint.cs
rename to src/Shared/MSBuild.Testing/FIleThumbPrint.cs
diff --git a/src/Shared/MSBuild.Testing/MSBuild.Testing.targets b/src/Shared/MSBuild.Testing/MSBuild.Testing.targets
new file mode 100644
index 0000000000000000000000000000000000000000..8868a23d9791d63136476e6007809cf3a6e14ec5
--- /dev/null
+++ b/src/Shared/MSBuild.Testing/MSBuild.Testing.targets
@@ -0,0 +1,53 @@
+<Project>
+
+<ItemGroup>
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>ArtifactsLogDir</_Parameter1>
+      <_Parameter2>$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'log', '$(_BuildConfig)'))</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>ProcDumpToolPath</_Parameter1>
+      <_Parameter2>$(ProcDumpPath)</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>Testing.RepoRoot</_Parameter1>
+      <_Parameter2>$(RepoRoot)</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>MicrosoftNETCoreAppRuntimeVersion</_Parameter1>
+      <_Parameter2>$(MicrosoftNETCoreAppRuntimeVersion)</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>DefaultNetCoreTargetFramework</_Parameter1>
+      <_Parameter2>$(DefaultNetCoreTargetFramework)</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>MicrosoftNetCompilersToolsetPackageVersion</_Parameter1>
+      <_Parameter2>$(MicrosoftNetCompilersToolsetPackageVersion)</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>RazorSdkDirectoryRoot</_Parameter1>
+      <_Parameter2>$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\sdk-output\</_Parameter2>
+    </AssemblyAttribute>
+
+     <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>BlazorWebAssemblySdkDirectoryRoot</_Parameter1>
+      <_Parameter2>$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\sdk-output\</_Parameter2>
+    </AssemblyAttribute>
+
+    <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
+      <_Parameter1>TestAppsRoot</_Parameter1>
+      <_Parameter2>$(TestAppsRoot)</_Parameter2>
+    </AssemblyAttribute>
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="$(MSBuildThisFileDirectory)*.cs" LinkBase="Infrastructure" />
+  </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessKind.cs b/src/Shared/MSBuild.Testing/MSBuildProcessKind.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessKind.cs
rename to src/Shared/MSBuild.Testing/MSBuildProcessKind.cs
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessManager.cs b/src/Shared/MSBuild.Testing/MSBuildProcessManager.cs
similarity index 98%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessManager.cs
rename to src/Shared/MSBuild.Testing/MSBuildProcessManager.cs
index cc79523d5bf16ce44c125e247f37a2e5cfa53349..3d9ada158bf8d1d74f17392bd469610de31fa74a 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessManager.cs
+++ b/src/Shared/MSBuild.Testing/MSBuildProcessManager.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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;
@@ -37,6 +37,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
                 $"/p:MicrosoftNETCoreAppRuntimeVersion={BuildVariables.MicrosoftNETCoreAppRuntimeVersion}",
                 $"/p:MicrosoftNetCompilersToolsetPackageVersion={BuildVariables.MicrosoftNetCompilersToolsetPackageVersion}",
                 $"/p:RazorSdkDirectoryRoot={BuildVariables.RazorSdkDirectoryRoot}",
+                $"/p:BlazorWebAssemblySdkDirectoryRoot={BuildVariables.BlazorWebAssemblySdkDirectoryRoot}",
                 $"/p:RepoRoot={BuildVariables.RepoRoot}",
                 $"/p:Configuration={project.Configuration}",
                 $"/t:{target}",
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildResult.cs b/src/Shared/MSBuild.Testing/MSBuildResult.cs
similarity index 100%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildResult.cs
rename to src/Shared/MSBuild.Testing/MSBuildResult.cs
diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ProjectDirectory.cs b/src/Shared/MSBuild.Testing/ProjectDirectory.cs
similarity index 97%
rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ProjectDirectory.cs
rename to src/Shared/MSBuild.Testing/ProjectDirectory.cs
index d0d0e571f46ba988504ce7a654e69324356ced70..0f669c31fceeb36e5cc7ed55ffbf1be38d6f931f 100644
--- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ProjectDirectory.cs
+++ b/src/Shared/MSBuild.Testing/ProjectDirectory.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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;
@@ -49,12 +49,11 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
                 }
 
                 var repositoryRoot = BuildVariables.RepoRoot;
-                var solutionRoot = Path.Combine(repositoryRoot, "src", "Razor");
                 var binariesRoot = Path.GetDirectoryName(typeof(ProjectDirectory).Assembly.Location);
+                var testAppsRoot = BuildVariables.TestAppsRoot;
 
                 foreach (var project in new string[] { originalProjectName, }.Concat(additionalProjects))
                 {
-                    var testAppsRoot = Path.Combine(solutionRoot, "test", "testassets");
                     var projectRoot = Path.Combine(testAppsRoot, project);
                     if (!Directory.Exists(projectRoot))
                     {
@@ -146,6 +145,11 @@ $@"<Project>
                     .ForEach(file =>
                     {
                         var source = Path.Combine(testAppsRoot, file);
+                        if (!File.Exists(source))
+                        {
+                            return;
+                        }
+
                         var destination = Path.Combine(projectDestination, file);
                         File.Copy(source, destination);
                     });
diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java
index 58a95445c87f3f470f46cc6bb4b9a297173ba9d8..8f4f4f0df91d1c9a8c8acf701a18739bc45a6e94 100644
--- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java
+++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java
@@ -19,14 +19,14 @@ import okhttp3.*;
 final class DefaultHttpClient extends HttpClient {
     private OkHttpClient client = null;
 
-    public DefaultHttpClient() {
-        this(0, null);
+    public DefaultHttpClient(Action1<OkHttpClient.Builder> configureBuilder) {
+        this(null, configureBuilder);
     }
 
     public DefaultHttpClient cloneWithTimeOut(int timeoutInMilliseconds) {
         OkHttpClient newClient = client.newBuilder().readTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS)
                 .build();
-        return new DefaultHttpClient(timeoutInMilliseconds, newClient);
+        return new DefaultHttpClient(newClient, null);
     }
 
     @Override
@@ -36,7 +36,7 @@ final class DefaultHttpClient extends HttpClient {
         }
     }
 
-    public DefaultHttpClient(int timeoutInMilliseconds, OkHttpClient client) {
+    public DefaultHttpClient(OkHttpClient client, Action1<OkHttpClient.Builder> configureBuilder) {
         if (client != null) {
             this.client = client;
         } else {
@@ -90,9 +90,10 @@ final class DefaultHttpClient extends HttpClient {
                 }
             });
 
-            if (timeoutInMilliseconds > 0) {
-                builder.readTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS);
+            if (configureBuilder != null) {
+                configureBuilder.invoke(builder);
             }
+
             this.client = builder.build();
         }
     }
diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java
index d91e382ed915d284e73a2a2691bd6b80f9b0c14b..e1f38e6888a43a4c950a10e1448ac6d6b657e2d9 100644
--- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java
+++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java
@@ -7,6 +7,7 @@ import java.util.HashMap;
 import java.util.Map;
 
 import io.reactivex.Single;
+import okhttp3.OkHttpClient;
 
 /**
  * A builder for configuring {@link HubConnection} instances.
@@ -20,6 +21,7 @@ public class HttpHubConnectionBuilder {
     private long handshakeResponseTimeout = 0;
     private Map<String, String> headers;
     private TransportEnum transportEnum;
+    private Action1<OkHttpClient.Builder> configureBuilder;
 
     HttpHubConnectionBuilder(String url) {
         this.url = url;
@@ -113,12 +115,25 @@ public class HttpHubConnectionBuilder {
         return this;
     }
 
+    /**
+     * Sets a method that will be called when constructing the HttpClient to allow customization such as certificate validation, proxies, and cookies.
+     * By default the client will have a cookie jar added and a read timeout for LongPolling.
+     *
+     * @param configureBuilder Callback for configuring the OkHttpClient.Builder.
+     * @return This instance of the HttpHubConnectionBuilder.
+     */
+    public HttpHubConnectionBuilder setHttpClientBuilderCallback(Action1<OkHttpClient.Builder> configureBuilder) {
+        this.configureBuilder = configureBuilder;
+        return this;
+    }
+
     /**
      * Builds a new instance of {@link HubConnection}.
      *
      * @return A new instance of {@link HubConnection}.
      */
     public HubConnection build() {
-        return new HubConnection(url, transport, skipNegotiate, httpClient, accessTokenProvider, handshakeResponseTimeout, headers, transportEnum);
+        return new HubConnection(url, transport, skipNegotiate, httpClient, accessTokenProvider,
+            handshakeResponseTimeout, headers, transportEnum, configureBuilder);
     }
 }
diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java
index 5c884cfff142397667c606406dc70c7fcc57c6bf..addbd5c2f6aea2e6173862028dc3f032b7e378af 100644
--- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java
+++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java
@@ -20,6 +20,7 @@ import io.reactivex.Completable;
 import io.reactivex.Observable;
 import io.reactivex.Single;
 import io.reactivex.subjects.*;
+import okhttp3.OkHttpClient;
 
 /**
  * A connection used to invoke hub methods on a SignalR Server.
@@ -126,7 +127,8 @@ public class HubConnection implements AutoCloseable {
     }
 
     HubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient httpClient,
-                  Single<String> accessTokenProvider, long handshakeResponseTimeout, Map<String, String> headers, TransportEnum transportEnum) {
+                  Single<String> accessTokenProvider, long handshakeResponseTimeout, Map<String, String> headers, TransportEnum transportEnum,
+                  Action1<OkHttpClient.Builder> configureBuilder) {
         if (url == null || url.isEmpty()) {
             throw new IllegalArgumentException("A valid url is required.");
         }
@@ -143,7 +145,7 @@ public class HubConnection implements AutoCloseable {
         if (httpClient != null) {
             this.httpClient = httpClient;
         } else {
-            this.httpClient = new DefaultHttpClient();
+            this.httpClient = new DefaultHttpClient(configureBuilder);
         }
 
         if (transport != null) {
diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java
index 5edda35267c5c1e79d81f9e56818dc0689477d1f..4aeec16836ee34ca11acfd801d3544043b6edd84 100644
--- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java
+++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java
@@ -16,13 +16,6 @@ import io.reactivex.Completable;
 import io.reactivex.Single;
 
 class WebSocketTransportTest {
-    // @Test Skipping until we add functional test support
-    public void WebSocketThrowsIfItCantConnect() {
-        Transport transport = new WebSocketTransport(new HashMap<>(), new DefaultHttpClient());
-        RuntimeException exception = assertThrows(RuntimeException.class, () -> transport.start("http://url.fake.example").blockingAwait(1, TimeUnit.SECONDS));
-        assertEquals("There was an error starting the WebSocket transport.", exception.getMessage());
-    }
-
     @Test
     public void CanPassNullExitCodeToOnClosed() {
         WebSocketTransport transport = new WebSocketTransport(new HashMap<>(), new WebSocketTestHttpClient());