Skip to content

[Blazor] Prepare for custom cache removal #61666

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -470,10 +470,6 @@ function isDescriptorInDocument(descriptor: ComponentDescriptor): boolean {
}

function areWebAssemblyResourcesLikelyCached(config: MonoConfig): boolean {
if (!config.cacheBootResources) {
return false;
}

const hash = getWebAssemblyResourceHash(config);
if (!hash) {
return false;
Expand Down
3 changes: 3 additions & 0 deletions src/Components/WebAssembly/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
<PropertyGroup>
<WasmPreloadAssets>false</WasmPreloadAssets>
</PropertyGroup>
<PropertyGroup>
<BlazorCacheBootResources>false</BlazorCacheBootResources>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ public class BootResourceRequestLog
{
private readonly ConcurrentBag<string> _requestPaths = new ConcurrentBag<string>();

public IReadOnlyCollection<string> RequestPaths => _requestPaths;
public IReadOnlyCollection<string> RequestPathsWithNewContent => _requestPaths;

public void AddRequest(HttpRequest request)
public void AddRequest(string originalRequestPath, HttpResponse response)
{
_requestPaths.Add(request.Path);
if (response.StatusCode != StatusCodes.Status304NotModified)
{
_requestPaths.Add(originalRequestPath);
}
}

public void Clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,18 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, BootReso
{
var mapAlternativePathApp = Configuration.GetValue<bool>("UseAlternativeBasePath");
var mapAllApps = Configuration.GetValue<bool>("MapAllApps");
app.Use((context, next) =>
app.Use(async (context, next) =>
{
string originalRequestPath = context.Request.Path;
await next(context);

// This is used by E2E tests to verify that the correct resources were fetched,
// and that it was possible to override the loading mechanism
if (context.Request.Query.ContainsKey("customizedbootresource")
|| context.Request.Headers.ContainsKey("customizedbootresource")
|| context.Request.Path.Value.EndsWith("/blazor.boot.json", StringComparison.Ordinal))
|| context.Request.Headers.ContainsKey("customizedbootresource"))
{
bootResourceRequestLog.AddRequest(context.Request);
bootResourceRequestLog.AddRequest(originalRequestPath, context.Response);
}
return next(context);
});

if (env.IsDevelopment())
Expand Down
108 changes: 2 additions & 106 deletions src/Components/test/E2ETest/Tests/BootResourceCachingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests;
public partial class BootResourceCachingTest
: ServerTestBase<AspNetSiteServerFixture>
{
// The cache name is derived from the application's base href value (in this case, '/')
private const string CacheName = "dotnet-resources-/";

public BootResourceCachingTest(
BrowserFixture browserFixture,
AspNetSiteServerFixture serverFixture,
Expand Down Expand Up @@ -56,114 +53,13 @@ public void CachesResourcesAfterFirstLoad()
Navigate("/");
WaitUntilLoaded();
var subsequentResourcesRequested = GetAndClearRequestedPaths();
Assert.DoesNotContain(subsequentResourcesRequested, path =>
path.Contains("/dotnet.native.", StringComparison.Ordinal) &&
path.EndsWith(".wasm", StringComparison.Ordinal));
Assert.NotEmpty(subsequentResourcesRequested.Where(path => path.EndsWith(".js", StringComparison.Ordinal)));
Assert.DoesNotContain(subsequentResourcesRequested, path =>
!path.Contains("/dotnet.native.", StringComparison.Ordinal) &&
path.EndsWith(".wasm", StringComparison.Ordinal));
}

[Fact]
public async Task IncrementallyUpdatesCache()
{
// Perform a first load to populate the cache
Navigate("/");
WaitUntilLoaded();
var cacheEntryUrls1 = GetCacheEntryUrls();
var cacheEntryForComponentsDll = cacheEntryUrls1.Single(IsFingerprintedComponentsEntry);
var cacheEntryForDotNetWasm = cacheEntryUrls1.Single(IsFingerprintedDotNetWasmEntry);
var cacheEntryForDotNetWasmWithChangedHash = cacheEntryForDotNetWasm.Replace(".sha256-", ".sha256-different");

// Remove some items we do need, and add an item we don't need
RemoveCacheEntry(cacheEntryForComponentsDll);
RemoveCacheEntry(cacheEntryForDotNetWasm);
AddCacheEntry(cacheEntryForDotNetWasmWithChangedHash, "ignored content");
var cacheEntryUrls2 = GetCacheEntryUrls();
Assert.DoesNotContain(cacheEntryForComponentsDll, cacheEntryUrls2);
Assert.DoesNotContain(cacheEntryForDotNetWasm, cacheEntryUrls2);
Assert.Contains(cacheEntryForDotNetWasmWithChangedHash, cacheEntryUrls2);

// On the next load, we'll fetch only the items we need (not things already cached)
GetAndClearRequestedPaths();
Navigate("about:blank");
Browser.Equal(string.Empty, () => Browser.Title);
Navigate("/");
WaitUntilLoaded();
var subsequentResourcesRequested = GetAndClearRequestedPaths();
Assert.Collection(subsequentResourcesRequested.Where(url => url.Contains(".wasm")),
requestedDll => Assert.True(IsFingerprintedComponentsEntry(requestedDll)),
requestedDll => Assert.True(IsFingerprintedDotNetWasmEntry(requestedDll)));

var cacheEntryUrls3 = GetCacheEntryUrls();
// wait until the cache was cleaned, max 500ms
for (var i = 0; i < 5; i++)
{
if (!cacheEntryUrls3.Contains(cacheEntryForDotNetWasmWithChangedHash))
{
break;
}
await Task.Delay(100); // wait for cache purge
cacheEntryUrls3 = GetCacheEntryUrls();
}
Assert.Contains(cacheEntryForComponentsDll, cacheEntryUrls3);
Assert.Contains(cacheEntryForDotNetWasm, cacheEntryUrls3);
Assert.DoesNotContain(cacheEntryForDotNetWasmWithChangedHash, cacheEntryUrls3);
}

[GeneratedRegex("/Microsoft\\.AspNetCore\\.Components\\.\\w*\\.wasm")]
private static partial Regex GetFingerprintedComponentsEntryRegex { get; }

[GeneratedRegex("/dotnet\\.native\\.\\w*\\.wasm")]
private static partial Regex GetFingerprintedDotNetWasmEntryRegex { get; }

private static bool IsFingerprintedComponentsEntry(string url)
=> GetFingerprintedComponentsEntryRegex.IsMatch(url);

private static bool IsFingerprintedDotNetWasmEntry(string url)
=> GetFingerprintedDotNetWasmEntryRegex.IsMatch(url);

private IReadOnlyCollection<string> GetCacheEntryUrls()
{
var js = @"
(async function(cacheName, completedCallback) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
const urls = keys.map(r => r.url);
completedCallback(urls);
}).apply(null, arguments)";
var jsExecutor = (IJavaScriptExecutor)Browser;
var result = (IEnumerable<object>)jsExecutor.ExecuteAsyncScript(js, CacheName);
return result.Cast<string>().ToList();
}

private void RemoveCacheEntry(string url)
{
var js = @"
(async function(cacheName, urlToRemove, completedCallback) {
const cache = await caches.open(cacheName);
await cache.delete(urlToRemove);
completedCallback();
}).apply(null, arguments)";
((IJavaScriptExecutor)Browser).ExecuteAsyncScript(js, CacheName, url);
}

private void AddCacheEntry(string url, string content)
{
var js = @"
(async function(cacheName, urlToAdd, contentToAdd, completedCallback) {
const cache = await caches.open(cacheName);
await cache.put(urlToAdd, new Response(contentToAdd));
completedCallback();
}).apply(null, arguments)";
((IJavaScriptExecutor)Browser).ExecuteAsyncScript(js, CacheName, url, content);
Assert.Empty(subsequentResourcesRequested);
}

private IReadOnlyCollection<string> GetAndClearRequestedPaths()
{
var requestLog = _serverFixture.Host.Services.GetRequiredService<BootResourceRequestLog>();
var result = requestLog.RequestPaths.ToList();
var result = requestLog.RequestPathsWithNewContent.ToList();
requestLog.Clear();
return result;
}
Expand Down
3 changes: 3 additions & 0 deletions src/Components/test/testassets/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@
<PropertyGroup>
<WasmPreloadAssets>false</WasmPreloadAssets>
</PropertyGroup>
<PropertyGroup>
<BlazorCacheBootResources>false</BlazorCacheBootResources>
</PropertyGroup>
</Project>
Loading