Просмотр исходного кода

Add Request Decompression middleware (#40279)

Co-authored-by: Pranav K <[email protected]>
Co-authored-by: Sébastien Ros <[email protected]>
David Acker 3 лет назад
Родитель
Сommit
d5a539f19b
37 измененных файлов с 2217 добавлено и 5 удалено
  1. 79 0
      AspNetCore.sln
  2. 1 0
      eng/ProjectReferences.props
  3. 1 0
      eng/SharedFramework.Local.props
  4. 2 0
      src/Framework/test/TestData.cs
  5. 15 0
      src/Http/Http.Abstractions/src/Metadata/IRequestSizeLimitMetadata.cs
  6. 2 0
      src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
  7. 5 1
      src/Middleware/Middleware.slnf
  8. 4 0
      src/Middleware/RequestDecompression/perf/Microbenchmarks/AssemblyInfo.cs
  9. 15 0
      src/Middleware/RequestDecompression/perf/Microbenchmarks/Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj
  10. 92 0
      src/Middleware/RequestDecompression/perf/Microbenchmarks/RequestDecompressionMiddlewareBenchmark.cs
  11. 15 0
      src/Middleware/RequestDecompression/sample/CustomDecompressionProvider.cs
  12. 27 0
      src/Middleware/RequestDecompression/sample/Properties/launchsettings.json
  13. 13 0
      src/Middleware/RequestDecompression/sample/RequestDecompressionSample.csproj
  14. 48 0
      src/Middleware/RequestDecompression/sample/Startup.cs
  15. 18 0
      src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs
  16. 90 0
      src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs
  17. 18 0
      src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs
  18. 18 0
      src/Middleware/RequestDecompression/src/GZipDecompressionProvider.cs
  19. 17 0
      src/Middleware/RequestDecompression/src/IDecompressionProvider.cs
  20. 19 0
      src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs
  21. 24 0
      src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj
  22. 1 0
      src/Middleware/RequestDecompression/src/PublicAPI.Shipped.txt
  23. 13 0
      src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt
  24. 26 0
      src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs
  25. 141 0
      src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs
  26. 20 0
      src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs
  27. 52 0
      src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs
  28. 94 0
      src/Middleware/RequestDecompression/src/SizeLimitedStream.cs
  29. 176 0
      src/Middleware/RequestDecompression/test/DefaultRequestDecompressionProviderTests.cs
  30. 14 0
      src/Middleware/RequestDecompression/test/Microsoft.AspNetCore.RequestDecompression.Tests.csproj
  31. 22 0
      src/Middleware/RequestDecompression/test/RequestDecompressionBuilderExtensionsTests.cs
  32. 945 0
      src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs
  33. 30 0
      src/Middleware/RequestDecompression/test/RequestDecompressionOptionsTests.cs
  34. 37 0
      src/Middleware/RequestDecompression/test/RequestDecompressionServiceExtensionsTests.cs
  35. 107 0
      src/Middleware/RequestDecompression/test/SizeLimitedStreamTests.cs
  36. 10 2
      src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs
  37. 6 2
      src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs

+ 79 - 0
AspNetCore.sln

@@ -1654,6 +1654,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.SdkAnalyzers.Tests", "src\Tools\SDK-Analyzers\Components\test\Microsoft.AspNetCore.Components.SdkAnalyzers.Tests.csproj", "{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RequestDecompression", "RequestDecompression", "{5465F96F-33D5-454E-9C40-494E58AEEE5D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestDecompression.Tests", "src\Middleware\RequestDecompression\test\Microsoft.AspNetCore.RequestDecompression.Tests.csproj", "{97996D39-7722-4AFC-A41A-AD61CA7A413D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RequestDecompressionSample", "src\Middleware\RequestDecompression\sample\RequestDecompressionSample.csproj", "{37144E52-611B-40E8-807C-2821F5A814CB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestDecompression", "src\Middleware\RequestDecompression\src\Microsoft.AspNetCore.RequestDecompression.csproj", "{559FE354-7E08-4310-B4F3-AE30F34DEED5}"
+EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LinkabilityChecker", "LinkabilityChecker", "{94F95276-7CDF-44A8-B159-D09702EF6794}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinkabilityChecker", "src\Tools\LinkabilityChecker\LinkabilityChecker.csproj", "{EA7D844B-C180-41C7-9D55-273AD88BF71F}"
@@ -1710,6 +1718,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Html.A
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RateLimiting", "RateLimiting", "{1D865E78-7A66-4CA9-92EE-2B350E45281F}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestDecompression.Microbenchmarks", "src\Middleware\RequestDecompression\perf\Microbenchmarks\Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj", "{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}"
+EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts", "src\Tools\dotnet-user-jwts\src\dotnet-user-jwts.csproj", "{B34CB502-0286-4939-B25F-45998528A802}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet-user-jwts", "dotnet-user-jwts", "{AB4B9E75-719C-4589-B852-20FBFD727730}"
@@ -9967,6 +9977,54 @@ Global
 		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x64.Build.0 = Release|Any CPU
 		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x86.ActiveCfg = Release|Any CPU
 		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x86.Build.0 = Release|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|arm64.ActiveCfg = Debug|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|arm64.Build.0 = Debug|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|x64.Build.0 = Debug|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Debug|x86.Build.0 = Debug|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|Any CPU.Build.0 = Release|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|arm64.ActiveCfg = Release|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|arm64.Build.0 = Release|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|x64.ActiveCfg = Release|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|x64.Build.0 = Release|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|x86.ActiveCfg = Release|Any CPU
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D}.Release|x86.Build.0 = Release|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|arm64.ActiveCfg = Debug|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|arm64.Build.0 = Debug|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|x64.Build.0 = Debug|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Debug|x86.Build.0 = Debug|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Release|Any CPU.Build.0 = Release|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Release|arm64.ActiveCfg = Release|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Release|arm64.Build.0 = Release|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Release|x64.ActiveCfg = Release|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Release|x64.Build.0 = Release|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Release|x86.ActiveCfg = Release|Any CPU
+		{37144E52-611B-40E8-807C-2821F5A814CB}.Release|x86.Build.0 = Release|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|arm64.ActiveCfg = Debug|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|arm64.Build.0 = Debug|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|x64.Build.0 = Debug|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Debug|x86.Build.0 = Debug|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|Any CPU.Build.0 = Release|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|arm64.ActiveCfg = Release|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|arm64.Build.0 = Release|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|x64.ActiveCfg = Release|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|x64.Build.0 = Release|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|x86.ActiveCfg = Release|Any CPU
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5}.Release|x86.Build.0 = Release|Any CPU
 		{EA7D844B-C180-41C7-9D55-273AD88BF71F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{EA7D844B-C180-41C7-9D55-273AD88BF71F}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{EA7D844B-C180-41C7-9D55-273AD88BF71F}.Debug|arm64.ActiveCfg = Debug|Any CPU
@@ -10255,6 +10313,22 @@ Global
 		{487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x64.Build.0 = Release|Any CPU
 		{487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.ActiveCfg = Release|Any CPU
 		{487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.Build.0 = Release|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|arm64.ActiveCfg = Debug|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|arm64.Build.0 = Debug|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|x64.Build.0 = Debug|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Debug|x86.Build.0 = Debug|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|arm64.ActiveCfg = Release|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|arm64.Build.0 = Release|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|x64.ActiveCfg = Release|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|x64.Build.0 = Release|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|x86.ActiveCfg = Release|Any CPU
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0}.Release|x86.Build.0 = Release|Any CPU
 		{B34CB502-0286-4939-B25F-45998528A802}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{B34CB502-0286-4939-B25F-45998528A802}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{B34CB502-0286-4939-B25F-45998528A802}.Debug|arm64.ActiveCfg = Debug|Any CPU
@@ -11122,6 +11196,10 @@ Global
 		{CC45FA2D-128B-485D-BA6D-DFD9735CB3C3} = {6C06163A-80E9-49C1-817C-B391852BA563}
 		{825BCF97-67A9-4834-B3A8-C3DC97A90E41} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3}
 		{DC349A25-0DBF-4468-99E1-B95C22D3A7EF} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3}
+		{5465F96F-33D5-454E-9C40-494E58AEEE5D} = {E5963C9F-20A6-4385-B364-814D2581FADF}
+		{97996D39-7722-4AFC-A41A-AD61CA7A413D} = {5465F96F-33D5-454E-9C40-494E58AEEE5D}
+		{37144E52-611B-40E8-807C-2821F5A814CB} = {5465F96F-33D5-454E-9C40-494E58AEEE5D}
+		{559FE354-7E08-4310-B4F3-AE30F34DEED5} = {5465F96F-33D5-454E-9C40-494E58AEEE5D}
 		{94F95276-7CDF-44A8-B159-D09702EF6794} = {0B200A66-B809-4ED3-A790-CB1C2E80975E}
 		{EA7D844B-C180-41C7-9D55-273AD88BF71F} = {94F95276-7CDF-44A8-B159-D09702EF6794}
 		{7A331A1C-E2C4-4E37-B0A0-B5AA10661229} = {8DAC59BE-CB96-4F04-909C-56C22E7665EB}
@@ -11150,6 +11228,7 @@ Global
 		{51D07AA9-6297-4F66-A7BD-71CE7E3F4A3F} = {0F84F170-57D0-496B-8E2C-7984178EF69F}
 		{487EF7BE-5009-4C70-B79E-45519BDD9603} = {412D4C15-F48F-4DB1-940A-131D1AA87088}
 		{1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF}
+		{3309FA1E-4E95-49E9-BE2A-827D01FD63C0} = {5465F96F-33D5-454E-9C40-494E58AEEE5D}
 		{B34CB502-0286-4939-B25F-45998528A802} = {AB4B9E75-719C-4589-B852-20FBFD727730}
 		{AB4B9E75-719C-4589-B852-20FBFD727730} = {0B200A66-B809-4ED3-A790-CB1C2E80975E}
 		{7F079E92-32D5-4257-B95B-CFFB0D49C160} = {7FD32066-C831-4E29-978C-9A2215E85C67}

+ 1 - 0
eng/ProjectReferences.props

@@ -90,6 +90,7 @@
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Localization" ProjectPath="$(RepoRoot)src\Middleware\Localization\src\Microsoft.AspNetCore.Localization.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.MiddlewareAnalysis" ProjectPath="$(RepoRoot)src\Middleware\MiddlewareAnalysis\src\Microsoft.AspNetCore.MiddlewareAnalysis.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.RateLimiting" ProjectPath="$(RepoRoot)src\Middleware\RateLimiting\src\Microsoft.AspNetCore.RateLimiting.csproj" />
+    <ProjectReferenceProvider Include="Microsoft.AspNetCore.RequestDecompression" ProjectPath="$(RepoRoot)src\Middleware\RequestDecompression\src\Microsoft.AspNetCore.RequestDecompression.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCaching.Abstractions" ProjectPath="$(RepoRoot)src\Middleware\ResponseCaching.Abstractions\src\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCaching" ProjectPath="$(RepoRoot)src\Middleware\ResponseCaching\src\Microsoft.AspNetCore.ResponseCaching.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCompression" ProjectPath="$(RepoRoot)src\Middleware\ResponseCompression\src\Microsoft.AspNetCore.ResponseCompression.csproj" />

+ 1 - 0
eng/SharedFramework.Local.props

@@ -77,6 +77,7 @@
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.HttpsPolicy" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Localization.Routing" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Localization" />
+    <AspNetCoreAppReference Include="Microsoft.AspNetCore.RequestDecompression" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.ResponseCaching.Abstractions" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.ResponseCaching" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.ResponseCompression" />

+ 2 - 0
src/Framework/test/TestData.cs

@@ -75,6 +75,7 @@ public static class TestData
                 "Microsoft.AspNetCore.Mvc.ViewFeatures",
                 "Microsoft.AspNetCore.Razor",
                 "Microsoft.AspNetCore.Razor.Runtime",
+                "Microsoft.AspNetCore.RequestDecompression",
                 "Microsoft.AspNetCore.ResponseCaching",
                 "Microsoft.AspNetCore.ResponseCaching.Abstractions",
                 "Microsoft.AspNetCore.ResponseCompression",
@@ -210,6 +211,7 @@ public static class TestData
                 { "Microsoft.AspNetCore.Mvc.ViewFeatures", "7.0.0.0" },
                 { "Microsoft.AspNetCore.Razor", "7.0.0.0" },
                 { "Microsoft.AspNetCore.Razor.Runtime", "7.0.0.0" },
+                { "Microsoft.AspNetCore.RequestDecompression", "7.0.0.0" },
                 { "Microsoft.AspNetCore.ResponseCaching", "7.0.0.0" },
                 { "Microsoft.AspNetCore.ResponseCaching.Abstractions", "7.0.0.0" },
                 { "Microsoft.AspNetCore.ResponseCompression", "7.0.0.0" },

+ 15 - 0
src/Http/Http.Abstractions/src/Metadata/IRequestSizeLimitMetadata.cs

@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Http.Metadata;
+
+/// <summary>
+/// Interface marking attributes that specify the maximum allowed size of the request body.
+/// </summary>
+public interface IRequestSizeLimitMetadata
+{
+    /// <summary>
+    /// The maximum allowed size of the current request body in bytes.
+    /// </summary>
+    long? MaxRequestBodySize { get; }
+}

+ 2 - 0
src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

@@ -12,6 +12,8 @@ Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata<T>() ->
 Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask<object?>
 Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata
 Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string?
+Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata
+Microsoft.AspNetCore.Http.Metadata.IRequestSizeLimitMetadata.MaxRequestBodySize.get -> long?
 Microsoft.AspNetCore.Http.RouteHandlerContext
 Microsoft.AspNetCore.Http.RouteHandlerContext.EndpointMetadata.get -> Microsoft.AspNetCore.Http.EndpointMetadataCollection!
 Microsoft.AspNetCore.Http.RouteHandlerContext.MethodInfo.get -> System.Reflection.MethodInfo!

+ 5 - 1
src/Middleware/Middleware.slnf

@@ -78,6 +78,10 @@
       "src\\Middleware\\MiddlewareAnalysis\\test\\Microsoft.AspNetCore.MiddlewareAnalysis.Tests.csproj",
       "src\\Middleware\\RateLimiting\\src\\Microsoft.AspNetCore.RateLimiting.csproj",
       "src\\Middleware\\RateLimiting\\test\\Microsoft.AspNetCore.RateLimiting.Tests.csproj",
+      "src\\Middleware\\RequestDecompression\\perf\\Microbenchmarks\\Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj",
+      "src\\Middleware\\RequestDecompression\\sample\\RequestDecompressionSample.csproj",
+      "src\\Middleware\\RequestDecompression\\src\\Microsoft.AspNetCore.RequestDecompression.csproj",
+      "src\\Middleware\\RequestDecompression\\test\\Microsoft.AspNetCore.RequestDecompression.Tests.csproj",
       "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj",
       "src\\Middleware\\ResponseCaching\\samples\\ResponseCachingSample\\ResponseCachingSample.csproj",
       "src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj",
@@ -117,4 +121,4 @@
       "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj"
     ]
   }
-}
+}

