diff --git a/eng/Versions.props b/eng/Versions.props
index 9fda53fc996b..a429a4f553c4 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -248,9 +248,9 @@
0.192.0
3.0.0
7.2.2
- 4.0.0-beta4
- 91.0.4472.1900
- 4.0.0-beta4
+ 4.0.0-rc1
+ 93.0.4577.1500
+ 4.0.0-rc1
1.4.0
4.0.0
2.2.4
diff --git a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj
index 142f928723f9..5ac8032197c4 100644
--- a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj
+++ b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs b/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs
index 24f537afc6e6..160aad77849c 100644
--- a/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs
+++ b/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs
@@ -57,6 +57,229 @@ public void GetFileInfoPrefixRespectsOsCaseSensitivity()
Assert.Equal(expectedResult, file.Exists);
}
+ [Theory]
+ [InlineData("/img/icon.png", true)]
+ [InlineData("/Img/hero.gif", true)]
+ // Note that we've changed the casing of the first segment
+ [InlineData("/Img/icon.png", false)]
+ [InlineData("/img/hero.gif", false)]
+ public void ParseWorksWithNodesThatOnlyDifferOnCasing(string path, bool exists)
+ {
+ exists = exists | OperatingSystem.IsWindows();
+ // Arrange
+ using var memoryStream = new MemoryStream();
+ using var writer = new StreamWriter(memoryStream);
+ writer.Write(@"{
+ ""ContentRoots"": [
+ ""D:\\path\\"",
+ ""D:\\other\\""
+ ],
+ ""Root"": {
+ ""Children"": {
+ ""img"": {
+ ""Children"": {
+ ""icon.png"": {
+ ""Asset"": {
+ ""ContentRootIndex"": 0,
+ ""SubPath"": ""icon.png""
+ }
+ }
+ }
+ },
+ ""Img"": {
+ ""Children"": {
+ ""hero.gif"": {
+ ""Asset"": {
+ ""ContentRootIndex"": 1,
+ ""SubPath"": ""hero.gif""
+ }
+ }
+ }
+ }
+ }
+ }
+}");
+ var first = new Mock();
+ first.Setup(s => s.GetFileInfo("icon.png")).Returns(new TestFileInfo() { Name = "icon.png", Exists = true });
+ var second = new Mock();
+ second.Setup(s => s.GetFileInfo("hero.gif")).Returns(new TestFileInfo() { Name = "hero.gif", Exists = true });
+
+ writer.Flush();
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ var manifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(memoryStream);
+ var comparer = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.PathComparer;
+
+ var provider = new ManifestStaticWebAssetFileProvider(
+ manifest,
+ contentRoot => contentRoot switch
+ {
+ "D:\\path\\" => first.Object,
+ "D:\\other\\" => second.Object,
+ _ => throw new InvalidOperationException("Unknown provider")
+ });
+
+ // Act
+ var file = provider.GetFileInfo(path);
+
+ // Assert
+ Assert.Equal(exists, file.Exists);
+ }
+
+ [Theory]
+ [InlineData("/img/Subdir/icon.png", true)]
+ [InlineData("/Img/subdir/hero.gif", true)]
+ // Note that we've changed the casing of the second segment
+ [InlineData("/img/subdir/icon.png", false)]
+ [InlineData("/Img/Subdir/hero.gif", false)]
+ public void ParseWorksWithMergesNodesRecursively(string path, bool exists)
+ {
+ // Arrange
+ exists = exists | OperatingSystem.IsWindows();
+ var firstLevelCount = OperatingSystem.IsWindows() ? 1 : 2;
+ using var memoryStream = new MemoryStream();
+ using var writer = new StreamWriter(memoryStream);
+ writer.Write(@"{
+ ""ContentRoots"": [
+ ""D:\\path\\"",
+ ""D:\\other\\""
+ ],
+ ""Root"": {
+ ""Children"": {
+ ""img"": {
+ ""Children"": {
+ ""Subdir"": {
+ ""Children"": {
+ ""icon.png"": {
+ ""Asset"": {
+ ""ContentRootIndex"": 0,
+ ""SubPath"": ""icon.png""
+ }
+ }
+ }
+ }
+ }
+ },
+ ""Img"": {
+ ""Children"": {
+ ""subdir"": {
+ ""Children"": {
+ ""hero.gif"": {
+ ""Asset"": {
+ ""ContentRootIndex"": 1,
+ ""SubPath"": ""hero.gif""
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}");
+ var first = new Mock();
+ first.Setup(s => s.GetFileInfo("icon.png")).Returns(new TestFileInfo() { Name = "icon.png", Exists = true });
+ var second = new Mock();
+ second.Setup(s => s.GetFileInfo("hero.gif")).Returns(new TestFileInfo() { Name = "hero.gif", Exists = true });
+
+ writer.Flush();
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ var manifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(memoryStream);
+ var comparer = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.PathComparer;
+
+ var provider = new ManifestStaticWebAssetFileProvider(
+ manifest,
+ contentRoot => contentRoot switch
+ {
+ "D:\\path\\" => first.Object,
+ "D:\\other\\" => second.Object,
+ _ => throw new InvalidOperationException("Unknown provider")
+ });
+
+ // Act
+ var file = provider.GetFileInfo(path);
+
+ // Assert
+ Assert.Equal(exists, file.Exists);
+ Assert.Equal(firstLevelCount, manifest.Root.Children.Count);
+ Assert.All(manifest.Root.Children.Values, c => Assert.Single(c.Children));
+ }
+
+ [Theory]
+ [InlineData("/img/Subdir", true)]
+ [InlineData("/Img/subdir/hero.gif", true)]
+ // Note that we've changed the casing of the second segment
+ [InlineData("/img/subdir", false)]
+ [InlineData("/Img/Subdir/hero.gif", false)]
+ public void ParseWorksFolderAndFileWithDiferentCasing(string path, bool exists)
+ {
+ // Arrange
+ exists = exists | OperatingSystem.IsWindows();
+ var firstLevelCount = OperatingSystem.IsWindows() ? 1 : 2;
+ using var memoryStream = new MemoryStream();
+ using var writer = new StreamWriter(memoryStream);
+ // img/Subdir is a file without extension
+ writer.Write(@"{
+ ""ContentRoots"": [
+ ""D:\\path\\"",
+ ""D:\\other\\""
+ ],
+ ""Root"": {
+ ""Children"": {
+ ""img"": {
+ ""Children"": {
+ ""Subdir"": {
+ ""Asset"": {
+ ""ContentRootIndex"": 0,
+ ""SubPath"": ""Subdir""
+ }
+ }
+ }
+ },
+ ""Img"": {
+ ""Children"": {
+ ""subdir"": {
+ ""Children"": {
+ ""hero.gif"": {
+ ""Asset"": {
+ ""ContentRootIndex"": 1,
+ ""SubPath"": ""hero.gif""
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}");
+ var first = new Mock();
+ first.Setup(s => s.GetFileInfo("Subdir")).Returns(new TestFileInfo() { Name = "Subdir", Exists = true });
+ var second = new Mock();
+ second.Setup(s => s.GetFileInfo("hero.gif")).Returns(new TestFileInfo() { Name = "hero.gif", Exists = true });
+
+ writer.Flush();
+ memoryStream.Seek(0, SeekOrigin.Begin);
+ var manifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(memoryStream);
+ var comparer = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.PathComparer;
+
+ var provider = new ManifestStaticWebAssetFileProvider(
+ manifest,
+ contentRoot => contentRoot switch
+ {
+ "D:\\path\\" => first.Object,
+ "D:\\other\\" => second.Object,
+ _ => throw new InvalidOperationException("Unknown provider")
+ });
+
+ // Act
+ var file = provider.GetFileInfo(path);
+
+ // Assert
+ Assert.Equal(exists, file.Exists);
+ Assert.Equal(firstLevelCount, manifest.Root.Children.Count);
+ Assert.All(manifest.Root.Children.Values, c => Assert.Single(c.Children));
+ }
+
[Fact]
public void CanFindFileListedOnTheManifest()
{
diff --git a/src/Shared/StaticWebAssets/StaticWebAssetsFileProvider.cs b/src/Shared/StaticWebAssets/ManifestStaticWebAssetFileProvider.cs
similarity index 85%
rename from src/Shared/StaticWebAssets/StaticWebAssetsFileProvider.cs
rename to src/Shared/StaticWebAssets/ManifestStaticWebAssetFileProvider.cs
index 77b2f397982c..6221f2cc5968 100644
--- a/src/Shared/StaticWebAssets/StaticWebAssetsFileProvider.cs
+++ b/src/Shared/StaticWebAssets/ManifestStaticWebAssetFileProvider.cs
@@ -3,7 +3,6 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.FileProviders;
@@ -367,9 +366,58 @@ private sealed class OSBasedCaseConverter : JsonConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
- return new Dictionary(
- JsonSerializer.Deserialize>(ref reader, options)!,
- StaticWebAssetManifest.PathComparer);
+ var parsed = JsonSerializer.Deserialize>(ref reader, options)!;
+ var result = new Dictionary(StaticWebAssetManifest.PathComparer);
+ MergeChildren(parsed, result);
+ return result;
+
+ static void MergeChildren(
+ IDictionary newChildren,
+ IDictionary existing)
+ {
+ foreach (var (key, value) in newChildren)
+ {
+ if (!existing.TryGetValue(key, out var existingNode))
+ {
+ existing.Add(key, value);
+ }
+ else
+ {
+ if (value.Patterns != null)
+ {
+ if (existingNode.Patterns == null)
+ {
+ existingNode.Patterns = value.Patterns;
+ }
+ else
+ {
+ if (value.Patterns.Length > 0)
+ {
+ var newList = new StaticWebAssetPattern[existingNode.Patterns.Length + value.Patterns.Length];
+ existingNode.Patterns.CopyTo(newList, 0);
+ value.Patterns.CopyTo(newList, existingNode.Patterns.Length);
+ existingNode.Patterns = newList;
+ }
+ }
+ }
+
+ if (value.Children != null)
+ {
+ if (existingNode.Children == null)
+ {
+ existingNode.Children = value.Children;
+ }
+ else
+ {
+ if (value.Children.Count > 0)
+ {
+ MergeChildren(value.Children, existingNode.Children);
+ }
+ }
+ }
+ }
+ }
+ }
}
public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options)