From 296921cd4cd43b2f2f8bcc4d4358eba92f2b325f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:30:25 +0000 Subject: [PATCH 1/2] Initial plan From 28c30c51a7cca132b6a1967ae7987eb57bc97387 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:56:39 +0000 Subject: [PATCH 2/2] Better errors for Blazor asset integrity problems - Wrap ResourceCollectionProvider.LoadResourceCollection() in try-catch to provide descriptive error when resource collection fails to load (e.g., due to integrity check failures) - Fix HostedServiceExecutor.StopAsync() to include a message in the AggregateException instead of using the default (which shows as resource key AggregateException_ctor_DefaultMessage) - Add tests for both changes Co-authored-by: oroztocil <79744616+oroztocil@users.noreply.github.com> --- .../Shared/src/ResourceCollectionProvider.cs | 17 +++- .../src/Hosting/HostedServiceExecutor.cs | 2 +- .../test/Hosting/HostedServiceExecutorTest.cs | 58 ++++++++++++++ .../test/ResourceCollectionProviderTest.cs | 78 +++++++++++++++++++ 4 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 src/Components/WebAssembly/WebAssembly/test/Hosting/HostedServiceExecutorTest.cs create mode 100644 src/Components/WebAssembly/WebAssembly/test/ResourceCollectionProviderTest.cs diff --git a/src/Components/Shared/src/ResourceCollectionProvider.cs b/src/Components/Shared/src/ResourceCollectionProvider.cs index 2084abdb3e53..e1f4c4c34fa6 100644 --- a/src/Components/Shared/src/ResourceCollectionProvider.cs +++ b/src/Components/Shared/src/ResourceCollectionProvider.cs @@ -54,8 +54,19 @@ private async Task LoadResourceCollection() return ResourceAssetCollection.Empty; } - var module = await _jsRuntime.InvokeAsync("import", _url); - var result = await module.InvokeAsync("get"); - return result == null ? ResourceAssetCollection.Empty : new ResourceAssetCollection(result); + try + { + var module = await _jsRuntime.InvokeAsync("import", _url); + var result = await module.InvokeAsync("get"); + return result == null ? ResourceAssetCollection.Empty : new ResourceAssetCollection(result); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to load the Blazor resource collection from '{_url}'. " + + "This is likely caused by a mismatch in the file integrity check, which can happen when files are modified after they are published. " + + "Ensure that all published files are deployed correctly and that none have been modified after publish.", + ex); + } } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/HostedServiceExecutor.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/HostedServiceExecutor.cs index a1866a988647..25f2890a4098 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/HostedServiceExecutor.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/HostedServiceExecutor.cs @@ -45,7 +45,7 @@ public async Task StopAsync(CancellationToken token) // Throw an aggregate exception if there were any exceptions if (exceptions is not null) { - var aggregateException = new AggregateException(exceptions); + var aggregateException = new AggregateException("One or more hosted services failed to stop.", exceptions); try { Log.ErrorStoppingHostedServices(_logger, aggregateException); diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/HostedServiceExecutorTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/HostedServiceExecutorTest.cs new file mode 100644 index 000000000000..7361b56dac47 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/HostedServiceExecutorTest.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +public class HostedServiceExecutorTest +{ + [Fact] + public async Task StopAsync_WithFailingServices_ThrowsAggregateExceptionWithDescriptiveMessage() + { + // Arrange + var services = new[] + { + new FaultyHostedService("Service 1 error"), + new FaultyHostedService("Service 2 error"), + }; + var executor = new HostedServiceExecutor(services, NullLogger.Instance); + + // Act + var ex = await Assert.ThrowsAsync(() => executor.StopAsync(CancellationToken.None)); + + // Assert + Assert.StartsWith("One or more hosted services failed to stop.", ex.Message); + Assert.Equal(2, ex.InnerExceptions.Count); + } + + [Fact] + public async Task StopAsync_WithNoFailingServices_DoesNotThrow() + { + // Arrange + var services = new[] + { + new SuccessfulHostedService(), + new SuccessfulHostedService(), + }; + var executor = new HostedServiceExecutor(services, NullLogger.Instance); + + // Act (should not throw) + await executor.StopAsync(CancellationToken.None); + } + + private class FaultyHostedService(string errorMessage) : IHostedService + { + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => + Task.FromException(new InvalidOperationException(errorMessage)); + } + + private class SuccessfulHostedService : IHostedService + { + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/src/Components/WebAssembly/WebAssembly/test/ResourceCollectionProviderTest.cs b/src/Components/WebAssembly/WebAssembly/test/ResourceCollectionProviderTest.cs new file mode 100644 index 000000000000..48081c82d52d --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/test/ResourceCollectionProviderTest.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.JSInterop; +using Moq; + +namespace Microsoft.AspNetCore.Components; + +public class ResourceCollectionProviderTest +{ + [Fact] + public async Task GetResourceCollection_WhenUrlIsNull_ReturnsEmpty() + { + // Arrange + var jsRuntime = Mock.Of(); + var provider = new ResourceCollectionProvider(jsRuntime); + + // Act + var result = await provider.GetResourceCollection(); + + // Assert + Assert.Same(ResourceAssetCollection.Empty, result); + } + + [Fact] + public async Task GetResourceCollection_WhenImportFails_ThrowsDescriptiveError() + { + // Arrange + var url = "/_framework/resource-collection.abc123.js"; + var jsRuntime = new Mock(); + jsRuntime + .Setup(r => r.InvokeAsync("import", It.IsAny())) + .ThrowsAsync(new JSException("Failed to fetch dynamically imported module")); + + var provider = new ResourceCollectionProvider(jsRuntime.Object); + provider.ResourceCollectionUrl = url; + + // Act + var ex = await Assert.ThrowsAsync( + () => provider.GetResourceCollection()); + + // Assert + Assert.Contains($"Failed to load the Blazor resource collection from '{url}'", ex.Message); + Assert.Contains("integrity", ex.Message, StringComparison.OrdinalIgnoreCase); + Assert.NotNull(ex.InnerException); + Assert.IsType(ex.InnerException); + } + + [Fact] + public async Task GetResourceCollection_WhenGetFails_ThrowsDescriptiveError() + { + // Arrange + var url = "/_framework/resource-collection.abc123.js"; + var jsRuntime = new Mock(); + var moduleReference = new Mock(); + + jsRuntime + .Setup(r => r.InvokeAsync("import", It.IsAny())) + .ReturnsAsync(moduleReference.Object); + + moduleReference + .Setup(m => m.InvokeAsync("get", It.IsAny())) + .ThrowsAsync(new JSException("An error occurred")); + + var provider = new ResourceCollectionProvider(jsRuntime.Object); + provider.ResourceCollectionUrl = url; + + // Act + var ex = await Assert.ThrowsAsync( + () => provider.GetResourceCollection()); + + // Assert + Assert.Contains($"Failed to load the Blazor resource collection from '{url}'", ex.Message); + Assert.Contains("integrity", ex.Message, StringComparison.OrdinalIgnoreCase); + Assert.NotNull(ex.InnerException); + Assert.IsType(ex.InnerException); + } +}