+ 4 - 0
src/Middleware/RequestDecompression/perf/Microbenchmarks/AssemblyInfo.cs

@@ -0,0 +1,4 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]

+ 15 - 0
src/Middleware/RequestDecompression/perf/Microbenchmarks/Microsoft.AspNetCore.RequestDecompression.Microbenchmarks.csproj

@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="BenchmarkDotNet" />
+    <Reference Include="Microsoft.AspNetCore.RequestDecompression" />
+
+    <Compile Include="$(SharedSourceRoot)BenchmarkRunner\*.cs" />
+  </ItemGroup>
+
+</Project>

+ 92 - 0
src/Middleware/RequestDecompression/perf/Microbenchmarks/RequestDecompressionMiddlewareBenchmark.cs

@@ -0,0 +1,92 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using BenchmarkDotNet.Attributes;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Http.Metadata;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.RequestDecompression.Benchmarks;
+
+public class RequestDecompressionMiddlewareBenchmark
+{
+    private RequestDecompressionMiddleware _middleware;
+
+    [GlobalSetup]
+    public void GlobalSetup()
+    {
+        var requestDecompressionProvider = new DefaultRequestDecompressionProvider(
+            NullLogger<DefaultRequestDecompressionProvider>.Instance,
+            Options.Create(new RequestDecompressionOptions())
+        );
+
+        _middleware = new RequestDecompressionMiddleware(
+            context => Task.CompletedTask,
+            NullLogger<RequestDecompressionMiddleware>.Instance,
+            requestDecompressionProvider
+        );
+    }
+
+    [Params(true, false)]
+    public bool HasRequestSizeLimitMetadata { get; set; }
+
+    [Benchmark]
+    public async Task HandleRequest_Compressed()
+    {
+        var context = CreateHttpContext(HasRequestSizeLimitMetadata);
+
+        context.Request.Headers.ContentEncoding = "gzip";
+
+        await _middleware.Invoke(context);
+    }
+
+    [Benchmark]
+    public async Task HandleRequest_Uncompressed()
+    {
+        var context = CreateHttpContext(HasRequestSizeLimitMetadata);
+
+        await _middleware.Invoke(context);
+    }
+
+    private static DefaultHttpContext CreateHttpContext(bool hasRequestSizeLimitMetadata)
+    {
+        var features = new FeatureCollection();
+        features.Set<IHttpRequestFeature>(new HttpRequestFeature());
+        features.Set<IHttpResponseFeature>(new HttpResponseFeature());
+        features.Set<IHttpMaxRequestBodySizeFeature>(new MaxRequestBodySizeFeature());
+        features.Set<IEndpointFeature>(new EndpointFeature(hasRequestSizeLimitMetadata));
+        var context = new DefaultHttpContext(features);
+        return context;
+    }
+
+    private sealed class MaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature
+    {
+        public bool IsReadOnly => false;
+
+        public long? MaxRequestBodySize { get; set; } = 30_000_000;
+    }
+
+    private sealed class EndpointFeature : IEndpointFeature
+    {
+        public Endpoint Endpoint { get; set; }
+
+        public EndpointFeature(bool hasRequestSizeLimitMetadata)
+        {
+            var metadataCollection = hasRequestSizeLimitMetadata
+                ? new EndpointMetadataCollection(new SizeLimitMetadata())
+                : new EndpointMetadataCollection();
+
+            Endpoint = new Endpoint(
+                requestDelegate: null,
+                metadata: metadataCollection,
+                displayName: null);
+        }
+    }
+
+    private sealed class SizeLimitMetadata : IRequestSizeLimitMetadata
+    {
+        public long? MaxRequestBodySize { get; set; } = 50_000_000;
+    }
+}

+ 15 - 0
src/Middleware/RequestDecompression/sample/CustomDecompressionProvider.cs

@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.RequestDecompression;
+
+namespace RequestDecompressionSample;
+
+public class CustomDecompressionProvider : IDecompressionProvider
+{
+    public Stream GetDecompressionStream(Stream stream)
+    {
+        // Create a custom decompression stream wrapper here.
+        return stream;
+    }
+}

+ 27 - 0
src/Middleware/RequestDecompression/sample/Properties/launchsettings.json

@@ -0,0 +1,27 @@
+{
+    "iisSettings": {
+      "windowsAuthentication": false,
+      "anonymousAuthentication": true,
+      "iisExpress": {
+        "applicationUrl": "http://localhost:6164/",
+        "sslPort": 0
+      }
+    },
+    "profiles": {
+      "RequestDecompressionSample": {
+        "commandName": "Project",
+        "launchBrowser": true,
+        "launchUrl": "http://localhost:5000/",
+        "environmentVariables": {
+          "ASPNETCORE_ENVIRONMENT": "Development"
+        }
+      },
+      "IIS Express": {
+        "commandName": "IISExpress",
+        "launchBrowser": true,
+        "environmentVariables": {
+          "ASPNETCORE_ENVIRONMENT": "Development"
+        }
+      }
+    }
+  }

+ 13 - 0
src/Middleware/RequestDecompression/sample/RequestDecompressionSample.csproj

@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.RequestDecompression" />
+    <Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
+    <Reference Include="Microsoft.Extensions.Logging.Console" />
+  </ItemGroup>
+
+</Project>

+ 48 - 0
src/Middleware/RequestDecompression/sample/Startup.cs

@@ -0,0 +1,48 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace RequestDecompressionSample;
+
+public class Startup
+{
+    public void ConfigureServices(IServiceCollection services)
+    {
+        services.AddRequestDecompression(options =>
+        {
+            options.DecompressionProviders.Add("custom", new CustomDecompressionProvider());
+        });
+    }
+
+    public void Configure(IApplicationBuilder app)
+    {
+        app.UseRequestDecompression();
+        app.Map("/test", testApp =>
+        {
+            testApp.Run(async context =>
+            {
+                using var reader = new StreamReader(context.Request.Body);
+                var decompressedBody = await reader.ReadToEndAsync(context.RequestAborted);
+
+                await context.Response.WriteAsync(decompressedBody, context.RequestAborted);
+            });
+        });
+    }
+
+    public static Task Main(string[] args)
+    {
+        var host = new HostBuilder()
+            .ConfigureWebHost(webHostBuilder =>
+            {
+                webHostBuilder
+                .UseKestrel()
+                .ConfigureLogging(factory =>
+                {
+                    factory.AddConsole()
+                    .SetMinimumLevel(LogLevel.Debug);
+                })
+                .UseStartup<Startup>();
+            }).Build();
+
+        return host.RunAsync();
+    }
+}

