diff --git a/src/Components/Web.JS/src/Platform/WebAssemblyResourceLoader.ts b/src/Components/Web.JS/src/Platform/WebAssemblyResourceLoader.ts index 5ab3364d5b..f1e6690ee5 100644 --- a/src/Components/Web.JS/src/Platform/WebAssemblyResourceLoader.ts +++ b/src/Components/Web.JS/src/Platform/WebAssemblyResourceLoader.ts @@ -85,7 +85,14 @@ export class WebAssemblyResourceLoader { const cacheKey = toAbsoluteUri(`${url}.${contentHash}`); this.usedCacheKeys[cacheKey] = true; - const cachedResponse = await cache.match(cacheKey); + let cachedResponse: Response | undefined; + try { + cachedResponse = await cache.match(cacheKey); + } catch { + // Be tolerant to errors reading from the cache. This is a guard for https://bugs.chromium.org/p/chromium/issues/detail?id=968444 where + // chromium browsers may sometimes throw when working with the cache. + } + if (cachedResponse) { // It's in the cache. const responseBytes = parseInt(cachedResponse.headers.get('content-length') || '0'); @@ -136,12 +143,19 @@ export class WebAssemblyResourceLoader { // Add to cache as a custom response object so we can track extra data such as responseBytes // We can't rely on the server sending content-length (ASP.NET Core doesn't by default) - await cache.put(cacheKey, new Response(responseData, { + const responseToCache = new Response(responseData, { headers: { 'content-type': response.headers.get('content-type') || '', 'content-length': (responseBytes || response.headers.get('content-length') || '').toString() } - })); + }); + + try { + await cache.put(cacheKey, responseToCache); + } catch { + // Be tolerant to errors writing to the cache. This is a guard for https://bugs.chromium.org/p/chromium/issues/detail?id=968444 where + // chromium browsers may sometimes throw when performing cache operations. + } } } diff --git a/src/Components/WebAssembly/Build/src/targets/ServiceWorkerAssetsManifest.targets b/src/Components/WebAssembly/Build/src/targets/ServiceWorkerAssetsManifest.targets index 3a72b4e62a..c17edf60e0 100644 --- a/src/Components/WebAssembly/Build/src/targets/ServiceWorkerAssetsManifest.targets +++ b/src/Components/WebAssembly/Build/src/targets/ServiceWorkerAssetsManifest.targets @@ -163,7 +163,7 @@ diff --git a/src/Components/WebAssembly/Build/test/BuildIntegrationTests/PwaManifestTests.cs b/src/Components/WebAssembly/Build/test/BuildIntegrationTests/PwaManifestTests.cs index a268e8f8ea..464d361e70 100644 --- a/src/Components/WebAssembly/Build/test/BuildIntegrationTests/PwaManifestTests.cs +++ b/src/Components/WebAssembly/Build/test/BuildIntegrationTests/PwaManifestTests.cs @@ -1,6 +1,7 @@ using System.IO; using System.Linq; using System.Text.Json; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Xunit; @@ -41,5 +42,77 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build var entries = assets.EnumerateArray().Select(e => e.GetProperty("url").GetString()).OrderBy(e => e).ToArray(); Assert.All(entries, e => expectedExtensions.Contains(Path.GetExtension(e))); } + + [Fact] + public async Task Publish_UpdatesServiceWorkerVersionHash_WhenSourcesChange() + { + // Arrange + using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" }); + var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", args: "/bl:initial.binlog /p:ServiceWorkerAssetsManifest=service-worker-assets.js"); + + Assert.BuildPassed(result); + + var publishOutputDirectory = project.PublishOutputDirectory; + + var serviceWorkerFile = Assert.FileExists(result, publishOutputDirectory, "wwwroot", "serviceworkers", "my-service-worker.js"); + var version = File.ReadAllLines(serviceWorkerFile).Last(); + var match = Regex.Match(version, "\\/\\* Manifest version: (.{8}) \\*\\/"); + Assert.True(match.Success); + Assert.Equal(2, match.Groups.Count); + Assert.NotNull(match.Groups[1].Value); + var capture = match.Groups[1].Value; + + // Act + var cssFile = Path.Combine(project.DirectoryPath, "LinkToWebRoot", "css", "app.css"); + File.WriteAllText(cssFile, ".updated { }"); + + // Assert + var updatedResult = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", args: "/bl:updated.binlog /p:ServiceWorkerAssetsManifest=service-worker-assets.js"); + + Assert.BuildPassed(result); + + var updatedVersion = File.ReadAllLines(serviceWorkerFile).Last(); + var updatedMatch = Regex.Match(updatedVersion, "\\/\\* Manifest version: (.{8}) \\*\\/"); + Assert.True(updatedMatch.Success); + Assert.Equal(2, updatedMatch.Groups.Count); + Assert.NotNull(updatedMatch.Groups[1].Value); + var updatedCapture = updatedMatch.Groups[1].Value; + + Assert.NotEqual(capture, updatedCapture); + } + + [Fact] + public async Task Publish_DeterministicAcrossBuilds_WhenNoSourcesChange() + { + // Arrange + using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" }); + var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", args: "/bl:initial.binlog /p:ServiceWorkerAssetsManifest=service-worker-assets.js"); + + Assert.BuildPassed(result); + + var publishOutputDirectory = project.PublishOutputDirectory; + + var serviceWorkerFile = Assert.FileExists(result, publishOutputDirectory, "wwwroot", "serviceworkers", "my-service-worker.js"); + var version = File.ReadAllLines(serviceWorkerFile).Last(); + var match = Regex.Match(version, "\\/\\* Manifest version: (.{8}) \\*\\/"); + Assert.True(match.Success); + Assert.Equal(2, match.Groups.Count); + Assert.NotNull(match.Groups[1].Value); + var capture = match.Groups[1].Value; + + // Act && Assert + var updatedResult = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", args: "/bl:updated.binlog /p:ServiceWorkerAssetsManifest=service-worker-assets.js"); + + Assert.BuildPassed(result); + + var updatedVersion = File.ReadAllLines(serviceWorkerFile).Last(); + var updatedMatch = Regex.Match(updatedVersion, "\\/\\* Manifest version: (.{8}) \\*\\/"); + Assert.True(updatedMatch.Success); + Assert.Equal(2, updatedMatch.Groups.Count); + Assert.NotNull(updatedMatch.Groups[1].Value); + var updatedCapture = updatedMatch.Groups[1].Value; + + Assert.Equal(capture, updatedCapture); + } } } diff --git a/src/Mvc/Mvc.Formatters.Xml/src/XmlDataContractSerializerInputFormatter.cs b/src/Mvc/Mvc.Formatters.Xml/src/XmlDataContractSerializerInputFormatter.cs index 8132dfdba8..a2b32b2306 100644 --- a/src/Mvc/Mvc.Formatters.Xml/src/XmlDataContractSerializerInputFormatter.cs +++ b/src/Mvc/Mvc.Formatters.Xml/src/XmlDataContractSerializerInputFormatter.cs @@ -143,6 +143,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } readStream = new FileBufferingReadStream(request.Body, memoryThreshold); + // Ensure the file buffer stream is always disposed at the end of a request. + request.HttpContext.Response.RegisterForDispose(readStream); await readStream.DrainAsync(CancellationToken.None); readStream.Seek(0L, SeekOrigin.Begin); diff --git a/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerInputFormatter.cs b/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerInputFormatter.cs index 667bf14c01..3efca1f0ce 100644 --- a/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerInputFormatter.cs +++ b/src/Mvc/Mvc.Formatters.Xml/src/XmlSerializerInputFormatter.cs @@ -124,6 +124,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } readStream = new FileBufferingReadStream(request.Body, memoryThreshold); + // Ensure the file buffer stream is always disposed at the end of a request. + request.HttpContext.Response.RegisterForDispose(readStream); await readStream.DrainAsync(CancellationToken.None); readStream.Seek(0L, SeekOrigin.Begin); diff --git a/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonInputFormatter.cs b/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonInputFormatter.cs index ac26b61911..3528916b3e 100644 --- a/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonInputFormatter.cs +++ b/src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonInputFormatter.cs @@ -153,6 +153,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } readStream = new FileBufferingReadStream(request.Body, memoryThreshold); + // Ensure the file buffer stream is always disposed at the end of a request. + request.HttpContext.Response.RegisterForDispose(readStream); await readStream.DrainAsync(CancellationToken.None); readStream.Seek(0L, SeekOrigin.Begin); @@ -278,7 +280,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// /// Called during deserialization to get the . The formatter context - /// that is passed gives an ability to create serializer specific to the context. + /// that is passed gives an ability to create serializer specific to the context. /// /// The used during deserialization. /// @@ -297,7 +299,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// /// Called during deserialization to get the . The formatter context - /// that is passed gives an ability to create serializer specific to the context. + /// that is passed gives an ability to create serializer specific to the context. /// /// A context object used by an input formatter for deserializing the request body into an object. /// The used during deserialization. diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/Error.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/Error.cshtml similarity index 94% rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/Error.razor rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/Error.cshtml index cd87ea9518..6436437154 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/Error.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/Error.cshtml @@ -1,16 +1,16 @@ -@page "/error" - - -

Error.

-

An error occurred while processing your request.

- -

Development Mode

-

- Swapping to Development environment will display more detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

\ No newline at end of file +@page + + +

Error.

+

An error occurred while processing your request.

+ +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs index 43b03d7ca8..02dac7f0d3 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs @@ -25,13 +25,13 @@ namespace ComponentsWebAssembly_CSharp builder.RootComponents.Add("app"); #if (!Hosted || NoAuth) - builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); #else builder.Services.AddHttpClient("ComponentsWebAssembly_CSharp.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) .AddHttpMessageHandler(); // Supply HttpClient instances that include access tokens when making requests to the server project - builder.Services.AddTransient(sp => sp.GetRequiredService().CreateClient("ComponentsWebAssembly_CSharp.ServerAPI")); + builder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("ComponentsWebAssembly_CSharp.ServerAPI")); #endif #if(!NoAuth) diff --git a/src/ProjectTemplates/test/template-baselines.json b/src/ProjectTemplates/test/template-baselines.json index 5d70579fb6..bf2f88c6a7 100644 --- a/src/ProjectTemplates/test/template-baselines.json +++ b/src/ProjectTemplates/test/template-baselines.json @@ -914,7 +914,7 @@ "Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs", "Data/Migrations/ApplicationDbContextModelSnapshot.cs", "Pages/Counter.razor", - "Pages/Error.razor", + "Pages/Error.cshtml", "Pages/FetchData.razor", "Pages/Index.razor", "Pages/_Host.cshtml", @@ -951,7 +951,7 @@ "Data/WeatherForecast.cs", "Data/WeatherForecastService.cs", "Pages/Counter.razor", - "Pages/Error.razor", + "Pages/Error.cshtml", "Pages/FetchData.razor", "Pages/Index.razor", "Pages/_Host.cshtml", @@ -988,7 +988,7 @@ "Data/WeatherForecast.cs", "Data/WeatherForecastService.cs", "Pages/Counter.razor", - "Pages/Error.razor", + "Pages/Error.cshtml", "Pages/FetchData.razor", "Pages/Index.razor", "Pages/_Host.cshtml", @@ -1024,7 +1024,7 @@ "_Imports.razor", "Data/WeatherForecast.cs", "Data/WeatherForecastService.cs", - "Pages/Error.razor", + "Pages/Error.cshtml", "Pages/Counter.razor", "Pages/FetchData.razor", "Pages/Index.razor", @@ -1061,7 +1061,7 @@ "Data/WeatherForecast.cs", "Data/WeatherForecastService.cs", "Pages/Counter.razor", - "Pages/Error.razor", + "Pages/Error.cshtml", "Pages/FetchData.razor", "Pages/Index.razor", "Pages/_Host.cshtml", @@ -1098,7 +1098,7 @@ "Data/WeatherForecast.cs", "Data/WeatherForecastService.cs", "Pages/Counter.razor", - "Pages/Error.razor", + "Pages/Error.cshtml", "Pages/FetchData.razor", "Pages/Index.razor", "Pages/_Host.cshtml",