From 60e3d8ed06118b0b7141531e15ea61cf7c045463 Mon Sep 17 00:00:00 2001 From: stratos Date: Mon, 5 May 2025 10:55:33 +0800 Subject: [PATCH 01/16] Using GZip to compress swagger-ui-dist files in embedded resource to reduce the output dll size --- .gitignore | 3 +- Directory.Packages.props | 3 +- .../GZipCompressedEmbeddedFileProvider.cs | 143 ++++++++++++++++++ .../SwaggerUIMiddleware.cs | 3 +- .../Swashbuckle.AspNetCore.SwaggerUI.csproj | 10 +- .../SwaggerUIIntegrationTests.cs | 78 ++++++++++ 6 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 src/Swashbuckle.AspNetCore.SwaggerUI/GZipCompressedEmbeddedFileProvider.cs diff --git a/.gitignore b/.gitignore index 0857e5a38e..9ef250aa55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -[Oo]bj/ +[Oo]bj/ [Bb]in/ +[Tt]est[Rr]esult*/ .vs/ .idea* BenchmarkDotNet.Artifacts*/ diff --git a/Directory.Packages.props b/Directory.Packages.props index ad33649e3b..3a2f7a1026 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,4 +1,4 @@ - + @@ -29,6 +29,7 @@ + diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/GZipCompressedEmbeddedFileProvider.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/GZipCompressedEmbeddedFileProvider.cs new file mode 100644 index 0000000000..e51f851fd4 --- /dev/null +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/GZipCompressedEmbeddedFileProvider.cs @@ -0,0 +1,143 @@ +using System.Buffers; +using System.Collections; +using System.Collections.Concurrent; +using System.IO.Compression; +using System.Reflection; + +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; + +namespace Swashbuckle.AspNetCore.SwaggerUI; + +/// +/// a wrapper to provider gzip decompressed resource file info +/// +/// +/// +internal sealed class GZipCompressedEmbeddedFileProvider(Assembly assembly, string baseNamespace) : IFileProvider +{ + private readonly string _baseNamespace = string.IsNullOrEmpty(baseNamespace) ? string.Empty : baseNamespace + "."; + + private readonly EmbeddedFileProvider _embeddedFileProvider = new(assembly, baseNamespace); + + private readonly ConcurrentDictionary _subpathLengthCache = new(StringComparer.Ordinal); + + /// + public IDirectoryContents GetDirectoryContents(string subpath) + { + //logic same as EmbeddedFileProvider + return subpath?.Length == 0 || string.Equals(subpath, "/", StringComparison.Ordinal) + ? new DecompresDirectoryContents(EnumerateItems().ToList()) + : new DecompresDirectoryContents([]); + } + + /// + public IFileInfo GetFileInfo(string subpath) => new GZipDecompresFileInfoWrapper(_embeddedFileProvider.GetFileInfo(subpath), subpath, GetSubpathDecompressedLength); + + /// + public IChangeToken Watch(string filter) => _embeddedFileProvider.Watch(filter); + + private static string NormalizeSubPath(string subpath) => string.Join(".", subpath.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries).Where(m => !string.IsNullOrWhiteSpace(m))); + + /// + /// Get the 's length by decompress and read it + /// + /// + /// + private static long ReadStreamDecompressedLength(Stream stream) + { + const int BufferSize = 4096; + + long length = 0; + var buffer = ArrayPool.Shared.Rent(BufferSize); + + try + { + using var gzipStream = new GZipStream(stream, CompressionMode.Decompress, true); + var readLength = 0; + do + { + readLength = gzipStream.Read(buffer, 0, BufferSize); + length += readLength; + } while (readLength != 0); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + return length; + } + + private IEnumerable EnumerateItems() + { + return assembly.GetManifestResourceNames() + .Where(name => name.StartsWith(_baseNamespace)) + .Select(name => name.Substring(_baseNamespace.Length)) + .Select(subpath => this.GetFileInfo(subpath)) + .Where(static fileInfo => fileInfo.Exists); + } + + /// + /// Get 's decompressed length and cache it + /// + /// + /// + private long GetSubpathDecompressedLength(string subpath) + { + if (_subpathLengthCache.TryGetValue(subpath, out var length)) + { + return length; + } + + var resourceName = $"{_baseNamespace}{NormalizeSubPath(subpath)}"; + + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + { + return -1; + } + + return _subpathLengthCache.GetOrAdd(subpath, _ => ReadStreamDecompressedLength(stream)); + } + + private sealed class DecompresDirectoryContents(List fileInfos) : IDirectoryContents + { + /// + public bool Exists => fileInfos.Count > 0; + + /// + public IEnumerator GetEnumerator() => fileInfos.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + private sealed class GZipDecompresFileInfoWrapper(IFileInfo fileInfo, string subpath, Func getSubpathLengthFunc) : IFileInfo + { + /// + public bool Exists => fileInfo.Exists; + + /// + public bool IsDirectory => fileInfo.IsDirectory; + + /// + public DateTimeOffset LastModified => fileInfo.LastModified; + + /// + public long Length => fileInfo.Exists ? getSubpathLengthFunc(subpath) : -1; + + /// + public string Name => fileInfo.Name; + + /// + public string PhysicalPath => fileInfo.PhysicalPath; + + /// + public Stream CreateReadStream() + { + return fileInfo.CreateReadStream() is { } stream + ? new GZipStream(stream, CompressionMode.Decompress) + : throw new FileNotFoundException(message: null, fileName: fileInfo.Name); + } + } +} diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs index 77f454671a..e94931461f 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.StaticFiles; -using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Reflection; @@ -110,7 +109,7 @@ private static StaticFileMiddleware CreateStaticFileMiddleware( var staticFileOptions = new StaticFileOptions { RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}", - FileProvider = new EmbeddedFileProvider(typeof(SwaggerUIMiddleware).Assembly, EmbeddedFileNamespace), + FileProvider = new GZipCompressedEmbeddedFileProvider(typeof(SwaggerUIMiddleware).Assembly, EmbeddedFileNamespace), OnPrepareResponse = (context) => SetCacheHeaders(context.Context.Response, options), }; diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj b/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj index 00075542f3..39dfb18c47 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj @@ -16,7 +16,11 @@ - + + + + + @@ -34,6 +38,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs index 410bf63822..cbc89d5014 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs @@ -1,4 +1,6 @@ using System.Net; +using Microsoft.Extensions.FileProviders; +using Swashbuckle.AspNetCore.SwaggerUI; namespace Swashbuckle.AspNetCore.IntegrationTests; @@ -23,6 +25,82 @@ public async Task RoutePrefix_RedirectsToPathRelativeIndexUrl( Assert.Equal(expectedRedirectPath, response.Headers.Location.ToString()); } + [Theory] + [InlineData("Swashbuckle.AspNetCore.SwaggerUI.node_modules")] + [InlineData("Swashbuckle.AspNetCore.SwaggerUI.node_modules.swagger_ui_dist")] + public void ResourceRead_CompressedEmbeddedFileProvider(string baseNamespace) + { + //confirm that GZipCompressedEmbeddedFileProvider is the same as EmbeddedFileProvider + var provider = new EmbeddedFileProvider(typeof(SwaggerUIOptions).Assembly, baseNamespace); + var compressedProvider = new GZipCompressedEmbeddedFileProvider(typeof(SwaggerUIOptions).Assembly, baseNamespace); + var checkSubpaths = new string[] + { + "/", + null, + string.Empty, + " ", + "\t", + "\n", + "/swagger_ui_dist", + "swagger_ui_dist", + "/nodir", + "nodir" + }; + + foreach (var subpath in checkSubpaths) + { + AssertResources(provider, compressedProvider, subpath); + } + + var nonExistentFile = Guid.NewGuid().ToString(); + AssertFileInfo(provider.GetFileInfo(nonExistentFile), compressedProvider.GetFileInfo(nonExistentFile)); + + static void AssertResources(IFileProvider expectedProvider, IFileProvider actualProvider, string subpath) + { + var expectedContents = expectedProvider.GetDirectoryContents(subpath); + var actualContents = actualProvider.GetDirectoryContents(subpath); + + Assert.Equal(expectedContents.Exists, actualContents.Exists); + Assert.Equal(expectedContents.Count(), actualContents.Count()); + var actualResourceMap = actualContents.ToDictionary(m => m.Name); + + foreach (var expectedFileInfo in expectedContents) + { + Assert.True(actualResourceMap.TryGetValue(expectedFileInfo.Name, out var actualFileInfo)); + Assert.NotNull(actualFileInfo); + Assert.True(actualFileInfo.Exists); + AssertFileInfo(expectedFileInfo, actualFileInfo); + AssertFileInfo(expectedProvider.GetFileInfo(expectedFileInfo.Name), actualProvider.GetFileInfo(expectedFileInfo.Name)); + } + } + + static void AssertFileInfo(IFileInfo expectedFileInfo, IFileInfo actualFileInfo) + { + Assert.Equal(expectedFileInfo.Exists, actualFileInfo.Exists); + Assert.Equal(expectedFileInfo.IsDirectory, actualFileInfo.IsDirectory); + Assert.Equal(expectedFileInfo.LastModified, actualFileInfo.LastModified); + Assert.Equal(expectedFileInfo.PhysicalPath, actualFileInfo.PhysicalPath); + + if (expectedFileInfo.Exists && !expectedFileInfo.IsDirectory) + { + Assert.True(actualFileInfo.Length > 0); + + using var stream = actualFileInfo.CreateReadStream(); + Assert.NotNull(stream); + var buffer = new byte[256]; + var readLength = stream.Read(buffer, 0, buffer.Length); + Assert.True(readLength > 0); + //we can check for correctness here + } + else + { + Assert.Equal(expectedFileInfo.Length, actualFileInfo.Length); + Assert.ThrowsAny(() => expectedFileInfo.CreateReadStream()); + Assert.ThrowsAny(() => actualFileInfo.CreateReadStream()); + } + } + } + [Theory] [InlineData(typeof(Basic.Startup), "/index.html", "/swagger-ui.js", "/index.css", "/swagger-ui.css")] [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.html", "/swagger/swagger-ui.js", "swagger/index.css", "/swagger/swagger-ui.css")] From c73a5a1a1a2bb01fc19bd3eff86fff7560753248 Mon Sep 17 00:00:00 2001 From: stratos Date: Mon, 5 May 2025 23:20:00 +0800 Subject: [PATCH 02/16] Using GZip to compress redoc/bundles files. Add UIIntegrationTests for swagger and redoc assets check by http request. Add project Swashbuckle.AspNetCore.SwaggerUI.Test for GZipCompressedEmbeddedFileProvider unit test. --- Swashbuckle.AspNetCore.sln | 7 ++ .../ReDocMiddleware.cs | 2 +- .../Swashbuckle.AspNetCore.ReDoc.csproj | 10 +- .../GZipCompressedEmbeddedFileProvider.cs | 4 +- .../SwaggerUIMiddleware.cs | 1 + .../Swashbuckle.AspNetCore.SwaggerUI.csproj | 4 - .../ReDocIntegrationTests.cs | 19 ++++ .../SwaggerUIIntegrationTests.cs | 102 ++++-------------- ...hbuckle.AspNetCore.IntegrationTests.csproj | 9 ++ ...GZipCompressedEmbeddedFileProviderTests.cs | 92 ++++++++++++++++ ...ashbuckle.AspNetCore.SwaggerUI.Test.csproj | 28 +++++ 11 files changed, 190 insertions(+), 88 deletions(-) create mode 100644 test/Swashbuckle.AspNetCore.SwaggerUI.Test/GZipCompressedEmbeddedFileProviderTests.cs create mode 100644 test/Swashbuckle.AspNetCore.SwaggerUI.Test/Swashbuckle.AspNetCore.SwaggerUI.Test.csproj diff --git a/Swashbuckle.AspNetCore.sln b/Swashbuckle.AspNetCore.sln index 459bbf319f..f051e4b0fd 100644 --- a/Swashbuckle.AspNetCore.sln +++ b/Swashbuckle.AspNetCore.sln @@ -126,6 +126,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{0C7326F1-F EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swashbuckle.AspNetCore.Benchmarks", "perf\Swashbuckle.AspNetCore.Benchmarks\Swashbuckle.AspNetCore.Benchmarks.csproj", "{28F5840B-AE87-46DB-9D83-C3C8C77C6A13}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Swashbuckle.AspNetCore.SwaggerUI.Test", "test\Swashbuckle.AspNetCore.SwaggerUI.Test\Swashbuckle.AspNetCore.SwaggerUI.Test.csproj", "{7FB06619-0D8E-E106-C16B-8F72B7AAC16A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -284,6 +286,10 @@ Global {28F5840B-AE87-46DB-9D83-C3C8C77C6A13}.Debug|Any CPU.Build.0 = Debug|Any CPU {28F5840B-AE87-46DB-9D83-C3C8C77C6A13}.Release|Any CPU.ActiveCfg = Release|Any CPU {28F5840B-AE87-46DB-9D83-C3C8C77C6A13}.Release|Any CPU.Build.0 = Release|Any CPU + {7FB06619-0D8E-E106-C16B-8F72B7AAC16A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FB06619-0D8E-E106-C16B-8F72B7AAC16A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FB06619-0D8E-E106-C16B-8F72B7AAC16A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FB06619-0D8E-E106-C16B-8F72B7AAC16A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -330,6 +336,7 @@ Global {D06A88E8-6F42-4F40-943A-E266C0AE6EC9} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} {F88B6070-BE3C-45F9-978C-2ECBA9518C24} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} {28F5840B-AE87-46DB-9D83-C3C8C77C6A13} = {0C7326F1-F731-4CF9-8A98-80F39541D28F} + {7FB06619-0D8E-E106-C16B-8F72B7AAC16A} = {0ADCB223-F375-45AB-8BC4-834EC9C69554} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {36FC6A67-247D-4149-8EDD-79FFD1A75F51} diff --git a/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs b/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs index 012a8ad0a3..4f6ecaba90 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.ReDoc/ReDocMiddleware.cs @@ -103,7 +103,7 @@ private static StaticFileMiddleware CreateStaticFileMiddleware( var staticFileOptions = new StaticFileOptions { RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}", - FileProvider = new EmbeddedFileProvider(typeof(ReDocMiddleware).Assembly, EmbeddedFileNamespace), + FileProvider = new GZipCompressedEmbeddedFileProvider(typeof(ReDocMiddleware).Assembly, EmbeddedFileNamespace), OnPrepareResponse = (context) => SetCacheHeaders(context.Context.Response, options), }; diff --git a/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj b/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj index 953668a98b..6a28d7409c 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj +++ b/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj @@ -24,7 +24,7 @@ - + @@ -40,6 +40,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -50,6 +54,10 @@ + + + + diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/GZipCompressedEmbeddedFileProvider.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/GZipCompressedEmbeddedFileProvider.cs index e51f851fd4..343cec44c4 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/GZipCompressedEmbeddedFileProvider.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/GZipCompressedEmbeddedFileProvider.cs @@ -3,11 +3,9 @@ using System.Collections.Concurrent; using System.IO.Compression; using System.Reflection; - -using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; -namespace Swashbuckle.AspNetCore.SwaggerUI; +namespace Microsoft.Extensions.FileProviders; /// /// a wrapper to provider gzip decompressed resource file info diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs index e94931461f..c925146613 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Options; using System.Reflection; using System.Security.Cryptography; +using Microsoft.Extensions.FileProviders; #if NET using System.Diagnostics.CodeAnalysis; diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj b/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj index 39dfb18c47..e78a8c7c62 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj @@ -15,10 +15,6 @@ true - - - - diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs index f14cafb8cb..6daa4ea801 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Security.Cryptography; using Microsoft.AspNetCore.Builder; using Swashbuckle.AspNetCore.ReDoc; using ReDocApp = ReDoc; @@ -162,4 +163,22 @@ public void ReDocOptions_Extensions() Assert.True(options.ConfigObject.SortPropsAlphabetically); Assert.True(options.ConfigObject.UntrustedSpec); } + + [Fact] + public async Task ReDocMiddleware_Returns_ExpectedAssetContents() + { + var site = new TestSite(typeof(ReDocApp.Startup)); + using var client = site.BuildClient(); + + var diskFile ="redoc/bundles/redoc.standalone.js"; + var diskFileName = Path.GetFileName("redoc/bundles/redoc.standalone.js"); + + using var htmlResponse = await client.GetAsync("/Api-Docs/redoc.standalone.js", TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.OK, htmlResponse.StatusCode); + + using var stream = await htmlResponse.Content.ReadAsStreamAsync(TestContext.Current.CancellationToken); + using var diskFileStream = File.OpenRead(diskFile); + + Assert.Equal(MD5.HashData(diskFileStream), MD5.HashData(stream)); + } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs index cbc89d5014..694c3b9a92 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs @@ -1,6 +1,6 @@ -using System.Net; -using Microsoft.Extensions.FileProviders; -using Swashbuckle.AspNetCore.SwaggerUI; +using System.IO; +using System.Net; +using System.Security.Cryptography; namespace Swashbuckle.AspNetCore.IntegrationTests; @@ -25,82 +25,6 @@ public async Task RoutePrefix_RedirectsToPathRelativeIndexUrl( Assert.Equal(expectedRedirectPath, response.Headers.Location.ToString()); } - [Theory] - [InlineData("Swashbuckle.AspNetCore.SwaggerUI.node_modules")] - [InlineData("Swashbuckle.AspNetCore.SwaggerUI.node_modules.swagger_ui_dist")] - public void ResourceRead_CompressedEmbeddedFileProvider(string baseNamespace) - { - //confirm that GZipCompressedEmbeddedFileProvider is the same as EmbeddedFileProvider - var provider = new EmbeddedFileProvider(typeof(SwaggerUIOptions).Assembly, baseNamespace); - var compressedProvider = new GZipCompressedEmbeddedFileProvider(typeof(SwaggerUIOptions).Assembly, baseNamespace); - var checkSubpaths = new string[] - { - "/", - null, - string.Empty, - " ", - "\t", - "\n", - "/swagger_ui_dist", - "swagger_ui_dist", - "/nodir", - "nodir" - }; - - foreach (var subpath in checkSubpaths) - { - AssertResources(provider, compressedProvider, subpath); - } - - var nonExistentFile = Guid.NewGuid().ToString(); - AssertFileInfo(provider.GetFileInfo(nonExistentFile), compressedProvider.GetFileInfo(nonExistentFile)); - - static void AssertResources(IFileProvider expectedProvider, IFileProvider actualProvider, string subpath) - { - var expectedContents = expectedProvider.GetDirectoryContents(subpath); - var actualContents = actualProvider.GetDirectoryContents(subpath); - - Assert.Equal(expectedContents.Exists, actualContents.Exists); - Assert.Equal(expectedContents.Count(), actualContents.Count()); - var actualResourceMap = actualContents.ToDictionary(m => m.Name); - - foreach (var expectedFileInfo in expectedContents) - { - Assert.True(actualResourceMap.TryGetValue(expectedFileInfo.Name, out var actualFileInfo)); - Assert.NotNull(actualFileInfo); - Assert.True(actualFileInfo.Exists); - AssertFileInfo(expectedFileInfo, actualFileInfo); - AssertFileInfo(expectedProvider.GetFileInfo(expectedFileInfo.Name), actualProvider.GetFileInfo(expectedFileInfo.Name)); - } - } - - static void AssertFileInfo(IFileInfo expectedFileInfo, IFileInfo actualFileInfo) - { - Assert.Equal(expectedFileInfo.Exists, actualFileInfo.Exists); - Assert.Equal(expectedFileInfo.IsDirectory, actualFileInfo.IsDirectory); - Assert.Equal(expectedFileInfo.LastModified, actualFileInfo.LastModified); - Assert.Equal(expectedFileInfo.PhysicalPath, actualFileInfo.PhysicalPath); - - if (expectedFileInfo.Exists && !expectedFileInfo.IsDirectory) - { - Assert.True(actualFileInfo.Length > 0); - - using var stream = actualFileInfo.CreateReadStream(); - Assert.NotNull(stream); - var buffer = new byte[256]; - var readLength = stream.Read(buffer, 0, buffer.Length); - Assert.True(readLength > 0); - //we can check for correctness here - } - else - { - Assert.Equal(expectedFileInfo.Length, actualFileInfo.Length); - Assert.ThrowsAny(() => expectedFileInfo.CreateReadStream()); - Assert.ThrowsAny(() => actualFileInfo.CreateReadStream()); - } - } - } - [Theory] [InlineData(typeof(Basic.Startup), "/index.html", "/swagger-ui.js", "/index.css", "/swagger-ui.css")] [InlineData(typeof(CustomUIConfig.Startup), "/swagger/index.html", "/swagger/swagger-ui.js", "swagger/index.css", "/swagger/swagger-ui.css")] @@ -268,4 +192,24 @@ public async Task IndexUrl_Returns_ExpectedAssetPaths( Assert.Contains($"