+ 18 - 0
src/Middleware/RequestDecompression/src/BrotliDecompressionProvider.cs

@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO.Compression;
+
+namespace Microsoft.AspNetCore.RequestDecompression;
+
+/// <summary>
+/// Brotli decompression provider.
+/// </summary>
+internal sealed class BrotliDecompressionProvider : IDecompressionProvider
+{
+    /// <inheritdoc />
+    public Stream GetDecompressionStream(Stream stream)
+    {
+        return new BrotliStream(stream, CompressionMode.Decompress, leaveOpen: true);
+    }
+}

+ 90 - 0
src/Middleware/RequestDecompression/src/DefaultRequestDecompressionProvider.cs

@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.RequestDecompression;
+
+/// <inheritdoc />
+internal sealed partial class DefaultRequestDecompressionProvider : IRequestDecompressionProvider
+{
+    private readonly ILogger _logger;
+    private readonly IDictionary<string, IDecompressionProvider> _providers;
+
+    public DefaultRequestDecompressionProvider(
+        ILogger<DefaultRequestDecompressionProvider> logger,
+        IOptions<RequestDecompressionOptions> options)
+    {
+        if (logger is null)
+        {
+            throw new ArgumentNullException(nameof(logger));
+        }
+
+        if (options is null)
+        {
+            throw new ArgumentNullException(nameof(options));
+        }
+
+        _logger = logger;
+        _providers = options.Value.DecompressionProviders;
+    }
+
+    /// <inheritdoc />
+    public Stream? GetDecompressionStream(HttpContext context)
+    {
+        var encodings = context.Request.Headers.ContentEncoding;
+
+        if (StringValues.IsNullOrEmpty(encodings))
+        {
+            Log.NoContentEncoding(_logger);
+            return null;
+        }
+
+        if (encodings.Count > 1)
+        {
+            Log.MultipleContentEncodingsSpecified(_logger);
+            return null;
+        }
+
+        string encodingName = encodings!;
+
+        if (_providers.TryGetValue(encodingName, out var matchingProvider))
+        {
+            Log.DecompressingWith(_logger, encodingName);
+
+            context.Request.Headers.Remove(HeaderNames.ContentEncoding);
+
+            return matchingProvider.GetDecompressionStream(context.Request.Body);
+        }
+
+        Log.NoDecompressionProvider(_logger);
+        return null;
+    }
+
+    private static partial class Log
+    {
+        [LoggerMessage(1, LogLevel.Trace, "The Content-Encoding header is empty or not specified. Skipping request decompression.", EventName = "NoContentEncoding")]
+        public static partial void NoContentEncoding(ILogger logger);
+
+        [LoggerMessage(2, LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings.", EventName = "MultipleContentEncodingsSpecified")]
+        public static partial void MultipleContentEncodingsSpecified(ILogger logger);
+
+        [LoggerMessage(3, LogLevel.Debug, "No matching request decompression provider found.", EventName = "NoDecompressionProvider")]
+        public static partial void NoDecompressionProvider(ILogger logger);
+
+        public static void DecompressingWith(ILogger logger, string contentEncoding)
+        {
+            if (logger.IsEnabled(LogLevel.Debug))
+            {
+                DecompressingWithCore(logger, contentEncoding.ToLowerInvariant());
+            }
+        }
+        
+        [LoggerMessage(4, LogLevel.Debug, "The request will be decompressed with '{ContentEncoding}'.", EventName = "DecompressingWith", SkipEnabledCheck = true)]
+        private static partial void DecompressingWithCore(ILogger logger, string contentEncoding);
+    }
+}

+ 18 - 0
src/Middleware/RequestDecompression/src/DeflateDecompressionProvider.cs

@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO.Compression;
+
+namespace Microsoft.AspNetCore.RequestDecompression;
+
+/// <summary>
+/// DEFLATE decompression provider.
+/// </summary>
+internal sealed class DeflateDecompressionProvider : IDecompressionProvider
+{
+    /// <inheritdoc />
+    public Stream GetDecompressionStream(Stream stream)
+    {
+        return new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true);
+    }
+}

+ 18 - 0
src/Middleware/RequestDecompression/src/GZipDecompressionProvider.cs

@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO.Compression;
+
+namespace Microsoft.AspNetCore.RequestDecompression;
+
+/// <summary>
+/// GZip decompression provider.
+/// </summary>
+internal sealed class GZipDecompressionProvider : IDecompressionProvider
+{
+    /// <inheritdoc />
+    public Stream GetDecompressionStream(Stream stream)
+    {
+        return new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true);
+    }
+}

+ 17 - 0
src/Middleware/RequestDecompression/src/IDecompressionProvider.cs

@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.RequestDecompression;
+
+/// <summary>
+/// Provides a specific decompression implementation to decompress HTTP request bodies.
+/// </summary>
+public interface IDecompressionProvider
+{
+    /// <summary>
+    /// Creates a new decompression stream.
+    /// </summary>
+    /// <param name="stream">The compressed request body stream.</param>
+    /// <returns>The decompression stream.</returns>
+    Stream GetDecompressionStream(Stream stream);
+}

+ 19 - 0
src/Middleware/RequestDecompression/src/IRequestDecompressionProvider.cs

@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.RequestDecompression;
+
+/// <summary>
+/// Used to examine requests to see if decompression should be used.
+/// </summary>
+public interface IRequestDecompressionProvider
+{
+    /// <summary>
+    /// Examines the request and selects an acceptable decompression provider, if any.
+    /// </summary>
+    /// <param name="context">The <see cref="HttpContext"/>.</param>
+    /// <returns>The decompression stream when the provider is capable of decompressing the HTTP request body, otherwise <see langword="null" />.</returns>
+    Stream? GetDecompressionStream(HttpContext context);
+}

+ 24 - 0
src/Middleware/RequestDecompression/src/Microsoft.AspNetCore.RequestDecompression.csproj

@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <Description>ASP.NET Core middleware for HTTP Request decompression.</Description>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <IsAspNetCoreApp>true</IsAspNetCoreApp>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <PackageTags>aspnetcore</PackageTags>
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Http" />
+    <Reference Include="Microsoft.AspNetCore.Http.Extensions" />
+    <Reference Include="Microsoft.Extensions.Logging.Abstractions" />
+    <Reference Include="Microsoft.Extensions.Options" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <InternalsVisibleTo Include="Microsoft.AspNetCore.RequestDecompression.Tests" />
+    <InternalsVisibleTo Include="Microsoft.AspNetCore.RequestDecompression.Microbenchmarks" />
+  </ItemGroup>
+
+</Project>

+ 1 - 0
src/Middleware/RequestDecompression/src/PublicAPI.Shipped.txt

@@ -0,0 +1 @@
+#nullable enable

+ 13 - 0
src/Middleware/RequestDecompression/src/PublicAPI.Unshipped.txt

