Skip to content

Commit b8a8adf

Browse files
authored
[Static Assets] Consume original-resource instead of the Etag (#63379)
* tmp * Consume original-resource * Update test
1 parent cd1d256 commit b8a8adf

File tree

2 files changed

+73
-24
lines changed

2 files changed

+73
-24
lines changed

src/StaticAssets/src/Development/StaticAssetDevelopmentRuntimeHandler.cs

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Globalization;
56
using System.IO.Compression;
67
using System.IO.Pipelines;
@@ -22,20 +23,34 @@
2223
namespace Microsoft.AspNetCore.Builder;
2324

2425
// Handles changes during development to support common scenarios where for example, a developer changes a file in the wwwroot folder.
25-
internal sealed partial class StaticAssetDevelopmentRuntimeHandler(List<StaticAssetDescriptor> descriptors)
26+
internal sealed partial class StaticAssetDevelopmentRuntimeHandler
2627
{
2728
internal const string ReloadStaticAssetsAtRuntimeKey = "ReloadStaticAssetsAtRuntime";
2829

30+
private readonly Dictionary<(string Route, string ETag), StaticAssetDescriptor> _descriptorsMap = [];
31+
32+
public StaticAssetDevelopmentRuntimeHandler(List<StaticAssetDescriptor> descriptors)
33+
{
34+
CreateDescriptorMap(descriptors);
35+
}
36+
2937
public void AttachRuntimePatching(EndpointBuilder builder)
3038
{
3139
var original = builder.RequestDelegate!;
3240
var asset = builder.Metadata.OfType<StaticAssetDescriptor>().Single();
3341
if (asset.HasContentEncoding())
3442
{
35-
// This is a compressed asset, which might get out of "sync" with the original uncompressed version.
36-
// We are going to find the original by using the weak etag from this compressed asset and locating an asset with the same etag.
37-
var eTag = asset.GetWeakETag();
38-
asset = FindOriginalAsset(eTag.Tag.Value!, descriptors);
43+
var originalETag = GetDescriptorOriginalResourceProperty(asset);
44+
StaticAssetDescriptor? originalAsset = null;
45+
if (originalETag is not null && _descriptorsMap.TryGetValue((asset.Route, originalETag), out originalAsset))
46+
{
47+
asset = originalAsset;
48+
}
49+
else
50+
{
51+
Debug.Assert(originalETag != null, $"The static asset descriptor {asset.Route} - {asset.AssetPath} does not have an original-resource property.");
52+
Debug.Assert(originalAsset != null, $"The static asset descriptor {asset.Route} - {asset.AssetPath} has an original-resource property that does not match any known static asset descriptor.");
53+
}
3954
}
4055

4156
builder.RequestDelegate = async context =>
@@ -57,6 +72,57 @@ public void AttachRuntimePatching(EndpointBuilder builder)
5772
};
5873
}
5974

75+
private static string? GetDescriptorOriginalResourceProperty(StaticAssetDescriptor descriptor)
76+
{
77+
for (var i = 0; i < descriptor.Properties.Count; i++)
78+
{
79+
var property = descriptor.Properties[i];
80+
if (string.Equals(property.Name, "original-resource", StringComparison.OrdinalIgnoreCase))
81+
{
82+
return property.Value;
83+
}
84+
}
85+
86+
return null;
87+
}
88+
89+
private static string? GetDescriptorETagResponseHeader(StaticAssetDescriptor descriptor)
90+
{
91+
for (var i = 0; i < descriptor.ResponseHeaders.Count; i++)
92+
{
93+
var header = descriptor.ResponseHeaders[i];
94+
if (string.Equals(header.Name, HeaderNames.ETag, StringComparison.OrdinalIgnoreCase))
95+
{
96+
return header.Value;
97+
}
98+
}
99+
100+
return null;
101+
}
102+
103+
private void CreateDescriptorMap(List<StaticAssetDescriptor> descriptors)
104+
{
105+
for (var i = 0; i < descriptors.Count; i++)
106+
{
107+
var descriptor = descriptors[i];
108+
if (descriptor.HasContentEncoding())
109+
{
110+
continue;
111+
}
112+
var etag = GetDescriptorETagResponseHeader(descriptor);
113+
if (etag != null && !_descriptorsMap.ContainsKey((descriptor.Route, etag)))
114+
{
115+
_descriptorsMap[(descriptor.Route, etag)] = descriptor;
116+
}
117+
else
118+
{
119+
Debug.Assert(etag != null, $"The static asset descriptor {descriptor.Route} - {descriptor.AssetPath} does not have an ETag response header.");
120+
Debug.Assert(_descriptorsMap.ContainsKey((descriptor.Route, etag)),
121+
$"The static asset descriptor {descriptor.Route} - {descriptor.AssetPath} has an ETag response header that is already registered in the map. This should not happen, as the ETag should be unique for each static asset.");
122+
}
123+
}
124+
}
125+
60126
internal static string GetETag(IFileInfo fileInfo)
61127
{
62128
using var stream = fileInfo.CreateReadStream();
@@ -114,10 +180,7 @@ public Task SendFileAsync(string path, long offset, long? count, CancellationTok
114180
_context.Response.Headers.ContentLength = stream.Length;
115181

116182
var eTag = Convert.ToBase64String(SHA256.HashData(stream));
117-
var weakETag = $"W/{GetETag(fileInfo)}";
118-
119-
// Here we add the ETag for the Gzip stream as well as the weak ETag for the original asset.
120-
_context.Response.Headers.ETag = new StringValues([$"\"{eTag}\"", weakETag]);
183+
_context.Response.Headers.ETag = new StringValues($"\"{eTag}\"");
121184

122185
stream.Seek(0, SeekOrigin.Begin);
123186
return stream.CopyToAsync(_context.Response.Body, cancellationToken);
@@ -142,19 +205,6 @@ public Task StartAsync(CancellationToken cancellationToken = default)
142205
}
143206
}
144207

145-
private static StaticAssetDescriptor FindOriginalAsset(string tag, List<StaticAssetDescriptor> descriptors)
146-
{
147-
for (var i = 0; i < descriptors.Count; i++)
148-
{
149-
if (descriptors[i].HasETag(tag))
150-
{
151-
return descriptors[i];
152-
}
153-
}
154-
155-
throw new InvalidOperationException("The original asset was not found.");
156-
}
157-
158208
internal static bool IsEnabled(bool isBuildManifest, IServiceProvider serviceProvider)
159209
{
160210
var config = serviceProvider.GetRequiredService<IConfiguration>();

src/StaticAssets/test/StaticAssetsIntegrationTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,13 +519,12 @@ private static void CreateTestManifest(string appName, string webRoot, params Sp
519519
Route = resource.Path,
520520
AssetPath = $"{resource.Path}.gz",
521521
Selectors = [new StaticAssetSelector("Content-Encoding", "gzip", "1.0")],
522-
Properties = [],
522+
Properties = [new("original-resource", $"\"{GetEtag(resource.Content)}\"")],
523523
ResponseHeaders = [
524524
new ("Accept-Ranges", "bytes"),
525525
new ("Content-Type", GetContentType(filePath)),
526526

527527
new ("Content-Length", length.ToString(CultureInfo.InvariantCulture)),
528-
new ("ETag", $"W/\"{GetEtag(resource.Content)}\""),
529528
new ("ETag", $"\"{GetEtagForFile(compressedFilePath)}\""),
530529
new ("Last-Modified", lastModified.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture)),
531530

0 commit comments

Comments
 (0)