@@ -0,0 +1,13 @@
+#nullable enable
+Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions
+Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider
+Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider.GetDecompressionStream(System.IO.Stream! stream) -> System.IO.Stream!
+Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider
+Microsoft.AspNetCore.RequestDecompression.IRequestDecompressionProvider.GetDecompressionStream(Microsoft.AspNetCore.Http.HttpContext! context) -> System.IO.Stream?
+Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions
+Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.DecompressionProviders.get -> System.Collections.Generic.IDictionary<string!, Microsoft.AspNetCore.RequestDecompression.IDecompressionProvider!>!
+Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions.RequestDecompressionOptions() -> void
+Microsoft.Extensions.DependencyInjection.RequestDecompressionServiceExtensions
+static Microsoft.AspNetCore.Builder.RequestDecompressionBuilderExtensions.UseRequestDecompression(this Microsoft.AspNetCore.Builder.IApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
+static Microsoft.Extensions.DependencyInjection.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
+static Microsoft.Extensions.DependencyInjection.RequestDecompressionServiceExtensions.AddRequestDecompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.AspNetCore.RequestDecompression.RequestDecompressionOptions!>! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!

+ 26 - 0
src/Middleware/RequestDecompression/src/RequestDecompressionBuilderExtensions.cs

@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.RequestDecompression;
+
+namespace Microsoft.AspNetCore.Builder;
+
+/// <summary>
+/// Extension methods for the HTTP request decompression middleware.
+/// </summary>
+public static class RequestDecompressionBuilderExtensions
+{
+    /// <summary>
+    /// Adds middleware for dynamically decompressing HTTP request bodies.
+    /// </summary>
+    /// <param name="builder">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
+    public static IApplicationBuilder UseRequestDecompression(this IApplicationBuilder builder)
+    {
+        if (builder is null)
+        {
+            throw new ArgumentNullException(nameof(builder));
+        }
+
+        return builder.UseMiddleware<RequestDecompressionMiddleware>();
+    }
+}

+ 141 - 0
src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs

@@ -0,0 +1,141 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Globalization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Http.Metadata;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.RequestDecompression;
+
+/// <summary>
+/// Enables HTTP request decompression.
+/// </summary>
+internal sealed partial class RequestDecompressionMiddleware
+{
+    private readonly RequestDelegate _next;
+    private readonly ILogger<RequestDecompressionMiddleware> _logger;
+    private readonly IRequestDecompressionProvider _provider;
+
+    /// <summary>
+    /// Initialize the request decompression middleware.
+    /// </summary>
+    /// <param name="next">The delegate representing the remaining middleware in the request pipeline.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="provider">The <see cref="IRequestDecompressionProvider"/>.</param>
+    public RequestDecompressionMiddleware(
+        RequestDelegate next,
+        ILogger<RequestDecompressionMiddleware> logger,
+        IRequestDecompressionProvider provider)
+    {
+        if (next is null)
+        {
+            throw new ArgumentNullException(nameof(next));
+        }
+
+        if (logger is null)
+        {
+            throw new ArgumentNullException(nameof(logger));
+        }
+
+        if (provider is null)
+        {
+            throw new ArgumentNullException(nameof(provider));
+        }
+
+        _next = next;
+        _logger = logger;
+        _provider = provider;
+    }
+
+    /// <summary>
+    /// Invoke the middleware.
+    /// </summary>
+    /// <param name="context">The <see cref="HttpContext"/>.</param>
+    /// <returns>A task that represents the execution of this middleware.</returns>
+    public Task Invoke(HttpContext context)
+    {
+        SetMaxRequestBodySize(context);
+
+        var decompressionStream = _provider.GetDecompressionStream(context);
+        if (decompressionStream is null)
+        {
+            return _next(context);
+        }
+
+        return InvokeCore(context, decompressionStream);
+    }
+
+    private async Task InvokeCore(HttpContext context, Stream decompressionStream)
+    {
+        var request = context.Request.Body;
+        try
+        {
+            var sizeLimit =
+                context.GetEndpoint()?.Metadata?.GetMetadata<IRequestSizeLimitMetadata>()?.MaxRequestBodySize
+                    ?? context.Features.Get<IHttpMaxRequestBodySizeFeature>()?.MaxRequestBodySize;
+
+            context.Request.Body = new SizeLimitedStream(decompressionStream, sizeLimit);
+            await _next(context);
+        }
+        finally
+        {
+            context.Request.Body = request;
+            await decompressionStream.DisposeAsync();
+        }
+    }
+
+    private void SetMaxRequestBodySize(HttpContext context)
+    {
+        var sizeLimitMetadata = context.GetEndpoint()?.Metadata?.GetMetadata<IRequestSizeLimitMetadata>();
+        if (sizeLimitMetadata == null)
+        {
+            Log.MetadataNotFound(_logger);
+            return;
+        }
+
+        var maxRequestBodySizeFeature = context.Features.Get<IHttpMaxRequestBodySizeFeature>();
+        if (maxRequestBodySizeFeature == null)
+        {
+            Log.FeatureNotFound(_logger);
+        }
+        else if (maxRequestBodySizeFeature.IsReadOnly)
+        {
+            Log.FeatureIsReadOnly(_logger);
+        }
+        else
+        {
+            var maxRequestBodySize = sizeLimitMetadata.MaxRequestBodySize;
+            maxRequestBodySizeFeature.MaxRequestBodySize = maxRequestBodySize;
+
+            if (maxRequestBodySize.HasValue)
+            {
+                Log.MaxRequestBodySizeSet(_logger,
+                    maxRequestBodySize.Value.ToString(CultureInfo.InvariantCulture));
+            }
+            else
+            {
+                Log.MaxRequestBodySizeDisabled(_logger);
+            }
+        }
+    }
+
+    private static partial class Log
+    {
+        [LoggerMessage(1, LogLevel.Debug, $"The endpoint does not specify the {nameof(IRequestSizeLimitMetadata)}.", EventName = "MetadataNotFound")]
+        public static partial void MetadataNotFound(ILogger logger);
+
+        [LoggerMessage(2, LogLevel.Warning, $"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}.", EventName = "FeatureNotFound")]
+        public static partial void FeatureNotFound(ILogger logger);
+
+        [LoggerMessage(3, LogLevel.Warning, $"A request body size limit could not be applied. The {nameof(IHttpMaxRequestBodySizeFeature)} for the server is read-only.", EventName = "FeatureIsReadOnly")]
+        public static partial void FeatureIsReadOnly(ILogger logger);
+
+        [LoggerMessage(4, LogLevel.Debug, "The maximum request body size has been set to {RequestSize}.", EventName = "MaxRequestBodySizeSet")]
+        public static partial void MaxRequestBodySizeSet(ILogger logger, string requestSize);
+
+        [LoggerMessage(5, LogLevel.Debug, "The maximum request body size as been disabled.", EventName = "MaxRequestBodySizeDisabled")]
+        public static partial void MaxRequestBodySizeDisabled(ILogger logger);
+    }
+}

+ 20 - 0
src/Middleware/RequestDecompression/src/RequestDecompressionOptions.cs

@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.RequestDecompression;
+
+/// <summary>
+/// Options for the HTTP request decompression middleware.
+/// </summary>
+public sealed class RequestDecompressionOptions
+{
+    /// <summary>
+    /// The <see cref="IDecompressionProvider"/> types to use for request decompression.
+    /// </summary>
+    public IDictionary<string, IDecompressionProvider> DecompressionProviders { get; } = new Dictionary<string, IDecompressionProvider>(StringComparer.OrdinalIgnoreCase)
+    {
+        ["br"] = new BrotliDecompressionProvider(),
+        ["deflate"] = new DeflateDecompressionProvider(),
+        ["gzip"] = new GZipDecompressionProvider()
+    };
+}

+ 52 - 0
src/Middleware/RequestDecompression/src/RequestDecompressionServiceExtensions.cs

@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.RequestDecompression;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+/// <summary>
+/// Extension methods for the HTTP request decompression middleware.
+/// </summary>
+public static class RequestDecompressionServiceExtensions
+{
+    /// <summary>
+    /// Add request decompression services.
+    /// </summary>
+    /// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
+    /// <returns>The <see cref="IServiceCollection"/>.</returns>
+    public static IServiceCollection AddRequestDecompression(this IServiceCollection services)
+    {
+        if (services is null)
+        {
+            throw new ArgumentNullException(nameof(services));
+        }
+
+        services.TryAddSingleton<IRequestDecompressionProvider, DefaultRequestDecompressionProvider>();
+        return services;
+    }
+
+    /// <summary>
+    /// Add request decompression services and configure the related options.
+    /// </summary>
+    /// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
+    /// <param name="configureOptions">A delegate to configure the <see cref="RequestDecompressionOptions"/>.</param>
+    /// <returns>The <see cref="IServiceCollection"/>.</returns>
+    public static IServiceCollection AddRequestDecompression(this IServiceCollection services, Action<RequestDecompressionOptions> configureOptions)
+    {
+        if (services is null)
+        {
+            throw new ArgumentNullException(nameof(services));
+        }
+
+        if (configureOptions is null)
+        {
+            throw new ArgumentNullException(nameof(configureOptions));
+        }
+
+        services.Configure(configureOptions);
+        services.TryAddSingleton<IRequestDecompressionProvider, DefaultRequestDecompressionProvider>();
+        return services;
+    }
+}

+ 94 - 0
src/Middleware/RequestDecompression/src/SizeLimitedStream.cs

@@ -0,0 +1,94 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.RequestDecompression;
+
+internal sealed class SizeLimitedStream : Stream
+{
+    private readonly Stream _innerStream;
+    private readonly long? _sizeLimit;
+
+    private long _totalBytesRead;
+
+    public SizeLimitedStream(Stream innerStream, long? sizeLimit)
+    {
+        if (innerStream is null)
+        {
+            throw new ArgumentNullException(nameof(innerStream));
+        }
+
+        _innerStream = innerStream;
+        _sizeLimit = sizeLimit;
+    }
+
+    public override bool CanRead => _innerStream.CanRead;
+
+    public override bool CanSeek => _innerStream.CanSeek;
+
+    public override bool CanWrite => _innerStream.CanWrite;
+
+    public override long Length => _innerStream.Length;
+
+    public override long Position
+    {
+        get
+        {
+            return _innerStream.Position;
+        }
+        set
+        {
+            _innerStream.Position = value;
+        }
+    }
+
+    public override void Flush()
+    {
+        _innerStream.Flush();
+    }
+
+    public override int Read(byte[] buffer, int offset, int count)
+    {
+        var bytesRead = _innerStream.Read(buffer, offset, count);
+
+        _totalBytesRead += bytesRead;
+        if (_totalBytesRead > _sizeLimit)
+        {
+            throw new InvalidOperationException("The maximum number of bytes have been read.");
+        }
+
+        return bytesRead;
+    }
+
+    public override long Seek(long offset, SeekOrigin origin)
+    {
+        return _innerStream.Seek(offset, origin);
+    }
+
+    public override void SetLength(long value)
+    {
+        _innerStream.SetLength(value);
+    }
+
+    public override void Write(byte[] buffer, int offset, int count)
+    {
+        _innerStream.Write(buffer, offset, count);
+    }
+
+    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+    {
+        return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
+    }
+
+    public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+    {
+        var bytesRead = await _innerStream.ReadAsync(buffer, cancellationToken);
+
+        _totalBytesRead += bytesRead;
+        if (_totalBytesRead > _sizeLimit)
+        {
+            throw new InvalidOperationException("The maximum number of bytes have been read.");
+        }
+
+        return bytesRead;
+    }
+}

+ 176 - 0
src/Middleware/RequestDecompression/test/DefaultRequestDecompressionProviderTests.cs

@@ -0,0 +1,176 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Net.Http.Headers;
+using Microsoft.Extensions.Primitives;
+using System.IO.Compression;
+
+namespace Microsoft.AspNetCore.RequestDecompression.Tests;
+
+public class DefaultRequestDecompressionProviderTests
+{
+    [Theory]
+    [InlineData("br", typeof(BrotliStream))]
+    [InlineData("BR", typeof(BrotliStream))]
+    [InlineData("deflate", typeof(DeflateStream))]
+    [InlineData("DEFLATE", typeof(DeflateStream))]
+    [InlineData("gzip", typeof(GZipStream))]
+    [InlineData("GZIP", typeof(GZipStream))]
+    public void GetDecompressionProvider_SupportedContentEncoding_ReturnsProvider(
+        string contentEncoding,
+        Type expectedProviderType)
+    {
+        // Arrange
+        var httpContext = new DefaultHttpContext();
+        httpContext.Request.Headers.Add(HeaderNames.ContentEncoding, contentEncoding);
+
+        var (logger, sink) = GetTestLogger();
+        var options = Options.Create(new RequestDecompressionOptions());
+
+        var provider = new DefaultRequestDecompressionProvider(logger, options);
+
+        // Act
+        var matchingProvider = provider.GetDecompressionStream(httpContext);
+
+        // Assert
+        Assert.NotNull(matchingProvider);
+        Assert.IsType(expectedProviderType, matchingProvider);
+
+        var logMessages = sink.Writes.ToList();
+        AssertLog(logMessages.Single(), LogLevel.Debug,
+            $"The request will be decompressed with '{contentEncoding.ToLowerInvariant()}'.");
+
+        var contentEncodingHeader = httpContext.Request.Headers.ContentEncoding;
+        Assert.Empty(contentEncodingHeader);
+    }
+
+    [Fact]
+    public void GetDecompressionProvider_NoContentEncoding_ReturnsNull()
+    {
+        // Arrange
+        var httpContext = new DefaultHttpContext();
+
+        var (logger, sink) = GetTestLogger();
+        var options = Options.Create(new RequestDecompressionOptions());
+
+        var provider = new DefaultRequestDecompressionProvider(logger, options);
+
+        // Act
+        var matchingProvider = provider.GetDecompressionStream(httpContext);
+
+        // Assert
+        Assert.Null(matchingProvider);
+
+        var logMessages = sink.Writes.ToList();
+        AssertLog(logMessages.Single(), LogLevel.Trace,
+            "The Content-Encoding header is empty or not specified. Skipping request decompression.");
+
+        var contentEncodingHeader = httpContext.Request.Headers.ContentEncoding;
+        Assert.Empty(contentEncodingHeader);
+    }
+
+    [Fact]
+    public void GetDecompressionProvider_UnsupportedContentEncoding_ReturnsNull()
+    {
+        // Arrange
+        var contentEncoding = "custom";
+        var httpContext = new DefaultHttpContext();
+        httpContext.Request.Headers.Add(HeaderNames.ContentEncoding, contentEncoding);
+
+        var (logger, sink) = GetTestLogger();
+        var options = Options.Create(new RequestDecompressionOptions());
+
+        var provider = new DefaultRequestDecompressionProvider(logger, options);
+
+        // Act
+        var matchingProvider = provider.GetDecompressionStream(httpContext);
+
+        // Assert
+        Assert.Null(matchingProvider);
+
+        var logMessages = sink.Writes.ToList();
+        AssertLog(logMessages.Single(),
+            LogLevel.Debug, "No matching request decompression provider found.");
+
+        var contentEncodingHeader = httpContext.Request.Headers.ContentEncoding;
+        Assert.Equal(contentEncoding, contentEncodingHeader);
+    }
+
+    [Fact]
+    public void GetDecompressionProvider_MultipleContentEncodings_ReturnsNull()
+    {
+        // Arrange
+        var contentEncodings = new StringValues(new[] { "br", "gzip" });
+
+        var httpContext = new DefaultHttpContext();
+        httpContext.Request.Headers.Add(HeaderNames.ContentEncoding, contentEncodings);
+
+        var (logger, sink) = GetTestLogger();
+        var options = Options.Create(new RequestDecompressionOptions());
+
+        var provider = new DefaultRequestDecompressionProvider(logger, options);
+
+        // Act
+        var matchingProvider = provider.GetDecompressionStream(httpContext);
+
+        // Assert
+        Assert.Null(matchingProvider);
+
+        var logMessages = sink.Writes.ToList();
+        AssertLog(logMessages.Single(), LogLevel.Debug,
+            "Request decompression is not supported for multiple Content-Encodings.");
+
+        var contentEncodingHeader = httpContext.Request.Headers.ContentEncoding;
+        Assert.Equal(contentEncodings, contentEncodingHeader);
+    }
+
+    [Fact]
+    public void Ctor_NullLogger_Throws()
+    {
+        // Arrange
+        var (logger, _) = GetTestLogger();
+        IOptions<RequestDecompressionOptions> options = null;
+
+        // Act + Assert
+        Assert.Throws<ArgumentNullException>(() =>
+        {
+            new DefaultRequestDecompressionProvider(logger, options);
+        });
+    }
+
+    [Fact]
+    public void Ctor_NullOptions_Throws()
+    {
+        // Arrange
+        ILogger<DefaultRequestDecompressionProvider> logger = null;
+        var options = Options.Create(new RequestDecompressionOptions());
+
+        // Act + Assert
+        Assert.Throws<ArgumentNullException>(() =>
+        {
+            new DefaultRequestDecompressionProvider(logger, options);
+        });
+    }
+
+    private static (ILogger<DefaultRequestDecompressionProvider>, TestSink) GetTestLogger()
+    {
+        var sink = new TestSink(
+           TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
+           TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
+
+        var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+        var logger = loggerFactory.CreateLogger<DefaultRequestDecompressionProvider>();
+
+        return (logger, sink);
+    }
+
+    private static void AssertLog(WriteContext log, LogLevel level, string message)
+    {
+        Assert.Equal(level, log.LogLevel);
+        Assert.Equal(message, log.State.ToString());
+    }
+}

+ 14 - 0
src/Middleware/RequestDecompression/test/Microsoft.AspNetCore.RequestDecompression.Tests.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Http" />
+    <Reference Include="Microsoft.AspNetCore.RequestDecompression" />
+    <Reference Include="Microsoft.AspNetCore.TestHost" />
+    <Reference Include="Microsoft.Net.Http.Headers" />
+  </ItemGroup>
+
+</Project>

+ 22 - 0
src/Middleware/RequestDecompression/test/RequestDecompressionBuilderExtensionsTests.cs

@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Builder;
+
+namespace Microsoft.AspNetCore.RequestDecompression.Tests;
+
+public class RequestDecompressionBuilderExtensionsTests
+{
+    [Fact]
+    public void UseRequestDecompression_NullApplicationBuilder_Throws()
+    {
+        // Arrange
+        IApplicationBuilder builder = null;
+
+        // Act + Assert
+        Assert.Throws<ArgumentNullException>(() =>
+        {
+            builder.UseRequestDecompression();
+        });
+    }
+}

+ 945 - 0
src/Middleware/RequestDecompression/test/RequestDecompressionMiddlewareTests.cs

@@ -0,0 +1,945 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Globalization;
+using System.IO.Compression;
+using System.Net.Http;
+using System.Text;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Http.Metadata;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Testing;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.RequestDecompression.Tests;
+
+public class RequestDecompressionMiddlewareTests
+{
+    private const string TestRequestBodyData = "Test Request Body Data";
+
+    private static byte[] GetUncompressedContent(string input = TestRequestBodyData)
+    {
+        return Encoding.UTF8.GetBytes(input);
+    }
+
+    private static async Task<byte[]> GetCompressedContent(
+        Func<Stream, Stream> compressorDelegate,
+        byte[] uncompressedBytes)
+    {
+        await using var uncompressedStream = new MemoryStream(uncompressedBytes);
+
+        await using var compressedStream = new MemoryStream();
+        await using (var compressor = compressorDelegate(compressedStream))
+        {
+            await uncompressedStream.CopyToAsync(compressor);
+        }
+
+        return compressedStream.ToArray();
+    }
+
+    private static async Task<byte[]> GetBrotliCompressedContent(byte[] uncompressedBytes)
+    {
+        static Stream compressorDelegate(Stream compressedContent) =>
+            new BrotliStream(compressedContent, CompressionMode.Compress);
+
+        return await GetCompressedContent(compressorDelegate, uncompressedBytes);
+    }
+
+    private static async Task<byte[]> GetDeflateCompressedContent(byte[] uncompressedBytes)
+    {
+        static Stream compressorDelegate(Stream compressedContent) =>
+            new DeflateStream(compressedContent, CompressionMode.Compress);
+
+        return await GetCompressedContent(compressorDelegate, uncompressedBytes);
+    }
+
+    private static async Task<byte[]> GetGZipCompressedContent(byte[] uncompressedBytes)
+    {
+        static Stream compressorDelegate(Stream compressedContent) =>
+            new GZipStream(compressedContent, CompressionMode.Compress);
+
+        return await GetCompressedContent(compressorDelegate, uncompressedBytes);
+    }
+
+    [Fact]
+    public async Task Request_ContentEncodingBrotli_Decompressed()
+    {
+        // Arrange
+        var contentEncoding = "br";
+        var uncompressedBytes = GetUncompressedContent();
+        var compressedBytes = await GetBrotliCompressedContent(uncompressedBytes);
+
+        // Act
+        var (logMessages, decompressedBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding });
+
+        // Assert
+        AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant());
+        Assert.Equal(uncompressedBytes, decompressedBytes);
+    }
+
+    [Fact]
+    public async Task Request_ContentEncodingDeflate_Decompressed()
+    {
+        // Arrange
+        var contentEncoding = "deflate";
+        var uncompressedBytes = GetUncompressedContent();
+        var compressedBytes = await GetDeflateCompressedContent(uncompressedBytes);
+
+        // Act
+        var (logMessages, decompressedBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding });
+
+        // Assert
+        AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant());
+        Assert.Equal(uncompressedBytes, decompressedBytes);
+    }
+
+    [Fact]
+    public async Task Request_ContentEncodingGzip_Decompressed()
+    {
+        // Arrange
+        var contentEncoding = "gzip";
+        var uncompressedBytes = GetUncompressedContent();
+        var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
+
+        // Act
+        var (logMessages, decompressedBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding });
+
+        // Assert
+        AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant());
+        Assert.Equal(uncompressedBytes, decompressedBytes);
+    }
+
+    [Fact]
+    public async Task Request_NoContentEncoding_NotDecompressed()
+    {
+        // Arrange
+        var uncompressedBytes = GetUncompressedContent();
+
+        // Act
+        var (logMessages, outputBytes) = await InvokeMiddleware(uncompressedBytes);
+
+        // Assert
+        var logMessage = Assert.Single(logMessages);
+        AssertLog(logMessage, LogLevel.Trace, "The Content-Encoding header is empty or not specified. Skipping request decompression.");
+        Assert.Equal(uncompressedBytes, outputBytes);
+    }
+
+    [Fact]
+    public async Task Request_UnsupportedContentEncoding_NotDecompressed()
+    {
+        // Arrange
+        var uncompressedBytes = GetUncompressedContent();
+        var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
+        var contentEncoding = "custom";
+
+        // Act
+        var (logMessages, outputBytes) = await InvokeMiddleware(compressedBytes, new[] { contentEncoding });
+
+        // Assert
+        AssertNoDecompressionProviderLog(logMessages);
+        Assert.Equal(compressedBytes, outputBytes);
+    }
+
+    [Fact]
+    public async Task Request_MultipleContentEncodings_NotDecompressed()
+    {
+        // Arrange
+        var uncompressedBytes = GetUncompressedContent();
+        var inputBytes = await GetGZipCompressedContent(uncompressedBytes);
+        var contentEncodings = new[] { "br", "gzip" };
+
+        // Act
+        var (logMessages, outputBytes) = await InvokeMiddleware(inputBytes, contentEncodings);
+
+        // Assert
+        var logMessage = Assert.Single(logMessages);
+        AssertLog(logMessage, LogLevel.Debug, "Request decompression is not supported for multiple Content-Encodings.");
+        Assert.Equal(inputBytes, outputBytes);
+    }
+
+    [Fact]
+    public async Task Request_MiddlewareAddedMultipleTimes_OnlyDecompressedOnce()
+    {
+        // Arrange
+        var uncompressedBytes = GetUncompressedContent();
+        var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
+        var contentEncoding = "gzip";
+
+        var decompressedBytes = Array.Empty<byte>();
+
+        var sink = new TestSink(
+            TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
+            TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
+        var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+        using var host = new HostBuilder()
+            .ConfigureWebHost(webHostBuilder =>
+            {
+                webHostBuilder
+                .UseTestServer()
+                .ConfigureServices(services =>
+                {
+                    services.AddRequestDecompression();
+                    services.AddSingleton<ILoggerFactory>(loggerFactory);
+                })
+                .Configure(app =>
+                {
+                    app.Use((context, next) =>
+                    {
+                        context.Features.Set<IHttpMaxRequestBodySizeFeature>(
+                            new FakeHttpMaxRequestBodySizeFeature());
+                        return next(context);
+                    });
+                    app.UseRequestDecompression();
+                    app.UseRequestDecompression();
+                    app.Run(async context =>
+                    {
+                        await using var ms = new MemoryStream();
+                        await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
+                        decompressedBytes = ms.ToArray();
+                    });
+                });
+            }).Build();
+
+        await host.StartAsync();
+
+        var server = host.GetTestServer();
+        var client = server.CreateClient();
+
+        using var request = new HttpRequestMessage(HttpMethod.Post, "");
+        request.Content = new ByteArrayContent(compressedBytes);
+        request.Content.Headers.ContentEncoding.Add(contentEncoding);
+
+        // Act
+        await client.SendAsync(request);
+
+        // Assert
+        var logMessages = sink.Writes.ToList();
+
+        Assert.Equal(2, logMessages.Count);
+        AssertLog(logMessages.First(), LogLevel.Debug, $"The request will be decompressed with '{contentEncoding}'.");
+        AssertLog(logMessages.Skip(1).First(), LogLevel.Trace, "The Content-Encoding header is empty or not specified. Skipping request decompression.");
+
+        Assert.Equal(uncompressedBytes, decompressedBytes);
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task Request_Decompressed_ContentEncodingHeaderRemoved(bool isDecompressed)
+    {
+        // Arrange
+        var contentEncoding = isDecompressed ? "gzip" : "custom";
+        var contentEncodingHeader = new StringValues();
+
+        var uncompressedBytes = GetUncompressedContent();
+        var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
+
+        var outputBytes = Array.Empty<byte>();
+
+        var sink = new TestSink(
+            TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
+            TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
+        var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+        using var host = new HostBuilder()
+            .ConfigureWebHost(webHostBuilder =>
+            {
+                webHostBuilder
+                .UseTestServer()
+                .ConfigureServices(services =>
+                {
+                    services.AddRequestDecompression();
+                    services.AddSingleton<ILoggerFactory>(loggerFactory);
+                })
+                .Configure(app =>
+                {
+                    app.Use((context, next) =>
+                    {
+                        context.Features.Set<IHttpMaxRequestBodySizeFeature>(
+                            new FakeHttpMaxRequestBodySizeFeature());
+                        return next(context);
+                    });
+                    app.UseRequestDecompression();
+                    app.Run(async context =>
+                    {
+                        contentEncodingHeader = context.Request.Headers.ContentEncoding;
+
+                        await using var ms = new MemoryStream();
+                        await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
+                        outputBytes = ms.ToArray();
+                    });
+                });
+            }).Build();
+
+        await host.StartAsync();
+
+        var server = host.GetTestServer();
+        var client = server.CreateClient();
+
+        using var request = new HttpRequestMessage(HttpMethod.Post, "");
+        request.Content = new ByteArrayContent(compressedBytes);
+        request.Content.Headers.ContentEncoding.Add(contentEncoding);
+
+        // Act
+        await client.SendAsync(request);
+
+        // Assert
+        var logMessages = sink.Writes.ToList();
+
+        if (isDecompressed)
+        {
+            Assert.Empty(contentEncodingHeader);
+
+            AssertDecompressedWithLog(logMessages, contentEncoding);
+            Assert.Equal(uncompressedBytes, outputBytes);
+        }
+        else
+        {
+            Assert.Equal(contentEncoding, contentEncodingHeader);
+
+            AssertNoDecompressionProviderLog(logMessages);
+            Assert.Equal(compressedBytes, outputBytes);
+        }
+    }
+
+    [Fact]
+    public async Task Request_InvalidDataForContentEncoding_ThrowsInvalidOperationException()
+    {
+        // Arrange
+        var uncompressedBytes = GetUncompressedContent();
+        var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
+        var contentEncoding = "br";
+
+        Exception exception = null;
+
+        var sink = new TestSink(
+            TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
+            TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
+        var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+        using var host = new HostBuilder()
+            .ConfigureWebHost(webHostBuilder =>
+            {
+                webHostBuilder
+                .UseTestServer()
+                .ConfigureServices(services =>
+                {
+                    services.AddRequestDecompression();
+                    services.AddSingleton<ILoggerFactory>(loggerFactory);
+                })
+                .Configure(app =>
+                {
+                    app.Use((context, next) =>
+                    {
+                        context.Features.Set<IHttpMaxRequestBodySizeFeature>(
+                            new FakeHttpMaxRequestBodySizeFeature());
+                        return next(context);
+                    });
+                    app.UseRequestDecompression();
+                    app.Run(async context =>
+                    {
+                        exception = await Record.ExceptionAsync(async () =>
+                        {
+                            using var ms = new MemoryStream();
+                            await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
+                        });
+                    });
+                });
+            }).Build();
+
+        await host.StartAsync();
+
+        var server = host.GetTestServer();
+        var client = server.CreateClient();
+
+        using var request = new HttpRequestMessage(HttpMethod.Post, "");
+        request.Content = new ByteArrayContent(compressedBytes);
+        request.Content.Headers.ContentEncoding.Add(contentEncoding);
+
+        // Act
+        await client.SendAsync(request);
+
+        // Assert
+        var logMessages = sink.Writes.ToList();
+
+        AssertDecompressedWithLog(logMessages, contentEncoding.ToLowerInvariant());
+
+        Assert.NotNull(exception);
+        Assert.IsAssignableFrom<InvalidOperationException>(exception);
+    }
+
+    [Fact]
+    public async Task Options_RegisterCustomDecompressionProvider()
+    {
+        // Arrange
+        var uncompressedBytes = GetUncompressedContent();
+        var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
+        var contentEncoding = "custom";
+
+        // Act
+        var (logMessages, decompressedBytes) =
+            await InvokeMiddleware(
+                compressedBytes,
+                new[] { contentEncoding },
+                configure: (RequestDecompressionOptions options) =>
+                {
+                    options.DecompressionProviders.Add(contentEncoding, new CustomDecompressionProvider());
+                });
+
+        // Assert
+        AssertDecompressedWithLog(logMessages, contentEncoding);
+        Assert.Equal(uncompressedBytes, decompressedBytes);
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task Endpoint_HasRequestSizeLimit_UsedForRequest(bool exceedsLimit)
+    {
+        // Arrange
+        long attributeSizeLimit = 10;
+        long featureSizeLimit = 5;
+
+        var contentEncoding = "gzip";
+        var uncompressedBytes = new byte[attributeSizeLimit + (exceedsLimit ? 1 : 0)];
+        var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
+
+        var decompressedBytes = Array.Empty<byte>();
+        Exception exception = null;
+
+        var sink = new TestSink(
+           TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
+           TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
+        var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+        using var host = new HostBuilder()
+            .ConfigureWebHost(webHostBuilder =>
+            {
+                webHostBuilder
+                .UseTestServer()
+                .ConfigureServices(services =>
+                {
+                    services.AddRequestDecompression();
+                    services.AddSingleton<ILoggerFactory>(loggerFactory);
+                })
+                .Configure(app =>
+                {
+                    app.Use((context, next) =>
+                    {
+                        context.Features.Set<IEndpointFeature>(
+                            GetFakeEndpointFeature(attributeSizeLimit));
+                        context.Features.Set<IHttpMaxRequestBodySizeFeature>(
+                            new FakeHttpMaxRequestBodySizeFeature(featureSizeLimit));
+
+                        return next(context);
+                    });
+                    app.UseRequestDecompression();
+                    app.Run(async context =>
+                    {
+                        await using var ms = new MemoryStream();
+
+                        exception = await Record.ExceptionAsync(async () =>
+                        {
+                            await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
+                            decompressedBytes = ms.ToArray();
+                        });
+
+                        decompressedBytes = ms.ToArray();
+                    });
+                });
+            }).Build();
+
+        await host.StartAsync();
+
+        var server = host.GetTestServer();
+        var client = server.CreateClient();
+
+        using var request = new HttpRequestMessage(HttpMethod.Post, "");
+        request.Content = new ByteArrayContent(compressedBytes);
+        request.Content.Headers.ContentEncoding.Add(contentEncoding);
+
+        // Act
+        await client.SendAsync(request);
+
+        // Assert
+        var logMessages = sink.Writes.ToList();
+        AssertDecompressedWithLog(logMessages, contentEncoding);
+
+        if (exceedsLimit)
+        {
+            Assert.NotNull(exception);
+            Assert.IsAssignableFrom<InvalidOperationException>(exception);
+            Assert.Equal("The maximum number of bytes have been read.", exception.Message);
+        }
+        else
+        {
+            Assert.Null(exception);
+            Assert.Equal(uncompressedBytes, decompressedBytes);
+        }
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task Feature_HasRequestSizeLimit_UsedForRequest(bool exceedsLimit)
+    {
+        // Arrange
+        long featureSizeLimit = 10;
+
+        var contentEncoding = "gzip";
+        var uncompressedBytes = new byte[featureSizeLimit + (exceedsLimit ? 1 : 0)];
+        var compressedBytes = await GetGZipCompressedContent(uncompressedBytes);
+
+        var decompressedBytes = Array.Empty<byte>();
+        Exception exception = null;
+
+        var sink = new TestSink(
+           TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
+           TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
+        var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+        using var host = new HostBuilder()
+            .ConfigureWebHost(webHostBuilder =>
+            {
+                webHostBuilder
+                .UseTestServer()
+                .ConfigureServices(services =>
+                {
+                    services.AddRequestDecompression();
+                    services.AddSingleton<ILoggerFactory>(loggerFactory);
+                })
+                .Configure(app =>
+                {
+                    app.Use((context, next) =>
+                    {
+                        context.Features.Set<IHttpMaxRequestBodySizeFeature>(
+                            new FakeHttpMaxRequestBodySizeFeature(featureSizeLimit));
+
+                        return next(context);
+                    });
+                    app.UseRequestDecompression();
+                    app.Run(async context =>
+                    {
+                        await using var ms = new MemoryStream();
+
+                        exception = await Record.ExceptionAsync(async () =>
+                        {
+                            await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
+                            decompressedBytes = ms.ToArray();
+                        });
+
+                        decompressedBytes = ms.ToArray();
+                    });
+                });
+            }).Build();
+
+        await host.StartAsync();
+
+        var server = host.GetTestServer();
+        var client = server.CreateClient();
+
+        using var request = new HttpRequestMessage(HttpMethod.Post, "");
+        request.Content = new ByteArrayContent(compressedBytes);
+        request.Content.Headers.ContentEncoding.Add(contentEncoding);
+
+        // Act
+        await client.SendAsync(request);
+
+        // Assert
+        var logMessages = sink.Writes.ToList();
+        AssertDecompressedWithLog(logMessages, contentEncoding);
+
+        if (exceedsLimit)
+        {
+            Assert.NotNull(exception);
+            Assert.IsAssignableFrom<InvalidOperationException>(exception);
+            Assert.Equal("The maximum number of bytes have been read.", exception.Message);
+        }
+        else
+        {
+            Assert.Null(exception);
+            Assert.Equal(uncompressedBytes, decompressedBytes);
+        }
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task Endpoint_DoesNotHaveSizeLimitMetadata(bool isCompressed)
+    {
+        // Arrange
+        var sink = new TestSink();
+        var logger = new TestLogger<RequestDecompressionMiddleware>(
+            new TestLoggerFactory(sink, enabled: true));
+        IRequestDecompressionProvider provider = new FakeRequestDecompressionProvider(isCompressed);
+
+        var middleware = new RequestDecompressionMiddleware(
+            c =>
+            {
+                c.Response.StatusCode = StatusCodes.Status200OK;
+                return Task.CompletedTask;
+            },
+            logger,
+            provider);
+
+        var context = new DefaultHttpContext();
+
+        IEndpointFeature endpointFeature = new FakeEndpointFeature
+        {
+            Endpoint = new Endpoint(
+                requestDelegate: null,
+                metadata: new EndpointMetadataCollection(),
+                displayName: null)
+        };
+        context.HttpContext.Features.Set(endpointFeature);
+
+        long expectedRequestSizeLimit = 100;
+        IHttpMaxRequestBodySizeFeature maxRequestBodySizeFeature =
+            new FakeHttpMaxRequestBodySizeFeature(expectedRequestSizeLimit);
+        context.HttpContext.Features.Set(maxRequestBodySizeFeature);
+
+        // Act
+        await middleware.Invoke(context);
+
+        // Assert
+        var logMessages = sink.Writes.ToList();
+        AssertLog(Assert.Single(logMessages), LogLevel.Debug,
+            $"The endpoint does not specify the {nameof(IRequestSizeLimitMetadata)}.");
+
+        var actualRequestSizeLimit = maxRequestBodySizeFeature.MaxRequestBodySize;
+        Assert.Equal(expectedRequestSizeLimit, actualRequestSizeLimit);
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task Endpoint_DoesNotHaveBodySizeFeature(bool isCompressed)
+    {
+        // Arrange
+        var sink = new TestSink();
+        var logger = new TestLogger<RequestDecompressionMiddleware>(
+            new TestLoggerFactory(sink, enabled: true));
+        IRequestDecompressionProvider provider = new FakeRequestDecompressionProvider(isCompressed);
+
+        var middleware = new RequestDecompressionMiddleware(
+            c =>
+            {
+                c.Response.StatusCode = StatusCodes.Status200OK;
+                return Task.CompletedTask;
+            },
+            logger,
+            provider);
+
+        var context = new DefaultHttpContext();
+
+        IEndpointFeature endpointFeature = GetFakeEndpointFeature(100);
+        context.HttpContext.Features.Set(endpointFeature);
+
+        IHttpMaxRequestBodySizeFeature maxRequestBodySizeFeature = null;
+        context.HttpContext.Features.Set(maxRequestBodySizeFeature);
+
+        // Act
+        await middleware.Invoke(context);
+
+        // Assert
+        var logMessages = sink.Writes.ToList();
+        AssertLog(Assert.Single(logMessages), LogLevel.Warning,
+            $"A request body size limit could not be applied. This server does not support the {nameof(IHttpMaxRequestBodySizeFeature)}.");
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task Endpoint_BodySizeFeatureIsReadOnly(bool isCompressed)
+    {
+        // Arrange
+        var sink = new TestSink();
+        var logger = new TestLogger<RequestDecompressionMiddleware>(
+            new TestLoggerFactory(sink, enabled: true));
+        IRequestDecompressionProvider provider = new FakeRequestDecompressionProvider(isCompressed);
+
+        var middleware = new RequestDecompressionMiddleware(
+            c =>
+            {
+                c.Response.StatusCode = StatusCodes.Status200OK;
+                return Task.CompletedTask;
+            },
+            logger,
+            provider);
+
+        var context = new DefaultHttpContext();
+
+        IEndpointFeature endpointFeature = GetFakeEndpointFeature(100);
+        context.HttpContext.Features.Set(endpointFeature);
+
+        long expectedRequestSizeLimit = 50;
+        IHttpMaxRequestBodySizeFeature maxRequestBodySizeFeature =
+            new FakeHttpMaxRequestBodySizeFeature(expectedRequestSizeLimit, isReadOnly: true);
+        context.HttpContext.Features.Set(maxRequestBodySizeFeature);
+
+        // Act
+        await middleware.Invoke(context);
+
+        // Assert
+        var logMessages = sink.Writes.ToList();
+        AssertLog(Assert.Single(logMessages), LogLevel.Warning,
+            $"A request body size limit could not be applied. The {nameof(IHttpMaxRequestBodySizeFeature)} for the server is read-only.");
+
+        var actualRequestSizeLimit = maxRequestBodySizeFeature.MaxRequestBodySize;
+        Assert.Equal(expectedRequestSizeLimit, actualRequestSizeLimit);
+    }
+
+    [Theory]
+    [InlineData(true, true)]
+    [InlineData(true, false)]
+    [InlineData(false, false)]
+    [InlineData(false, true)]
+    public async Task Endpoint_HasBodySizeFeature_SetUsingSizeLimitMetadata(bool isCompressed, bool isRequestSizeLimitDisabled)
+    {
+        // Arrange
+        var sink = new TestSink();
+        var logger = new TestLogger<RequestDecompressionMiddleware>(
+            new TestLoggerFactory(sink, enabled: true));
+        IRequestDecompressionProvider provider = new FakeRequestDecompressionProvider(isCompressed);
+
+        var middleware = new RequestDecompressionMiddleware(
+            c =>
+            {
+                c.Response.StatusCode = StatusCodes.Status200OK;
+                return Task.CompletedTask;
+            },
+            logger,
+            provider);
+
+        var context = new DefaultHttpContext();
+
+        long? expectedRequestSizeLimit = isRequestSizeLimitDisabled ? null : 100;
+        IEndpointFeature endpointFeature = GetFakeEndpointFeature(expectedRequestSizeLimit);
+        context.HttpContext.Features.Set(endpointFeature);
+
+        IHttpMaxRequestBodySizeFeature maxRequestBodySizeFeature =
+            new FakeHttpMaxRequestBodySizeFeature(50);
+        context.HttpContext.Features.Set(maxRequestBodySizeFeature);
+
+        // Act
+        await middleware.Invoke(context);
+
+        // Assert
+        var logMessages = sink.Writes.ToList();
+
+        if (isRequestSizeLimitDisabled)
+        {
+            AssertLog(Assert.Single(logMessages), LogLevel.Debug,
+                "The maximum request body size as been disabled.");
+        }
+        else
+        {
+            AssertLog(Assert.Single(logMessages), LogLevel.Debug,
+                $"The maximum request body size has been set to {expectedRequestSizeLimit.Value.ToString(CultureInfo.InvariantCulture)}.");
+        }
+
+        var actualRequestSizeLimit = maxRequestBodySizeFeature.MaxRequestBodySize;
+        Assert.Equal(expectedRequestSizeLimit, actualRequestSizeLimit);
+    }
+
+    [Fact]
+    public void Ctor_NullRequestDelegate_Throws()
+    {
+        // Arrange
+        RequestDelegate requestDelegate = null;
+        var logger = new TestLogger<RequestDecompressionMiddleware>(
+            new TestLoggerFactory(new TestSink(), enabled: true));
+        var provider = new FakeRequestDecompressionProvider();
+
+        // Act + Assert
+        Assert.Throws<ArgumentNullException>(() =>
+        {
+            new RequestDecompressionMiddleware(requestDelegate, logger, provider);
+        });
+    }
+
+    [Fact]
+    public void Ctor_NullLogger_Throws()
+    {
+        // Arrange
+        static Task requestDelegate(HttpContext context) => Task.FromResult(context);
+        ILogger<RequestDecompressionMiddleware> logger = null;
+        var provider = new FakeRequestDecompressionProvider();
+
+        // Act + Assert
+        Assert.Throws<ArgumentNullException>(() =>
+        {
+            new RequestDecompressionMiddleware(requestDelegate, logger, provider);
+        });
+    }
+
+    [Fact]
+    public void Ctor_NullRequestDecompressionProvider_Throws()
+    {
+        // Arrange
+        static Task requestDelegate(HttpContext context) => Task.FromResult(context);
+        var logger = new TestLogger<RequestDecompressionMiddleware>(
+            new TestLoggerFactory(new TestSink(), enabled: true));
+        IRequestDecompressionProvider provider = null;
+
+        // Act + Assert
+        Assert.Throws<ArgumentNullException>(() =>
+        {
+            new RequestDecompressionMiddleware(requestDelegate, logger, provider);
+        });
+    }
+
+    private class FakeRequestDecompressionProvider : IRequestDecompressionProvider
+    {
+        private readonly bool _isCompressed;
+
+        public FakeRequestDecompressionProvider(bool isCompressed = false)
+        {
+            _isCompressed = isCompressed;
+        }
+
+#nullable enable
+        public Stream? GetDecompressionStream(HttpContext context)
+            => _isCompressed
+                ? new MemoryStream()
+                : null;
+#nullable disable
+    }
+
+    private static void AssertLog(WriteContext log, LogLevel level, string message)
+    {
+        Assert.Equal(level, log.LogLevel);
+        Assert.Equal(message, log.State.ToString());
+    }
+
+    private static void AssertDecompressedWithLog(List<WriteContext> logMessages, string encoding)
+    {
+        var logMessage = Assert.Single(logMessages);
+        AssertLog(logMessage, LogLevel.Debug, $"The request will be decompressed with '{encoding}'.");
+    }
+
+    private static void AssertNoDecompressionProviderLog(List<WriteContext> logMessages)
+    {
+        var logMessage = Assert.Single(logMessages);
+        AssertLog(logMessage, LogLevel.Debug, "No matching request decompression provider found.");
+    }
+
+    private static async Task<(List<WriteContext>, byte[])> InvokeMiddleware(
+        byte[] compressedContent,
+        string[] contentEncodings = null,
+        Action<RequestDecompressionOptions> configure = null)
+    {
+        var sink = new TestSink(
+            TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>,
+            TestSink.EnableWithTypeName<DefaultRequestDecompressionProvider>);
+        var loggerFactory = new TestLoggerFactory(sink, enabled: true);
+
+        var outputContent = Array.Empty<byte>();
+
+        using var host = new HostBuilder()
+            .ConfigureWebHost(webHostBuilder =>
+            {
+                webHostBuilder
+                .UseTestServer()
+                .ConfigureServices(services =>
+                {
+                    services.AddRequestDecompression(configure ?? (_ => { }));
+                    services.AddSingleton<ILoggerFactory>(loggerFactory);
+                })
+                .Configure(app =>
+                {
+                    app.Use((context, next) =>
+                    {
+                        context.Features.Set<IHttpMaxRequestBodySizeFeature>(
+                            new FakeHttpMaxRequestBodySizeFeature());
+                        return next(context);
+                    });
+                    app.UseRequestDecompression();
+                    app.Run(async context =>
+                    {
+                        await using var ms = new MemoryStream();
+                        await context.Request.Body.CopyToAsync(ms, context.RequestAborted);
+                        outputContent = ms.ToArray();
+                    });
+                });
+            }).Build();
+
+        await host.StartAsync();
+
+        var server = host.GetTestServer();
+        var client = server.CreateClient();
+
+        using var request = new HttpRequestMessage(HttpMethod.Post, "");
+        request.Content = new ByteArrayContent(compressedContent);
+
+        if (contentEncodings != null)
+        {
+            foreach (var encoding in contentEncodings)
+            {
+                request.Content.Headers.ContentEncoding.Add(encoding);
+            }
+        }
+
+        await client.SendAsync(request);
+
+        return (sink.Writes.ToList(), outputContent);
+    }
+    private class CustomDecompressionProvider : IDecompressionProvider
+    {
+        public Stream GetDecompressionStream(Stream stream)
+        {
+            return new GZipStream(stream, CompressionMode.Decompress);
+        }
+    }
+
+    private static FakeEndpointFeature GetFakeEndpointFeature(long? requestSizeLimit)
+    {
+        var requestSizeLimitMetadata = new FakeRequestSizeLimitMetadata
+        {
+            MaxRequestBodySize = requestSizeLimit
+        };
+
+        var endpointMetadata =
+            new EndpointMetadataCollection(new[] { requestSizeLimitMetadata });
+
+        return new FakeEndpointFeature
+        {
+            Endpoint = new Endpoint(
+                requestDelegate: null,
+                metadata: endpointMetadata,
+                displayName: null)
+        };
+    }
+
+    private class FakeEndpointFeature : IEndpointFeature
+    {
+        public Endpoint Endpoint { get; set; }
+    }
+
+    private class FakeRequestSizeLimitMetadata : IRequestSizeLimitMetadata
+    {
+        public long? MaxRequestBodySize { get; set; }
+    }
+
+    private class FakeHttpMaxRequestBodySizeFeature : IHttpMaxRequestBodySizeFeature
+    {
+        public FakeHttpMaxRequestBodySizeFeature(
+            long? maxRequestBodySize = null,
+            bool isReadOnly = false)
+        {
+            MaxRequestBodySize = maxRequestBodySize;
+            IsReadOnly = isReadOnly;
+        }
+
+        public bool IsReadOnly { get; }
+
+        public long? MaxRequestBodySize { get; set; }
+    }
+}

+ 30 - 0
src/Middleware/RequestDecompression/test/RequestDecompressionOptionsTests.cs

@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.RequestDecompression.Tests;
+
+public class RequestDecompressionOptionsTests
+{
+    [Fact]
+    public void Options_InitializedWithDefaultProviders()
+    {
+        // Arrange
+        var defaultProviderCount = 3;
+
+        // Act
+        var options = new RequestDecompressionOptions();
+
+        // Assert
+        var providers = options.DecompressionProviders;
+        Assert.Equal(defaultProviderCount, providers.Count);
+
+        var brotliProvider = Assert.Contains("br", providers);
+        Assert.IsType<BrotliDecompressionProvider>(brotliProvider);
+
+        var deflateProvider = Assert.Contains("deflate", providers);
+        Assert.IsType<DeflateDecompressionProvider>(deflateProvider);
+
+        var gzipProvider = Assert.Contains("gzip", providers);
+        Assert.IsType<GZipDecompressionProvider>(gzipProvider);
+    }
+}

+ 37 - 0
src/Middleware/RequestDecompression/test/RequestDecompressionServiceExtensionsTests.cs

@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.RequestDecompression.Tests;
+
+public class RequestDecompressionServiceExtensionsTests
+{
+    [Fact]
+    public void AddRequestDecompression_NullServiceCollection_Throws()
+    {
+        // Arrange
+        IServiceCollection serviceCollection = null;
+        var configureOptions = (RequestDecompressionOptions options) => { };
+
+        // Act + Assert
+        Assert.Throws<ArgumentNullException>(() =>
+        {
+            serviceCollection.AddRequestDecompression(configureOptions);
+        });
+    }
+
+    [Fact]
+    public void AddRequestDecompression_NullConfigureOptions_Throws()
+    {
+        // Arrange
+        var serviceCollection = new ServiceCollection();
+        Action<RequestDecompressionOptions> configureOptions = null;
+
+        // Act + Assert
+        Assert.Throws<ArgumentNullException>(() =>
+        {
+            serviceCollection.AddRequestDecompression(configureOptions);
+        });
+    }
+}

+ 107 - 0
src/Middleware/RequestDecompression/test/SizeLimitedStreamTests.cs

@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.RequestDecompression.Tests;
+
+public class SizeLimitedStreamTests
+{
+    [Fact]
+    public void Ctor_NullInnerStream_Throws()
+    {
+        // Arrange
+        Stream innerStream = null;
+
+        // Act + Assert
+        Assert.Throws<ArgumentNullException>(() =>
+        {
+            using var sizeLimitedStream = new SizeLimitedStream(innerStream, 1);
+        });
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public async Task ReadAsync_InnerStreamExceedsSizeLimit_Throws(bool exceedsLimit)
+    {
+        // Arrange
+        var sizeLimit = 10;
+        var bytes = new byte[sizeLimit + (exceedsLimit ? 1 : 0)];
+
+        using var innerStream = new MemoryStream(bytes);
+        using var sizeLimitedStream = new SizeLimitedStream(innerStream, sizeLimit);
+
+        var buffer = new byte[bytes.Length];
+
+        // Act
+        var exception = await Record.ExceptionAsync(async () =>
+        {
+            while (await sizeLimitedStream.ReadAsync(buffer) > 0) { }
+        });
+
+        // Assert
+        AssertStreamReadingException(exception, exceedsLimit);
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public void Read_InnerStreamExceedsSizeLimit_Throws(bool exceedsLimit)
+    {
+        // Arrange
+        var sizeLimit = 10;
+        var bytes = new byte[sizeLimit + (exceedsLimit ? 1 : 0)];
+
+        using var innerStream = new MemoryStream(bytes);
+        using var sizeLimitedStream = new SizeLimitedStream(innerStream, sizeLimit);
+
+        var buffer = new byte[bytes.Length];
+
+        // Act
+        var exception = Record.Exception(() =>
+        {
+            while (sizeLimitedStream.Read(buffer, 0, buffer.Length) > 0) { }
+        });
+
+        // Assert
+        AssertStreamReadingException(exception, exceedsLimit);
+    }
+
+    [Theory]
+    [InlineData(true)]
+    [InlineData(false)]
+    public void BeginRead_InnerStreamExceedsSizeLimit_Throws(bool exceedsLimit)
+    {
+        // Arrange
+        var sizeLimit = 10;
+        var bytes = new byte[sizeLimit + (exceedsLimit ? 1 : 0)];
+
+        using var innerStream = new MemoryStream(bytes);
+        using var sizeLimitedStream = new SizeLimitedStream(innerStream, sizeLimit);
+
+        var buffer = new byte[bytes.Length];
+
+        // Act
+        var exception = Record.Exception(() =>
+        {
+            var asyncResult = sizeLimitedStream.BeginRead(buffer, 0, buffer.Length, (o) => { }, null);
+            sizeLimitedStream.EndRead(asyncResult);
+        });
+
+        // Assert
+        AssertStreamReadingException(exception, exceedsLimit);
+    }
+
+    private static void AssertStreamReadingException(Exception exception, bool exceedsLimit)
+    {
+        if (exceedsLimit)
+        {
+            Assert.NotNull(exception);
+            Assert.IsAssignableFrom<InvalidOperationException>(exception);
+            Assert.Equal("The maximum number of bytes have been read.", exception.Message);
+        }
+        else
+        {
+            Assert.Null(exception);
+        }
+    }
+}

+ 10 - 2
src/Mvc/Mvc.Core/src/DisableRequestSizeLimitAttribute.cs

@@ -1,6 +1,7 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using Microsoft.AspNetCore.Http.Metadata;
 using Microsoft.AspNetCore.Mvc.Filters;
 using Microsoft.Extensions.DependencyInjection;
 
@@ -9,8 +10,12 @@ namespace Microsoft.AspNetCore.Mvc;
 /// <summary>
 /// Disables the request body size limit.
 /// </summary>
+/// <remarks>
+/// Disabling the request body size limit can be a security concern in regards to uncontrolled
+/// resource consumption, particularly if the request body is being buffered.
+/// </remarks>
 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
-public class DisableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter
+public class DisableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter, IRequestSizeLimitMetadata
 {
     /// <summary>
     /// Gets the order value for determining the order of execution of filters. Filters execute in
@@ -39,4 +44,7 @@ public class DisableRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrde
         var filter = serviceProvider.GetRequiredService<DisableRequestSizeLimitFilter>();
         return filter;
     }
+
+    /// <inheritdoc />
+    long? IRequestSizeLimitMetadata.MaxRequestBodySize => null;
 }

+ 6 - 2
src/Mvc/Mvc.Core/src/RequestSizeLimitAttribute.cs

@@ -1,6 +1,7 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using Microsoft.AspNetCore.Http.Metadata;
 using Microsoft.AspNetCore.Mvc.Filters;
 using Microsoft.Extensions.DependencyInjection;
 
@@ -10,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc;
 /// Sets the request body size limit to the specified size.
 /// </summary>
 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
-public class RequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter
+public class RequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter, IRequestSizeLimitMetadata
 {
     private readonly long _bytes;
 
@@ -51,4 +52,7 @@ public class RequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilt
         filter.Bytes = _bytes;
         return filter;
     }
+
+    /// <inheritdoc />
+    long? IRequestSizeLimitMetadata.MaxRequestBodySize => _bytes;
 }