diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml
index e356ab3d42..065ee40bdd 100644
--- a/.azure/pipelines/ci.yml
+++ b/.azure/pipelines/ci.yml
@@ -71,6 +71,7 @@ variables:
# The following extra properties are not set when testing. Use with final build.[cmd,sh] of asset-producing jobs.
- name: _PublishArgs
value: /p:Publish=true
+ /p:GenerateChecksums=true
/p:DotNetPublishBlobFeedKey=$(dotnetfeed-storage-access-key-1)
/p:DotNetPublishBlobFeedUrl=https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json
/p:DotNetPublishToBlobFeed=$(_DotNetPublishToBlobFeed)
@@ -594,7 +595,7 @@ stages:
parameters:
condition: ne(variables['SkipTests'], 'true')
jobName: MacOS_Test
- jobDisplayName: "Test: macOS 10.13"
+ jobDisplayName: "Test: macOS 10.14"
agentOs: macOS
isTestingJob: true
buildArgs: --all --test "/p:RunTemplateTests=false /p:SkipHelixReadyTests=true" $(_InternalRuntimeDownloadArgs)
diff --git a/.vsconfig b/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md
new file mode 100644
index 0000000000..775f221c98
--- /dev/null
+++ b/CODE-OF-CONDUCT.md
@@ -0,0 +1,6 @@
+# Code of Conduct
+
+This project has adopted the code of conduct defined by the Contributor Covenant
+to clarify expected behavior in our community.
+
+For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
diff --git a/Directory.Build.props b/Directory.Build.props
index c40d203441..8be130b168 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -179,7 +179,6 @@
.tar.gz
.zip
- .sha512
diff --git a/Directory.Build.targets b/Directory.Build.targets
index ccef1aa25c..5c0c27b516 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -105,6 +105,14 @@
true
+
+ $(RepoRoot)THIRD-PARTY-NOTICES.TXT
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 4c2e975c42..327c3e7208 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Follow the [Getting Started](https://docs.microsoft.com/aspnet/core/getting-star
Also check out the [.NET Homepage](https://www.microsoft.com/net) for released versions of .NET, getting started guides, and learning resources.
-See the [Issue Management Policies](https://github.com/dotnet/aspnetcore/blob/anurse/issue-policies/docs/IssueManagementPolicies.md) document for more information on how we handle incoming issues.
+See the [Issue Management Policies](https://github.com/dotnet/aspnetcore/blob/master/docs/IssueManagementPolicies.md) document for more information on how we handle incoming issues.
## How to Engage, Contribute, and Give Feedback
@@ -37,4 +37,4 @@ These are some other repos for related projects:
## Code of conduct
-This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
+See [CODE-OF-CONDUCT](./CODE-OF-CONDUCT.md)
diff --git a/eng/AfterSigning.targets b/eng/AfterSigning.targets
new file mode 100644
index 0000000000..4bbb0dcf03
--- /dev/null
+++ b/eng/AfterSigning.targets
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1))))
+
+ $(ArtifactsDir)\installers\
+
+
+
+
+
+
+
+
+
+
+
+
+ %(FullPath).sha512
+
+
+
+
+
+
+
+
diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props
index 057d2662c0..44b6452388 100644
--- a/eng/Baseline.Designer.props
+++ b/eng/Baseline.Designer.props
@@ -6,12 +6,12 @@
- 3.0.2
+ 3.0.3
- 3.0.2
+ 3.0.3
diff --git a/eng/Baseline.xml b/eng/Baseline.xml
index b4192011c4..df8a80748c 100644
--- a/eng/Baseline.xml
+++ b/eng/Baseline.xml
@@ -5,8 +5,8 @@ Update this list when preparing for a new patch.
-->
-
-
+
+
diff --git a/eng/Build.props b/eng/Build.props
index 6f1b3f1908..ef89408b47 100644
--- a/eng/Build.props
+++ b/eng/Build.props
@@ -152,6 +152,7 @@
$(RepoRoot)src\SiteExtensions\LoggingAggregate\test\**\*.csproj;
$(RepoRoot)src\Shared\**\*.*proj;
$(RepoRoot)src\Tools\**\*.*proj;
+ $(RepoRoot)src\Logging.AzureAppServices\**\src\*.csproj;
$(RepoRoot)src\Middleware\**\*.csproj;
$(RepoRoot)src\Razor\**\*.*proj;
$(RepoRoot)src\Mvc\**\*.*proj;
@@ -191,6 +192,7 @@
$(RepoRoot)src\Security\**\src\*.csproj;
$(RepoRoot)src\SiteExtensions\**\src\*.csproj;
$(RepoRoot)src\Tools\**\src\*.csproj;
+ $(RepoRoot)src\Logging.AzureAppServices\**\src\*.csproj;
$(RepoRoot)src\Middleware\**\src\*.csproj;
$(RepoRoot)src\Razor\**\src\*.csproj;
$(RepoRoot)src\Mvc\**\src\*.csproj;
diff --git a/eng/Dependencies.props b/eng/Dependencies.props
index d02e5158ae..6013eeca3d 100644
--- a/eng/Dependencies.props
+++ b/eng/Dependencies.props
@@ -29,7 +29,6 @@ and are generated based on the last package release.
-
@@ -120,8 +119,6 @@ and are generated based on the last package release.
-
-
diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props
index dda9b06ce8..a7d42c3782 100644
--- a/eng/ProjectReferences.props
+++ b/eng/ProjectReferences.props
@@ -32,6 +32,7 @@
+
diff --git a/eng/Publishing.props b/eng/Publishing.props
index ab7456c178..2c13cb29bf 100644
--- a/eng/Publishing.props
+++ b/eng/Publishing.props
@@ -1,7 +1,7 @@
-
+
- $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1))))
+ $(ArtifactsDir.Substring(0, $([MSBuild]::Subtract($(ArtifactsDir.Length), 1))))
$(PublishDependsOnTargets);_PublishInstallersAndChecksums
@@ -50,12 +50,10 @@
-
true
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index e63ae9d242..5ab63a957a 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -13,320 +13,312 @@
https://github.com/dotnet/blazor
dd7fb4d3931d556458f62642c2edfc59f6295bfb
-
+
https://github.com/dotnet/aspnetcore-tooling
- 46133a42cbf5af76e4f98a0b5221d55d342067a0
+ 4ec71cb57e45db101bbd4ffcf64dafa1711de0af
-
+
https://github.com/dotnet/aspnetcore-tooling
- 46133a42cbf5af76e4f98a0b5221d55d342067a0
+ 4ec71cb57e45db101bbd4ffcf64dafa1711de0af
-
+
https://github.com/dotnet/aspnetcore-tooling
- 46133a42cbf5af76e4f98a0b5221d55d342067a0
+ 4ec71cb57e45db101bbd4ffcf64dafa1711de0af
-
+
https://github.com/dotnet/aspnetcore-tooling
- 46133a42cbf5af76e4f98a0b5221d55d342067a0
+ 4ec71cb57e45db101bbd4ffcf64dafa1711de0af
-
+
https://github.com/dotnet/efcore
- 7a6aa0a4f513c28b5a0501a2db8880885def2236
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 7a6aa0a4f513c28b5a0501a2db8880885def2236
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 7a6aa0a4f513c28b5a0501a2db8880885def2236
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 7a6aa0a4f513c28b5a0501a2db8880885def2236
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 7a6aa0a4f513c28b5a0501a2db8880885def2236
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 7a6aa0a4f513c28b5a0501a2db8880885def2236
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/efcore
- 7a6aa0a4f513c28b5a0501a2db8880885def2236
+ b0636ed8050797d0a9c16da8b98c2eea7d7e1f16
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
-
- https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
-
-
- https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
-
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/runtime
- 2b487f31064fe07d3b3398a7432edd1fa5777796
+ e1fa5d7648d46f067e265211fc2c695d409fe788
-
+
https://github.com/dotnet/extensions
- ec073cff7d938e94cea1b239e9d8934627239a8a
+ 03c40031d618f923aa88da125cb078aabde9ebb1
https://github.com/dotnet/arcade
@@ -340,9 +332,9 @@
https://github.com/dotnet/arcade
09bb9d929120b402348c9a0e9c8c951e824059aa
-
+
https://github.com/dotnet/roslyn
- c9f2423cb5a2ab1ee8de0ef10e536d7672b1a2ea
+ 8167e4880190407325d6cf7282f6bb62267abc56
diff --git a/eng/Versions.props b/eng/Versions.props
index f17da370ff..737b0be4fb 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -9,7 +9,7 @@
5
0
0
- 3
+ 4
@@ -64,92 +64,90 @@
5.0.0-beta.20180.5
- 3.6.0-3.20177.6
+ 3.6.0-3.20201.6
- 5.0.0-preview.3-runtime.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
+ 5.0.0-preview.4-runtime.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
- 5.0.0-preview.3.20202.4
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
+ 5.0.0-preview.4.20201.1
- 5.0.0-preview.3.20202.4
+ 5.0.0-preview.4.20201.1
3.2.0-preview1.20067.1
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
- 5.0.0-preview.3.20202.8
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
+ 5.0.0-preview.4.20201.2
- 5.0.0-preview.3.20181.2
- 5.0.0-preview.3.20181.2
- 5.0.0-preview.3.20181.2
- 5.0.0-preview.3.20181.2
- 5.0.0-preview.3.20181.2
- 5.0.0-preview.3.20181.2
- 5.0.0-preview.3.20181.2
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
+ 5.0.0-preview.4.20203.1
- 5.0.0-preview.3.20202.16
- 5.0.0-preview.3.20202.16
- 5.0.0-preview.3.20202.16
- 5.0.0-preview.3.20202.16
+ 5.0.0-preview.4.20201.4
+ 5.0.0-preview.4.20201.4
+ 5.0.0-preview.4.20201.4
+ 5.0.0-preview.4.20201.4
diff --git a/eng/tools/.vsconfig b/eng/tools/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/eng/tools/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Analyzers/.vsconfig b/src/Analyzers/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Analyzers/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Antiforgery/.vsconfig b/src/Antiforgery/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Antiforgery/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Azure/.vsconfig b/src/Azure/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Azure/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Components/.vsconfig b/src/Components/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Components/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj b/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj
index 867dec8215..d53f7d01fe 100644
--- a/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj
+++ b/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj
@@ -10,6 +10,12 @@
+
+
+
diff --git a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj
index 57b1e5b4cd..7bc610f168 100644
--- a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj
+++ b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj
@@ -24,6 +24,7 @@
+
diff --git a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec
index a3e099dee6..459fed97d0 100644
--- a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec
+++ b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec
@@ -8,7 +8,7 @@
$CommonFileElements$
-
+
diff --git a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj
index 366a1bce5e..dc403233a8 100644
--- a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj
+++ b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj
@@ -34,6 +34,7 @@
+
diff --git a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.nuspec b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.nuspec
index 2f0f6b8479..77df4b791b 100644
--- a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.nuspec
+++ b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.nuspec
@@ -7,6 +7,6 @@
$CommonFileElements$
-
+
diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
index fefadced49..2f1841b906 100644
--- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
+++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
@@ -58,6 +58,7 @@
+
diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec b/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec
index 7017ce828e..e2190e2be5 100644
--- a/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec
+++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.multitarget.nuspec
@@ -21,6 +21,6 @@
-
+
diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec b/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec
index 9ea33e53dc..69d234bc08 100644
--- a/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec
+++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.netcoreapp.nuspec
@@ -15,6 +15,6 @@
-
+
diff --git a/src/Components/Directory.Build.props b/src/Components/Directory.Build.props
index b614476c4c..65e48e29f5 100644
--- a/src/Components/Directory.Build.props
+++ b/src/Components/Directory.Build.props
@@ -20,6 +20,8 @@
$(MSBuildThisFileDirectory)Blazor\Build\src\bin\$(Configuration)\$(DefaultNetCoreTargetFramework)\
+
+ $(MSBuildThisFileDirectory)THIRD-PARTY-NOTICES.txt
diff --git a/src/Components/Directory.Build.targets b/src/Components/Directory.Build.targets
index b6b1f773d9..d6569c4088 100644
--- a/src/Components/Directory.Build.targets
+++ b/src/Components/Directory.Build.targets
@@ -24,8 +24,6 @@
-
-
+
-
diff --git a/src/DataProtection/.vsconfig b/src/DataProtection/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/DataProtection/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs b/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs
index 5caee24b12..951b003063 100644
--- a/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs
+++ b/src/DataProtection/Extensions/test/DataProtectionProviderTests.cs
@@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.DataProtection
[ConditionalFact]
[X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)]
- [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")]
+ [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
public void System_UsesProvidedDirectoryAndCertificate()
{
var filePath = Path.Combine(GetTestFilesPath(), "TestCert.pfx");
@@ -165,7 +165,6 @@ namespace Microsoft.AspNetCore.DataProtection
[ConditionalFact]
[X509StoreIsAvailable(StoreName.My, StoreLocation.CurrentUser)]
- [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/6720")]
public void System_UsesProvidedCertificateNotFromStore()
{
using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
diff --git a/src/DefaultBuilder/.vsconfig b/src/DefaultBuilder/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/DefaultBuilder/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Features/JsonPatch/.vsconfig b/src/Features/JsonPatch/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Features/JsonPatch/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj b/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj
index e7b3fd2014..7397e945bf 100644
--- a/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj
+++ b/src/Framework/src/Microsoft.AspNetCore.App.Runtime.csproj
@@ -67,7 +67,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant
$(IntermediateOutputPath)ignoreme.dev.runtimeconfig.json
- $(IntermediateOutputPath).version
+ $(IntermediateOutputPath)$(SharedFxName).versions.txt
none
@@ -156,12 +156,6 @@ This package is an internal implementation of the .NET Core SDK and is not meant
$(InstallersOutputPath)$(RedistArchiveOutputFileName)
-
-
- $(RedistArchiveOutputPath)$(ChecksumExtension)
-
-
-
@@ -501,4 +495,13 @@ This package is an internal implementation of the .NET Core SDK and is not meant
+
+
+
+
+
+
+
diff --git a/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj b/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj
index 9abf901de4..8d9b26e7d0 100644
--- a/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj
+++ b/src/Framework/test/Microsoft.AspNetCore.App.UnitTests.csproj
@@ -36,6 +36,10 @@
<_Parameter1>TargetingPackLayoutRoot
<_Parameter2>$(TargetingPackLayoutRoot)
+
+ <_Parameter1>IsTargetingPackBuilding
+ <_Parameter2>$(IsTargetingPackBuilding)
+
<_Parameter1>VerifyAncmBinary
<_Parameter2>$(VerifyAncmBinary)
diff --git a/src/Framework/test/SharedFxTests.cs b/src/Framework/test/SharedFxTests.cs
index 2fc09b96b0..558fc34439 100644
--- a/src/Framework/test/SharedFxTests.cs
+++ b/src/Framework/test/SharedFxTests.cs
@@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore
[Fact]
public void ItContainsVersionFile()
{
- var versionFile = Path.Combine(_sharedFxRoot, ".version");
+ var versionFile = Path.Combine(_sharedFxRoot, "Microsoft.AspNetCore.App.versions.txt");
AssertEx.FileExists(versionFile);
var lines = File.ReadAllLines(versionFile);
Assert.Equal(2, lines.Length);
diff --git a/src/Framework/test/TargetingPackTests.cs b/src/Framework/test/TargetingPackTests.cs
index 82b43cb831..54da276513 100644
--- a/src/Framework/test/TargetingPackTests.cs
+++ b/src/Framework/test/TargetingPackTests.cs
@@ -20,17 +20,24 @@ namespace Microsoft.AspNetCore
private readonly string _expectedRid;
private readonly string _targetingPackRoot;
private readonly ITestOutputHelper _output;
+ private readonly bool _isTargetingPackBuilding;
public TargetingPackTests(ITestOutputHelper output)
{
_output = output;
_expectedRid = TestData.GetSharedFxRuntimeIdentifier();
_targetingPackRoot = Path.Combine(TestData.GetTestDataValue("TargetingPackLayoutRoot"), "packs", "Microsoft.AspNetCore.App.Ref", TestData.GetTestDataValue("TargetingPackVersion"));
+ _isTargetingPackBuilding = bool.Parse(TestData.GetTestDataValue("IsTargetingPackBuilding"));
}
[Fact]
public void AssembliesAreReferenceAssemblies()
{
+ if (!_isTargetingPackBuilding)
+ {
+ return;
+ }
+
IEnumerable dlls = Directory.GetFiles(_targetingPackRoot, "*.dll", SearchOption.AllDirectories);
Assert.NotEmpty(dlls);
@@ -58,6 +65,11 @@ namespace Microsoft.AspNetCore
[Fact]
public void PlatformManifestListsAllFiles()
{
+ if (!_isTargetingPackBuilding)
+ {
+ return;
+ }
+
var platformManifestPath = Path.Combine(_targetingPackRoot, "data", "PlatformManifest.txt");
var expectedAssemblies = TestData.GetSharedFxDependencies()
.Split(';', StringSplitOptions.RemoveEmptyEntries)
diff --git a/src/Grpc/.vsconfig b/src/Grpc/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Grpc/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs b/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs
index d1d6374cba..f2dcc4f5b7 100644
--- a/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs
+++ b/src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs
@@ -125,7 +125,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
description: "A timeout occurred while running check.",
duration: duration,
exception: ex,
- data: null);
+ data: null,
+ tags: registration.Tags);
Log.HealthCheckError(_logger, registration, ex, duration);
}
@@ -139,7 +140,8 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
description: ex.Message,
duration: duration,
exception: ex,
- data: null);
+ data: null,
+ tags: registration.Tags);
Log.HealthCheckError(_logger, registration, ex, duration);
}
diff --git a/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs b/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs
index 50cf7ebeae..1cb5b7420b 100644
--- a/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs
+++ b/src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs
@@ -113,6 +113,47 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks
});
}
+ [Fact]
+ public async Task CheckAsync_TagsArePresentInHealthReportEntryIfExceptionOccurs()
+ {
+ const string ExceptionMessage = "exception-message";
+ const string OperationCancelledMessage = "operation-cancelled-message";
+ var exceptionTags = new[] { "unhealthy-check-tag" };
+ var operationExceptionTags = new[] { "degraded-check-tag" };
+
+ // Arrange
+ var service = CreateHealthChecksService(b =>
+ {
+ b.AddAsyncCheck("ExceptionCheck", _ => throw new Exception(ExceptionMessage), exceptionTags);
+ b.AddAsyncCheck("OperationExceptionCheck", _ => throw new OperationCanceledException(OperationCancelledMessage), operationExceptionTags);
+ });
+
+ // Act
+ var results = await service.CheckHealthAsync();
+
+ // Assert
+ Assert.Collection(
+ results.Entries.OrderBy(kvp => kvp.Key),
+ actual =>
+ {
+ Assert.Equal("ExceptionCheck", actual.Key);
+ Assert.Equal(ExceptionMessage, actual.Value.Description);
+ Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
+ Assert.Equal(ExceptionMessage, actual.Value.Exception.Message);
+ Assert.Empty(actual.Value.Data);
+ Assert.Equal(actual.Value.Tags, exceptionTags);
+ },
+ actual =>
+ {
+ Assert.Equal("OperationExceptionCheck", actual.Key);
+ Assert.Equal("A timeout occurred while running check.", actual.Value.Description);
+ Assert.Equal(HealthStatus.Unhealthy, actual.Value.Status);
+ Assert.Equal(OperationCancelledMessage, actual.Value.Exception.Message);
+ Assert.Empty(actual.Value.Data);
+ Assert.Equal(actual.Value.Tags, operationExceptionTags);
+ });
+ }
+
[Fact]
public async Task CheckAsync_RunsFilteredChecksAndAggregatesResultsAsync()
{
diff --git a/src/Hosting/.vsconfig b/src/Hosting/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Hosting/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj
index ba625f4332..939dfcf376 100644
--- a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj
+++ b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj
@@ -23,7 +23,6 @@
-
diff --git a/src/Hosting/TestHost/src/HttpContextBuilder.cs b/src/Hosting/TestHost/src/HttpContextBuilder.cs
index f425a55b2d..736b0458a6 100644
--- a/src/Hosting/TestHost/src/HttpContextBuilder.cs
+++ b/src/Hosting/TestHost/src/HttpContextBuilder.cs
@@ -115,10 +115,25 @@ namespace Microsoft.AspNetCore.TestHost
// This could throw an error if there was a pending server read. Needs to
// happen before completing the response so the response returns the error.
var requestBodyInProgress = RequestBodyReadInProgress();
+ if (requestBodyInProgress)
+ {
+ // If request is still in progress then abort it.
+ CancelRequestBody();
+ }
// Matches Kestrel server: response is completed before request is drained
await CompleteResponseAsync();
- await CompleteRequestAsync(requestBodyInProgress);
+
+ if (!requestBodyInProgress)
+ {
+ // Writer was already completed in send request callback.
+ await _requestPipe.Reader.CompleteAsync();
+
+ // Don't wait for request to drain. It could block indefinitely. In a real server
+ // we would wait for a timeout and then kill the socket.
+ // Potential future improvement: add logging that the request timed out
+ }
+
_application.DisposeContext(_testContext, exception: null);
}
catch (Exception ex)
@@ -165,24 +180,6 @@ namespace Microsoft.AspNetCore.TestHost
CancelRequestBody();
}
- private async Task CompleteRequestAsync(bool requestBodyInProgress)
- {
- if (requestBodyInProgress)
- {
- // If request is still in progress then abort it.
- CancelRequestBody();
- }
- else
- {
- // Writer was already completed in send request callback.
- await _requestPipe.Reader.CompleteAsync();
- }
-
- // Don't wait for request to drain. It could block indefinitely. In a real server
- // we would wait for a timeout and then kill the socket.
- // Potential future improvement: add logging that the request timed out
- }
-
private bool RequestBodyReadInProgress()
{
try
diff --git a/src/Http/.vsconfig b/src/Http/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Http/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Http/Http.Extensions/ref/Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs b/src/Http/Http.Extensions/ref/Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs
index 14f3400e95..7fa291f591 100644
--- a/src/Http/Http.Extensions/ref/Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs
+++ b/src/Http/Http.Extensions/ref/Microsoft.AspNetCore.Http.Extensions.netcoreapp.cs
@@ -43,6 +43,7 @@ namespace Microsoft.AspNetCore.Http.Extensions
public partial class QueryBuilder : System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable
{
public QueryBuilder() { }
+ public QueryBuilder(System.Collections.Generic.IEnumerable> parameters) { }
public QueryBuilder(System.Collections.Generic.IEnumerable> parameters) { }
public void Add(string key, System.Collections.Generic.IEnumerable values) { }
public void Add(string key, string value) { }
diff --git a/src/Http/Http.Extensions/src/QueryBuilder.cs b/src/Http/Http.Extensions/src/QueryBuilder.cs
index e9feb391b1..ab2d95b79d 100644
--- a/src/Http/Http.Extensions/src/QueryBuilder.cs
+++ b/src/Http/Http.Extensions/src/QueryBuilder.cs
@@ -3,8 +3,10 @@
using System.Collections;
using System.Collections.Generic;
+using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
+using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Http.Extensions
{
@@ -23,6 +25,12 @@ namespace Microsoft.AspNetCore.Http.Extensions
_params = new List>(parameters);
}
+ public QueryBuilder(IEnumerable> parameters)
+ : this(parameters.SelectMany(kvp => kvp.Value, (kvp, v) => KeyValuePair.Create(kvp.Key, v)))
+ {
+
+ }
+
public void Add(string key, IEnumerable values)
{
foreach (var value in values)
@@ -78,4 +86,4 @@ namespace Microsoft.AspNetCore.Http.Extensions
return _params.GetEnumerator();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Http/Http.Extensions/test/QueryBuilderTests.cs b/src/Http/Http.Extensions/test/QueryBuilderTests.cs
index 7d15dd87bf..c2517c45d4 100644
--- a/src/Http/Http.Extensions/test/QueryBuilderTests.cs
+++ b/src/Http/Http.Extensions/test/QueryBuilderTests.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.AspNetCore.Http.Extensions
@@ -70,6 +71,18 @@ namespace Microsoft.AspNetCore.Http.Extensions
Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString());
}
+ [Fact]
+ public void AddMultipleValuesViaConstructor_WithStringValues()
+ {
+ var builder = new QueryBuilder(new[]
+ {
+ new KeyValuePair("key1", new StringValues(new [] { "value1", string.Empty, "value3" })),
+ new KeyValuePair("key2", string.Empty),
+ new KeyValuePair("key3", StringValues.Empty)
+ });
+ Assert.Equal("?key1=value1&key1=&key1=value3&key2=", builder.ToString());
+ }
+
[Fact]
public void AddMultipleValuesViaInitializer_AddedInOrder()
{
@@ -95,4 +108,4 @@ namespace Microsoft.AspNetCore.Http.Extensions
Assert.Equal("?key1=value1&key2=value2&key3=value3", builder1.ToString());
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs
index 886f5c0052..3aa6ce55dc 100644
--- a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs
+++ b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp.cs
@@ -216,6 +216,8 @@ namespace Microsoft.AspNetCore.WebUtilities
public static partial class QueryHelpers
{
public static string AddQueryString(string uri, System.Collections.Generic.IDictionary queryString) { throw null; }
+ public static string AddQueryString(string uri, System.Collections.Generic.IEnumerable> queryString) { throw null; }
+ public static string AddQueryString(string uri, System.Collections.Generic.IEnumerable> queryString) { throw null; }
public static string AddQueryString(string uri, string name, string value) { throw null; }
public static System.Collections.Generic.Dictionary ParseNullableQuery(string queryString) { throw null; }
public static System.Collections.Generic.Dictionary ParseQuery(string queryString) { throw null; }
diff --git a/src/Http/WebUtilities/src/QueryHelpers.cs b/src/Http/WebUtilities/src/QueryHelpers.cs
index ca71329f03..9c28f4ab2b 100644
--- a/src/Http/WebUtilities/src/QueryHelpers.cs
+++ b/src/Http/WebUtilities/src/QueryHelpers.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.Extensions.Primitives;
@@ -46,10 +47,10 @@ namespace Microsoft.AspNetCore.WebUtilities
}
///
- /// Append the given query keys and values to the uri.
+ /// Append the given query keys and values to the URI.
///
- /// The base uri.
- /// A collection of name value query pairs to append.
+ /// The base URI.
+ /// A dictionary of query keys and values to append.
/// The combined result.
/// is null.
/// is null.
@@ -68,7 +69,38 @@ namespace Microsoft.AspNetCore.WebUtilities
return AddQueryString(uri, (IEnumerable>)queryString);
}
- private static string AddQueryString(
+ ///
+ /// Append the given query keys and values to the URI.
+ ///
+ /// The base URI.
+ /// A collection of query names and values to append.
+ /// The combined result.
+ /// is null.
+ /// is null.
+ public static string AddQueryString(string uri, IEnumerable> queryString)
+ {
+ if (uri == null)
+ {
+ throw new ArgumentNullException(nameof(uri));
+ }
+
+ if (queryString == null)
+ {
+ throw new ArgumentNullException(nameof(queryString));
+ }
+
+ return AddQueryString(uri, queryString.SelectMany(kvp => kvp.Value, (kvp, v) => KeyValuePair.Create(kvp.Key, v)));
+ }
+
+ ///
+ /// Append the given query keys and values to the URI.
+ ///
+ /// The base URI.
+ /// A collection of name value query pairs to append.
+ /// The combined result.
+ /// is null.
+ /// is null.
+ public static string AddQueryString(
string uri,
IEnumerable> queryString)
{
diff --git a/src/Http/WebUtilities/test/QueryHelpersTests.cs b/src/Http/WebUtilities/test/QueryHelpersTests.cs
index a64bcbf03b..204813e5b6 100644
--- a/src/Http/WebUtilities/test/QueryHelpersTests.cs
+++ b/src/Http/WebUtilities/test/QueryHelpersTests.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.AspNetCore.WebUtilities
@@ -119,5 +120,37 @@ namespace Microsoft.AspNetCore.WebUtilities
var result = QueryHelpers.AddQueryString(uri, queryStrings);
Assert.Equal(expectedUri, result);
}
+
+ [Theory]
+ [InlineData("http://contoso.com/", "http://contoso.com/?param1=value1¶m1=¶m1=value3¶m2=")]
+ [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=")]
+ [InlineData("http://contoso.com/someaction?param2=1", "http://contoso.com/someaction?param2=1¶m1=value1¶m1=¶m1=value3¶m2=")]
+ [InlineData("http://contoso.com/some#action", "http://contoso.com/some?param1=value1¶m1=¶m1=value3¶m2=#action")]
+ [InlineData("http://contoso.com/some?param2=1#action", "http://contoso.com/some?param2=1¶m1=value1¶m1=¶m1=value3¶m2=#action")]
+ [InlineData("http://contoso.com/#action", "http://contoso.com/?param1=value1¶m1=¶m1=value3¶m2=#action")]
+ [InlineData(
+ "http://contoso.com/someaction?q=test#anchor?value",
+ "http://contoso.com/someaction?q=test¶m1=value1¶m1=¶m1=value3¶m2=#anchor?value")]
+ [InlineData(
+ "http://contoso.com/someaction#anchor?stuff",
+ "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=#anchor?stuff")]
+ [InlineData(
+ "http://contoso.com/someaction?name?something",
+ "http://contoso.com/someaction?name?something¶m1=value1¶m1=¶m1=value3¶m2=")]
+ [InlineData(
+ "http://contoso.com/someaction#name#something",
+ "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=#name#something")]
+ public void AddQueryStringWithEnumerableOfKeysAndStringValues(string uri, string expectedUri)
+ {
+ var queryStrings = new Dictionary()
+ {
+ { "param1", new StringValues(new [] { "value1", string.Empty, "value3" }) },
+ { "param2", string.Empty },
+ { "param3", StringValues.Empty }
+ };
+
+ var result = QueryHelpers.AddQueryString(uri, queryStrings);
+ Assert.Equal(expectedUri, result);
+ }
}
}
diff --git a/src/Identity/.vsconfig b/src/Identity/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Identity/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj b/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj
index 6da54c6869..23f9d4bad5 100644
--- a/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj
+++ b/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj
@@ -23,6 +23,7 @@
Bootstrap4
+ $(MSBuildThisFileDirectory)THIRD-PARTY-NOTICES.TXT
@@ -32,7 +33,6 @@
-
diff --git a/src/Identity/test/Identity.Test/IdentityUIScriptsTest.cs b/src/Identity/test/Identity.Test/IdentityUIScriptsTest.cs
index 005895f9c0..6bfa6efa5e 100644
--- a/src/Identity/test/Identity.Test/IdentityUIScriptsTest.cs
+++ b/src/Identity/test/Identity.Test/IdentityUIScriptsTest.cs
@@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.Identity.Test
[Theory]
[MemberData(nameof(ScriptWithIntegrityData))]
+ [QuarantinedTest]
public async Task IdentityUI_ScriptTags_SubresourceIntegrityCheck(ScriptTag scriptTag)
{
var integrity = await GetShaIntegrity(scriptTag);
diff --git a/src/Installers/Windows/.vsconfig b/src/Installers/Windows/.vsconfig
new file mode 100644
index 0000000000..8f411e8f86
--- /dev/null
+++ b/src/Installers/Windows/.vsconfig
@@ -0,0 +1,16 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Component.VC.ATL",
+ "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
+ "Microsoft.VisualStudio.Component.Windows10SDK.17134",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NativeDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/.vsconfig b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/.vsconfig
new file mode 100644
index 0000000000..8f411e8f86
--- /dev/null
+++ b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/.vsconfig
@@ -0,0 +1,16 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Component.VC.ATL",
+ "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
+ "Microsoft.VisualStudio.Component.Windows10SDK.17134",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NativeDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/.vsconfig b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/.vsconfig
new file mode 100644
index 0000000000..8f411e8f86
--- /dev/null
+++ b/src/Installers/Windows/AspNetCoreModule-Setup/IIS-Setup/IIS-Common/.vsconfig
@@ -0,0 +1,16 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Component.VC.ATL",
+ "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
+ "Microsoft.VisualStudio.Component.Windows10SDK.17134",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NativeDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Installers/Windows/Wix.targets b/src/Installers/Windows/Wix.targets
index 2e4b8dca03..de6636d51d 100644
--- a/src/Installers/Windows/Wix.targets
+++ b/src/Installers/Windows/Wix.targets
@@ -64,7 +64,7 @@
+ BeforeTargets="Build">
<_cabs Include="$(TargetDir)**/*.cab" />
@@ -72,10 +72,4 @@
-
-
- $(InstallersOutputPath)$(PackageFileName)$(ChecksumExtension)
-
-
-
diff --git a/src/Logging.AzureAppServices/Directory.Build.props b/src/Logging.AzureAppServices/Directory.Build.props
new file mode 100644
index 0000000000..68f87d4f24
--- /dev/null
+++ b/src/Logging.AzureAppServices/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+
+ true
+
+
diff --git a/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs b/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs
new file mode 100644
index 0000000000..9b680e9138
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/AzureAppServicesLoggerFactoryExtensions.cs
@@ -0,0 +1,98 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging.AzureAppServices;
+using Microsoft.Extensions.Logging.Configuration;
+using Microsoft.Extensions.Options;
+using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor;
+
+namespace Microsoft.Extensions.Logging
+{
+ ///
+ /// Extension methods for adding Azure diagnostics logger.
+ ///
+ public static class AzureAppServicesLoggerFactoryExtensions
+ {
+ ///
+ /// Adds an Azure Web Apps diagnostics logger.
+ ///
+ /// The extension method argument
+ public static ILoggingBuilder AddAzureWebAppDiagnostics(this ILoggingBuilder builder)
+ {
+ var context = WebAppContext.Default;
+
+ // Only add the provider if we're in Azure WebApp. That cannot change once the apps started
+ return AddAzureWebAppDiagnostics(builder, context);
+ }
+
+ internal static ILoggingBuilder AddAzureWebAppDiagnostics(this ILoggingBuilder builder, IWebAppContext context)
+ {
+ if (!context.IsRunningInAzureWebApp)
+ {
+ return builder;
+ }
+
+ builder.AddConfiguration();
+
+ var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(context);
+ var services = builder.Services;
+
+ var addedFileLogger = TryAddEnumerable(services, Singleton());
+ var addedBlobLogger = TryAddEnumerable(services, Singleton());
+
+ if (addedFileLogger || addedBlobLogger)
+ {
+ services.AddSingleton(context);
+ services.AddSingleton>(
+ new ConfigurationChangeTokenSource(config));
+ }
+
+ if (addedFileLogger)
+ {
+ services.AddSingleton>(CreateFileFilterConfigureOptions(config));
+ services.AddSingleton>(new FileLoggerConfigureOptions(config, context));
+ services.AddSingleton>(
+ new ConfigurationChangeTokenSource(config));
+ LoggerProviderOptions.RegisterProviderOptions(builder.Services);
+ }
+
+ if (addedBlobLogger)
+ {
+ services.AddSingleton>(CreateBlobFilterConfigureOptions(config));
+ services.AddSingleton>(new BlobLoggerConfigureOptions(config, context));
+ services.AddSingleton>(
+ new ConfigurationChangeTokenSource(config));
+ LoggerProviderOptions.RegisterProviderOptions(builder.Services);
+ }
+
+ return builder;
+ }
+
+ private static bool TryAddEnumerable(IServiceCollection collection, ServiceDescriptor descriptor)
+ {
+ var beforeCount = collection.Count;
+ collection.TryAddEnumerable(descriptor);
+ return beforeCount != collection.Count;
+ }
+
+ private static ConfigurationBasedLevelSwitcher CreateBlobFilterConfigureOptions(IConfiguration config)
+ {
+ return new ConfigurationBasedLevelSwitcher(
+ configuration: config,
+ provider: typeof(BlobLoggerProvider),
+ levelKey: "AzureBlobTraceLevel");
+ }
+
+ private static ConfigurationBasedLevelSwitcher CreateFileFilterConfigureOptions(IConfiguration config)
+ {
+ return new ConfigurationBasedLevelSwitcher(
+ configuration: config,
+ provider: typeof(FileLoggerProvider),
+ levelKey: "AzureDriveTraceLevel");
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs b/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs
new file mode 100644
index 0000000000..1e1285b358
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/AzureBlobLoggerOptions.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Options for Azure diagnostics blob logging.
+ ///
+ public class AzureBlobLoggerOptions: BatchingLoggerOptions
+ {
+ private string _blobName = "applicationLog.txt";
+
+ ///
+ /// Gets or sets the last section of log blob name.
+ /// Defaults to "applicationLog.txt".
+ ///
+ public string BlobName
+ {
+ get { return _blobName; }
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException(nameof(value), $"{nameof(BlobName)} must be non-empty string.");
+ }
+ _blobName = value;
+ }
+ }
+
+ internal string ContainerUrl { get; set; }
+
+ internal string ApplicationName { get; set; }
+
+ internal string ApplicationInstanceId { get; set; }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs b/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs
new file mode 100644
index 0000000000..af8b5a112e
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/AzureFileLoggerOptions.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Options for Azure diagnostics file logging.
+ ///
+ public class AzureFileLoggerOptions: BatchingLoggerOptions
+ {
+ private int? _fileSizeLimit = 10 * 1024 * 1024;
+ private int? _retainedFileCountLimit = 2;
+ private string _fileName = "diagnostics-";
+
+ ///
+ /// Gets or sets a strictly positive value representing the maximum log size in bytes or null for no limit.
+ /// Once the log is full, no more messages will be appended.
+ /// Defaults to 10MB.
+ ///
+ public int? FileSizeLimit
+ {
+ get { return _fileSizeLimit; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FileSizeLimit)} must be positive.");
+ }
+ _fileSizeLimit = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a strictly positive value representing the maximum retained file count or null for no limit.
+ /// Defaults to 2.
+ ///
+ public int? RetainedFileCountLimit
+ {
+ get { return _retainedFileCountLimit; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(RetainedFileCountLimit)} must be positive.");
+ }
+ _retainedFileCountLimit = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a string representing the prefix of the file name used to store the logging information.
+ /// The current date, in the format YYYYMMDD will be added after the given value.
+ /// Defaults to diagnostics-.
+ ///
+ public string FileName
+ {
+ get { return _fileName; }
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException(nameof(value));
+ }
+ _fileName = value;
+ }
+ }
+
+ internal string LogDirectory { get; set; }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.cs
new file mode 100644
index 0000000000..8dc8727b3a
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BatchLoggerConfigureOptions.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.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class BatchLoggerConfigureOptions : IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+ private readonly string _isEnabledKey;
+
+ public BatchLoggerConfigureOptions(IConfiguration configuration, string isEnabledKey)
+ {
+ _configuration = configuration;
+ _isEnabledKey = isEnabledKey;
+ }
+
+ public void Configure(BatchingLoggerOptions options)
+ {
+ options.IsEnabled = TextToBoolean(_configuration.GetSection(_isEnabledKey)?.Value);
+ }
+
+ private static bool TextToBoolean(string text)
+ {
+ if (string.IsNullOrEmpty(text) ||
+ !bool.TryParse(text, out var result))
+ {
+ result = false;
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BatchingLogger.cs b/src/Logging.AzureAppServices/src/BatchingLogger.cs
new file mode 100644
index 0000000000..bd192169f3
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BatchingLogger.cs
@@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Text;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class BatchingLogger : ILogger
+ {
+ private readonly BatchingLoggerProvider _provider;
+ private readonly string _category;
+
+ public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName)
+ {
+ _provider = loggerProvider;
+ _category = categoryName;
+ }
+
+ public IDisposable BeginScope(TState state)
+ {
+ return null;
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return _provider.IsEnabled;
+ }
+
+ public void Log(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel))
+ {
+ return;
+ }
+
+ var builder = new StringBuilder();
+ builder.Append(timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"));
+ builder.Append(" [");
+ builder.Append(logLevel.ToString());
+ builder.Append("] ");
+ builder.Append(_category);
+
+ var scopeProvider = _provider.ScopeProvider;
+ if (scopeProvider != null)
+ {
+ scopeProvider.ForEachScope((scope, stringBuilder) =>
+ {
+ stringBuilder.Append(" => ").Append(scope);
+ }, builder);
+
+ builder.AppendLine(":");
+ }
+ else
+ {
+ builder.Append(": ");
+ }
+
+ builder.AppendLine(formatter(state, exception));
+
+ if (exception != null)
+ {
+ builder.AppendLine(exception.ToString());
+ }
+
+ _provider.AddMessage(timestamp, builder.ToString());
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ Log(DateTimeOffset.Now, logLevel, eventId, state, exception, formatter);
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs b/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs
new file mode 100644
index 0000000000..9fbd964800
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BatchingLoggerOptions.cs
@@ -0,0 +1,80 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Options for a logger which batches up log messages.
+ ///
+ public class BatchingLoggerOptions
+ {
+ private int? _batchSize;
+ private int? _backgroundQueueSize = 1000;
+ private TimeSpan _flushPeriod = TimeSpan.FromSeconds(1);
+
+ ///
+ /// Gets or sets the period after which logs will be flushed to the store.
+ ///
+ public TimeSpan FlushPeriod
+ {
+ get { return _flushPeriod; }
+ set
+ {
+ if (value <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FlushPeriod)} must be positive.");
+ }
+ _flushPeriod = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum size of the background log message queue or null for no limit.
+ /// After maximum queue size is reached log event sink would start blocking.
+ /// Defaults to 1000.
+ ///
+ public int? BackgroundQueueSize
+ {
+ get { return _backgroundQueueSize; }
+ set
+ {
+ if (value < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BackgroundQueueSize)} must be non-negative.");
+ }
+ _backgroundQueueSize = value;
+ }
+ }
+
+ ///
+ /// Gets or sets a maximum number of events to include in a single batch or null for no limit.
+ ///
+ /// Defaults to null.
+ public int? BatchSize
+ {
+ get { return _batchSize; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(BatchSize)} must be positive.");
+ }
+ _batchSize = value;
+ }
+ }
+
+ ///
+ /// Gets or sets value indicating if logger accepts and queues writes.
+ ///
+ public bool IsEnabled { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether scopes should be included in the message.
+ /// Defaults to false.
+ ///
+ public bool IncludeScopes { get; set; } = false;
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs b/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs
new file mode 100644
index 0000000000..227a616f3b
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BatchingLoggerProvider.cs
@@ -0,0 +1,208 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// A provider of instances.
+ ///
+ public abstract class BatchingLoggerProvider : ILoggerProvider, ISupportExternalScope
+ {
+ private readonly List _currentBatch = new List();
+ private readonly TimeSpan _interval;
+ private readonly int? _queueSize;
+ private readonly int? _batchSize;
+ private readonly IDisposable _optionsChangeToken;
+
+ private int _messagesDropped;
+
+ private BlockingCollection _messageQueue;
+ private Task _outputTask;
+ private CancellationTokenSource _cancellationTokenSource;
+
+ private bool _includeScopes;
+ private IExternalScopeProvider _scopeProvider;
+
+ internal IExternalScopeProvider ScopeProvider => _includeScopes ? _scopeProvider : null;
+
+ internal BatchingLoggerProvider(IOptionsMonitor options)
+ {
+ // NOTE: Only IsEnabled is monitored
+
+ var loggerOptions = options.CurrentValue;
+ if (loggerOptions.BatchSize <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(loggerOptions.BatchSize), $"{nameof(loggerOptions.BatchSize)} must be a positive number.");
+ }
+ if (loggerOptions.FlushPeriod <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod), $"{nameof(loggerOptions.FlushPeriod)} must be longer than zero.");
+ }
+
+ _interval = loggerOptions.FlushPeriod;
+ _batchSize = loggerOptions.BatchSize;
+ _queueSize = loggerOptions.BackgroundQueueSize;
+
+ _optionsChangeToken = options.OnChange(UpdateOptions);
+ UpdateOptions(options.CurrentValue);
+ }
+
+ ///
+ /// Checks if the queue is enabled.
+ ///
+ public bool IsEnabled { get; private set; }
+
+ private void UpdateOptions(BatchingLoggerOptions options)
+ {
+ var oldIsEnabled = IsEnabled;
+ IsEnabled = options.IsEnabled;
+ _includeScopes = options.IncludeScopes;
+
+ if (oldIsEnabled != IsEnabled)
+ {
+ if (IsEnabled)
+ {
+ Start();
+ }
+ else
+ {
+ Stop();
+ }
+ }
+
+ }
+
+ internal abstract Task WriteMessagesAsync(IEnumerable messages, CancellationToken token);
+
+ private async Task ProcessLogQueue()
+ {
+ while (!_cancellationTokenSource.IsCancellationRequested)
+ {
+ var limit = _batchSize ?? int.MaxValue;
+
+ while (limit > 0 && _messageQueue.TryTake(out var message))
+ {
+ _currentBatch.Add(message);
+ limit--;
+ }
+
+ var messagesDropped = Interlocked.Exchange(ref _messagesDropped, 0);
+ if (messagesDropped != 0)
+ {
+ _currentBatch.Add(new LogMessage(DateTimeOffset.Now, $"{messagesDropped} message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this.{Environment.NewLine}"));
+ }
+
+ if (_currentBatch.Count > 0)
+ {
+ try
+ {
+ await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token);
+ }
+ catch
+ {
+ // ignored
+ }
+
+ _currentBatch.Clear();
+ }
+ else
+ {
+ await IntervalAsync(_interval, _cancellationTokenSource.Token);
+ }
+ }
+ }
+
+ ///
+ /// Wait for the given .
+ ///
+ /// The amount of time to wait.
+ /// A that can be used to cancel the delay.
+ /// A which completes when the has passed or the has been canceled.
+ protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
+ {
+ return Task.Delay(interval, cancellationToken);
+ }
+
+ internal void AddMessage(DateTimeOffset timestamp, string message)
+ {
+ if (!_messageQueue.IsAddingCompleted)
+ {
+ try
+ {
+ if (!_messageQueue.TryAdd(new LogMessage(timestamp, message), millisecondsTimeout: 0, cancellationToken: _cancellationTokenSource.Token))
+ {
+ Interlocked.Increment(ref _messagesDropped);
+ }
+ }
+ catch
+ {
+ //cancellation token canceled or CompleteAdding called
+ }
+ }
+ }
+
+ private void Start()
+ {
+ _messageQueue = _queueSize == null ?
+ new BlockingCollection(new ConcurrentQueue()) :
+ new BlockingCollection(new ConcurrentQueue(), _queueSize.Value);
+
+ _cancellationTokenSource = new CancellationTokenSource();
+ _outputTask = Task.Run(ProcessLogQueue);
+ }
+
+ private void Stop()
+ {
+ _cancellationTokenSource.Cancel();
+ _messageQueue.CompleteAdding();
+
+ try
+ {
+ _outputTask.Wait(_interval);
+ }
+ catch (TaskCanceledException)
+ {
+ }
+ catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException)
+ {
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ _optionsChangeToken?.Dispose();
+ if (IsEnabled)
+ {
+ Stop();
+ }
+ }
+
+ ///
+ /// Creates a with the given .
+ ///
+ /// The name of the category to create this logger with.
+ /// The that was created.
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new BatchingLogger(this, categoryName);
+ }
+
+ ///
+ /// Sets the scope on this provider.
+ ///
+ /// Provides the scope.
+ void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider)
+ {
+ _scopeProvider = scopeProvider;
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs b/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs
new file mode 100644
index 0000000000..e9805128b7
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BlobAppendReferenceWrapper.cs
@@ -0,0 +1,97 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ internal class BlobAppendReferenceWrapper : ICloudAppendBlob
+ {
+ private readonly Uri _fullUri;
+ private readonly HttpClient _client;
+ private readonly Uri _appendUri;
+
+ public BlobAppendReferenceWrapper(string containerUrl, string name, HttpClient client)
+ {
+ var uriBuilder = new UriBuilder(containerUrl);
+ uriBuilder.Path += "/" + name;
+ _fullUri = uriBuilder.Uri;
+
+ AppendBlockQuery(uriBuilder);
+ _appendUri = uriBuilder.Uri;
+ _client = client;
+ }
+
+ ///
+ public async Task AppendAsync(ArraySegment data, CancellationToken cancellationToken)
+ {
+ Task AppendDataAsync()
+ {
+ var message = new HttpRequestMessage(HttpMethod.Put, _appendUri)
+ {
+ Content = new ByteArrayContent(data.Array, data.Offset, data.Count)
+ };
+ AddCommonHeaders(message);
+
+ return _client.SendAsync(message, cancellationToken);
+ }
+
+ var response = await AppendDataAsync();
+
+ if (response.StatusCode == HttpStatusCode.NotFound)
+ {
+ // If no blob exists try creating it
+ var message = new HttpRequestMessage(HttpMethod.Put, _fullUri)
+ {
+ // Set Content-Length to 0 to create "Append Blob"
+ Content = new ByteArrayContent(Array.Empty()),
+ Headers =
+ {
+ { "If-None-Match", "*" }
+ }
+ };
+
+ AddCommonHeaders(message);
+
+ response = await _client.SendAsync(message, cancellationToken);
+
+ // If result is 2** or 412 try to append again
+ if (response.IsSuccessStatusCode ||
+ response.StatusCode == HttpStatusCode.PreconditionFailed)
+ {
+ // Retry sending data after blob creation
+ response = await AppendDataAsync();
+ }
+ }
+
+ response.EnsureSuccessStatusCode();
+ }
+
+ private static void AddCommonHeaders(HttpRequestMessage message)
+ {
+ message.Headers.Add("x-ms-blob-type", "AppendBlob");
+ message.Headers.Add("x-ms-version", "2016-05-31");
+ message.Headers.Date = DateTimeOffset.UtcNow;
+ }
+
+ private static void AppendBlockQuery(UriBuilder uriBuilder)
+ {
+ // See https://msdn.microsoft.com/en-us/library/system.uribuilder.query.aspx for:
+ // Note: Do not append a string directly to Query property.
+ // If the length of Query is greater than 1, retrieve the property value
+ // as a string, remove the leading question mark, append the new query string,
+ // and set the property with the combined string.
+ var queryToAppend = "comp=appendblock";
+ if (uriBuilder.Query != null && uriBuilder.Query.Length > 1)
+ uriBuilder.Query = uriBuilder.Query.Substring(1) + "&" + queryToAppend;
+ else
+ uriBuilder.Query = queryToAppend;
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.cs
new file mode 100644
index 0000000000..f9a186872b
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BlobLoggerConfigureOptions.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.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class BlobLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+ private readonly IWebAppContext _context;
+
+ public BlobLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context)
+ : base(configuration, "AzureBlobEnabled")
+ {
+ _configuration = configuration;
+ _context = context;
+ }
+
+ public void Configure(AzureBlobLoggerOptions options)
+ {
+ base.Configure(options);
+ options.ContainerUrl = _configuration.GetSection("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL")?.Value;
+ options.ApplicationName = _context.SiteName;
+ options.ApplicationInstanceId = _context.SiteInstanceId;
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/BlobLoggerProvider.cs b/src/Logging.AzureAppServices/src/BlobLoggerProvider.cs
new file mode 100644
index 0000000000..3d62ea2ac6
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/BlobLoggerProvider.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.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// The implementation that stores messages by appending them to Azure Blob in batches.
+ ///
+ [ProviderAlias("AzureAppServicesBlob")]
+ public class BlobLoggerProvider : BatchingLoggerProvider
+ {
+ private readonly string _appName;
+ private readonly string _fileName;
+ private readonly Func _blobReferenceFactory;
+ private readonly HttpClient _httpClient;
+
+ ///
+ /// Creates a new instance of
+ ///
+ /// The options to use when creating a provider.
+ public BlobLoggerProvider(IOptionsMonitor options)
+ : this(options, null)
+ {
+ _blobReferenceFactory = name => new BlobAppendReferenceWrapper(
+ options.CurrentValue.ContainerUrl,
+ name,
+ _httpClient);
+ }
+
+ ///
+ /// Creates a new instance of
+ ///
+ /// The container to store logs to.
+ /// Options to be used in creating a logger.
+ internal BlobLoggerProvider(
+ IOptionsMonitor options,
+ Func blobReferenceFactory) :
+ base(options)
+ {
+ var value = options.CurrentValue;
+ _appName = value.ApplicationName;
+ _fileName = value.ApplicationInstanceId + "_" + value.BlobName;
+ _blobReferenceFactory = blobReferenceFactory;
+ _httpClient = new HttpClient();
+ }
+
+ internal override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken)
+ {
+ var eventGroups = messages.GroupBy(GetBlobKey);
+ foreach (var eventGroup in eventGroups)
+ {
+ var key = eventGroup.Key;
+ var blobName = $"{_appName}/{key.Year}/{key.Month:00}/{key.Day:00}/{key.Hour:00}/{_fileName}";
+
+ var blob = _blobReferenceFactory(blobName);
+
+ using (var stream = new MemoryStream())
+ using (var writer = new StreamWriter(stream))
+ {
+ foreach (var logEvent in eventGroup)
+ {
+ writer.Write(logEvent.Message);
+ }
+
+ await writer.FlushAsync();
+ var tryGetBuffer = stream.TryGetBuffer(out var buffer);
+ Debug.Assert(tryGetBuffer);
+ await blob.AppendAsync(buffer, cancellationToken);
+ }
+ }
+ }
+
+ private (int Year, int Month, int Day, int Hour) GetBlobKey(LogMessage e)
+ {
+ return (e.Timestamp.Year,
+ e.Timestamp.Month,
+ e.Timestamp.Day,
+ e.Timestamp.Hour);
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs b/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs
new file mode 100644
index 0000000000..c62ccb2331
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/ConfigurationBasedLevelSwitcher.cs
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class ConfigurationBasedLevelSwitcher: IConfigureOptions
+ {
+ private readonly IConfiguration _configuration;
+ private readonly Type _provider;
+ private readonly string _levelKey;
+
+ public ConfigurationBasedLevelSwitcher(IConfiguration configuration, Type provider, string levelKey)
+ {
+ _configuration = configuration;
+ _provider = provider;
+ _levelKey = levelKey;
+ }
+
+ public void Configure(LoggerFilterOptions options)
+ {
+ options.Rules.Add(new LoggerFilterRule(_provider.FullName, null, GetLogLevel(), null));
+ }
+
+ private LogLevel GetLogLevel()
+ {
+ return TextToLogLevel(_configuration.GetSection(_levelKey)?.Value);
+ }
+
+ private static LogLevel TextToLogLevel(string text)
+ {
+ switch (text?.ToUpperInvariant())
+ {
+ case "ERROR":
+ return LogLevel.Error;
+ case "WARNING":
+ return LogLevel.Warning;
+ case "INFORMATION":
+ return LogLevel.Information;
+ case "VERBOSE":
+ return LogLevel.Trace;
+ default:
+ return LogLevel.None;
+ }
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs b/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs
new file mode 100644
index 0000000000..8cd1f5eb91
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/FileLoggerConfigureOptions.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class FileLoggerConfigureOptions : BatchLoggerConfigureOptions, IConfigureOptions
+ {
+ private readonly IWebAppContext _context;
+
+ public FileLoggerConfigureOptions(IConfiguration configuration, IWebAppContext context)
+ : base(configuration, "AzureDriveEnabled")
+ {
+ _context = context;
+ }
+
+ public void Configure(AzureFileLoggerOptions options)
+ {
+ base.Configure(options);
+ options.LogDirectory = Path.Combine(_context.HomeFolder, "LogFiles", "Application");
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/FileLoggerProvider.cs b/src/Logging.AzureAppServices/src/FileLoggerProvider.cs
new file mode 100644
index 0000000000..1143d38c07
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/FileLoggerProvider.cs
@@ -0,0 +1,89 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// A which writes out to a file.
+ ///
+ [ProviderAlias("AzureAppServicesFile")]
+ public class FileLoggerProvider : BatchingLoggerProvider
+ {
+ private readonly string _path;
+ private readonly string _fileName;
+ private readonly int? _maxFileSize;
+ private readonly int? _maxRetainedFiles;
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The options to use when creating a provider.
+ public FileLoggerProvider(IOptionsMonitor options) : base(options)
+ {
+ var loggerOptions = options.CurrentValue;
+ _path = loggerOptions.LogDirectory;
+ _fileName = loggerOptions.FileName;
+ _maxFileSize = loggerOptions.FileSizeLimit;
+ _maxRetainedFiles = loggerOptions.RetainedFileCountLimit;
+ }
+
+ internal override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken)
+ {
+ Directory.CreateDirectory(_path);
+
+ foreach (var group in messages.GroupBy(GetGrouping))
+ {
+ var fullName = GetFullName(group.Key);
+ var fileInfo = new FileInfo(fullName);
+ if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize)
+ {
+ return;
+ }
+
+ using (var streamWriter = File.AppendText(fullName))
+ {
+ foreach (var item in group)
+ {
+ await streamWriter.WriteAsync(item.Message);
+ }
+ }
+ }
+
+ RollFiles();
+ }
+
+ private string GetFullName((int Year, int Month, int Day) group)
+ {
+ return Path.Combine(_path, $"{_fileName}{group.Year:0000}{group.Month:00}{group.Day:00}.txt");
+ }
+
+ private (int Year, int Month, int Day) GetGrouping(LogMessage message)
+ {
+ return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day);
+ }
+
+ private void RollFiles()
+ {
+ if (_maxRetainedFiles > 0)
+ {
+ var files = new DirectoryInfo(_path)
+ .GetFiles(_fileName + "*")
+ .OrderByDescending(f => f.Name)
+ .Skip(_maxRetainedFiles.Value);
+
+ foreach (var item in files)
+ {
+ item.Delete();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs b/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs
new file mode 100644
index 0000000000..2f55bbb0d1
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/ICloudAppendBlob.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Represents an append blob, a type of blob where blocks of data are always committed to the end of the blob.
+ ///
+ internal interface ICloudAppendBlob
+ {
+ ///
+ /// Initiates an asynchronous operation to open a stream for writing to the blob.
+ ///
+ /// A object of type that represents the asynchronous operation.
+ Task AppendAsync(ArraySegment data, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/IWebAppContext.cs b/src/Logging.AzureAppServices/src/IWebAppContext.cs
new file mode 100644
index 0000000000..f8c826ceb8
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/IWebAppContext.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Represents an Azure WebApp context
+ ///
+ internal interface IWebAppContext
+ {
+ ///
+ /// Gets the path to the home folder if running in Azure WebApp
+ ///
+ string HomeFolder { get; }
+
+ ///
+ /// Gets the name of site if running in Azure WebApp
+ ///
+ string SiteName { get; }
+
+ ///
+ /// Gets the id of site if running in Azure WebApp
+ ///
+ string SiteInstanceId { get; }
+
+ ///
+ /// Gets a value indicating whether or new we're in an Azure WebApp
+ ///
+ bool IsRunningInAzureWebApp { get; }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/LogMessage.cs b/src/Logging.AzureAppServices/src/LogMessage.cs
new file mode 100644
index 0000000000..4a1179ceb3
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/LogMessage.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.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal readonly struct LogMessage
+ {
+ public LogMessage(DateTimeOffset timestamp, string message)
+ {
+ Timestamp = timestamp;
+ Message = message;
+ }
+
+ public DateTimeOffset Timestamp { get; }
+ public string Message { get; }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj b/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj
new file mode 100644
index 0000000000..5bedde8c6d
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/Microsoft.Extensions.Logging.AzureAppServices.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Logger implementation to support Azure App Services 'Diagnostics logs' and 'Log stream' features.
+ netstandard2.0
+ $(NoWarn);CS1591
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs b/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..7c7d332545
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/Properties/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.CompilerServices;
+
+
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
diff --git a/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs b/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs
new file mode 100644
index 0000000000..452c936f93
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/SiteConfigurationProvider.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using Microsoft.Extensions.Configuration;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ internal class SiteConfigurationProvider
+ {
+ public static IConfiguration GetAzureLoggingConfiguration(IWebAppContext context)
+ {
+ var settingsFolder = Path.Combine(context.HomeFolder, "site", "diagnostics");
+ var settingsFile = Path.Combine(settingsFolder, "settings.json");
+
+ return new ConfigurationBuilder()
+ .AddEnvironmentVariables()
+ .AddJsonFile(settingsFile, optional: true, reloadOnChange: true)
+ .Build();
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/src/WebAppContext.cs b/src/Logging.AzureAppServices/src/WebAppContext.cs
new file mode 100644
index 0000000000..8bdd3f1c76
--- /dev/null
+++ b/src/Logging.AzureAppServices/src/WebAppContext.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices
+{
+ ///
+ /// Represents the default implementation of .
+ ///
+ internal class WebAppContext : IWebAppContext
+ {
+ ///
+ /// Gets the default instance of the WebApp context.
+ ///
+ public static WebAppContext Default { get; } = new WebAppContext();
+
+ private WebAppContext() { }
+
+ ///
+ public string HomeFolder { get; } = Environment.GetEnvironmentVariable("HOME");
+
+ ///
+ public string SiteName { get; } = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME");
+
+ ///
+ public string SiteInstanceId { get; } = Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID");
+
+ ///
+ public bool IsRunningInAzureWebApp => !string.IsNullOrEmpty(HomeFolder) &&
+ !string.IsNullOrEmpty(SiteName);
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs b/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs
new file mode 100644
index 0000000000..2fd5955e86
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/AzureAppendBlobTests.cs
@@ -0,0 +1,186 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class AzureAppendBlobTests
+ {
+ public string _containerUrl = "https://host/container?query=1";
+ public string _blobName = "blob/path";
+
+ [Fact]
+ public async Task SendsDataAsStream()
+ {
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ AssertDefaultHeaders(message);
+
+ return new HttpResponseMessage(HttpStatusCode.OK);
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None);
+ }
+
+ private static void AssertDefaultHeaders(HttpRequestMessage message)
+ {
+ Assert.Equal(new[] {"AppendBlob"}, message.Headers.GetValues("x-ms-blob-type"));
+ Assert.Equal(new[] {"2016-05-31"}, message.Headers.GetValues("x-ms-version"));
+ Assert.NotNull(message.Headers.Date);
+ }
+
+ [Theory]
+ [InlineData(HttpStatusCode.Created)]
+ [InlineData(HttpStatusCode.PreconditionFailed)]
+ public async Task CreatesBlobIfNotExist(HttpStatusCode createStatusCode)
+ {
+ var stage = 0;
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ // First PUT request
+ if (stage == 0)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ }
+ // Create request
+ if (stage == 1)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString());
+ Assert.Equal(0, message.Content.Headers.ContentLength);
+ Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match"));
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(createStatusCode);
+ }
+ // First PUT request
+ if (stage == 2)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.Created);
+ }
+ throw new NotImplementedException();
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None);
+
+ Assert.Equal(3, stage);
+ }
+
+ [Fact]
+ public async Task ThrowsForUnknownStatus()
+ {
+ var stage = 0;
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ // First PUT request
+ if (stage == 0)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.InternalServerError);
+ }
+
+ throw new NotImplementedException();
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await Assert.ThrowsAsync(() => blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None));
+
+ Assert.Equal(1, stage);
+ }
+
+ [Fact]
+ public async Task ThrowsForUnknownStatusDuringCreation()
+ {
+ var stage = 0;
+ var testMessageHandler = new TestMessageHandler(async message =>
+ {
+ // First PUT request
+ if (stage == 0)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1&comp=appendblock", message.RequestUri.ToString());
+ Assert.Equal(new byte[] { 0, 2, 3 }, await message.Content.ReadAsByteArrayAsync());
+ Assert.Equal(3, message.Content.Headers.ContentLength);
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.NotFound);
+ }
+ // Create request
+ if (stage == 1)
+ {
+ Assert.Equal(HttpMethod.Put, message.Method);
+ Assert.Equal("https://host/container/blob/path?query=1", message.RequestUri.ToString());
+ Assert.Equal(0, message.Content.Headers.ContentLength);
+ Assert.Equal(new[] { "*" }, message.Headers.GetValues("If-None-Match"));
+
+ AssertDefaultHeaders(message);
+
+ stage++;
+ return new HttpResponseMessage(HttpStatusCode.InternalServerError);
+ }
+
+ throw new NotImplementedException();
+ });
+
+ var blob = new BlobAppendReferenceWrapper(_containerUrl, _blobName, new HttpClient(testMessageHandler));
+ await Assert.ThrowsAsync(() => blob.AppendAsync(new ArraySegment(new byte[] { 0, 2, 3 }), CancellationToken.None));
+
+ Assert.Equal(2, stage);
+ }
+
+
+ private class TestMessageHandler : HttpMessageHandler
+ {
+ private readonly Func> _callback;
+
+ public TestMessageHandler(Func> callback)
+ {
+ _callback = callback;
+ }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return await _callback(request);
+ }
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs b/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs
new file mode 100644
index 0000000000..4d9125335a
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/AzureBlobSinkTests.cs
@@ -0,0 +1,98 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class AzureBlobSinkTests
+ {
+ DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
+
+ [Fact]
+ public async Task WritesMessagesInBatches()
+ {
+ var blob = new Mock();
+ var buffers = new List();
+ blob.Setup(b => b.AppendAsync(It.IsAny>(), It.IsAny()))
+ .Callback((ArraySegment s, CancellationToken ct) => buffers.Add(ToArray(s)))
+ .Returns(Task.CompletedTask);
+
+ var sink = new TestBlobSink(name => blob.Object);
+ var logger = (BatchingLogger)sink.CreateLogger("Cat");
+
+ await sink.IntervalControl.Pause;
+
+ for (int i = 0; i < 5; i++)
+ {
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state);
+ }
+
+ sink.IntervalControl.Resume();
+ await sink.IntervalControl.Pause;
+
+ Assert.Single(buffers);
+ Assert.Equal(
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 0" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 1" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 2" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 3" + Environment.NewLine +
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Text 4" + Environment.NewLine,
+ Encoding.UTF8.GetString(buffers[0]));
+ }
+
+ [Fact]
+ public async Task GroupsByHour()
+ {
+ var blob = new Mock();
+ var buffers = new List();
+ var names = new List();
+
+ blob.Setup(b => b.AppendAsync(It.IsAny>(), It.IsAny()))
+ .Callback((ArraySegment s, CancellationToken ct) => buffers.Add(ToArray(s)))
+ .Returns(Task.CompletedTask);
+
+ var sink = new TestBlobSink(name =>
+ {
+ names.Add(name);
+ return blob.Object;
+ });
+ var logger = (BatchingLogger)sink.CreateLogger("Cat");
+
+ await sink.IntervalControl.Pause;
+
+ var startDate = _timestampOne;
+ for (int i = 0; i < 3; i++)
+ {
+ logger.Log(startDate, LogLevel.Information, 0, "Text " + i, null, (state, ex) => state);
+
+ startDate = startDate.AddHours(1);
+ }
+
+ sink.IntervalControl.Resume();
+ await sink.IntervalControl.Pause;
+
+ Assert.Equal(3, buffers.Count);
+
+ Assert.Equal("appname/2016/05/04/03/42_filename", names[0]);
+ Assert.Equal("appname/2016/05/04/04/42_filename", names[1]);
+ Assert.Equal("appname/2016/05/04/05/42_filename", names[2]);
+ }
+
+ private byte[] ToArray(ArraySegment inputStream)
+ {
+ return inputStream.Array
+ .Skip(inputStream.Offset)
+ .Take(inputStream.Count)
+ .ToArray();
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs b/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs
new file mode 100644
index 0000000000..00d7dcd58d
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/AzureDiagnosticsConfigurationProviderTests.cs
@@ -0,0 +1,70 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class AzureDiagnosticsConfigurationProviderTests
+ {
+ [Fact]
+ public void NoConfigFile()
+ {
+ var tempFolder = Path.Combine(Path.GetTempPath(), "AzureWebAppLoggerThisFolderShouldNotExist");
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder)
+ .Returns(tempFolder);
+
+ var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object);
+
+ Assert.NotNull(config);
+ }
+
+ [Fact]
+ public void ReadsSettingsFileAndEnvironment()
+ {
+ var tempFolder = Path.Combine(Path.GetTempPath(), "WebAppLoggerConfigurationDisabledInSettingsFile");
+
+ try
+ {
+ var settingsFolder = Path.Combine(tempFolder, "site", "diagnostics");
+ var settingsFile = Path.Combine(settingsFolder, "settings.json");
+
+ if (!Directory.Exists(settingsFolder))
+ {
+ Directory.CreateDirectory(settingsFolder);
+ }
+ Environment.SetEnvironmentVariable("RANDOM_ENVIRONMENT_VARIABLE", "USEFUL_VALUE");
+ File.WriteAllText(settingsFile, @"{ ""key"":""test value"" }");
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder)
+ .Returns(tempFolder);
+
+ var config = SiteConfigurationProvider.GetAzureLoggingConfiguration(contextMock.Object);
+
+ Assert.Equal("test value", config["key"]);
+ Assert.Equal("USEFUL_VALUE", config["RANDOM_ENVIRONMENT_VARIABLE"]);
+ }
+ finally
+ {
+ if (Directory.Exists(tempFolder))
+ {
+ try
+ {
+ Directory.Delete(tempFolder, recursive: true);
+ }
+ catch
+ {
+ // Don't break the test if temp folder deletion fails.
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs b/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs
new file mode 100644
index 0000000000..9ab0c0cb45
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/BatchingLoggerProviderTests.cs
@@ -0,0 +1,136 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class BatchingLoggerProviderTests
+ {
+ private DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
+ private string _nl = Environment.NewLine;
+ private Regex _timeStampRegex = new Regex(@"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3} .\d{2}:\d{2} ");
+
+ [Fact]
+ public async Task LogsInIntervals()
+ {
+ var provider = new TestBatchingLoggingProvider();
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
+ Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[0][1].Message);
+ }
+
+ [Fact]
+ public async Task IncludesScopes()
+ {
+ var provider = new TestBatchingLoggingProvider(includeScopes: true);
+ var factory = new LoggerFactory(new [] { provider });
+ var logger = factory.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ using (logger.BeginScope("Scope"))
+ {
+ using (logger.BeginScope("Scope2"))
+ {
+ logger.Log(LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ }
+ }
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Matches(_timeStampRegex, provider.Batches[0][0].Message);
+ Assert.EndsWith(
+ " [Information] Cat => Scope => Scope2:" + _nl +
+ "Info message" + _nl,
+ provider.Batches[0][0].Message);
+ }
+
+ [Fact]
+ public async Task RespectsBatchSize()
+ {
+ var provider = new TestBatchingLoggingProvider(maxBatchSize: 1);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(2, provider.Batches.Count);
+ Assert.Single(provider.Batches[0]);
+ Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
+
+ Assert.Single(provider.Batches[1]);
+ Assert.Equal("2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + _nl, provider.Batches[1][0].Message);
+ }
+
+ [Fact]
+ public async Task DropsMessagesWhenReachingMaxQueue()
+ {
+ var provider = new TestBatchingLoggingProvider(maxQueueSize: 1);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(2, provider.Batches[0].Length);
+ Assert.Equal("2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + _nl, provider.Batches[0][0].Message);
+ Assert.Equal("1 message(s) dropped because of queue size limit. Increase the queue size or decrease logging verbosity to avoid this." + _nl, provider.Batches[0][1].Message);
+ }
+
+ private class TestBatchingLoggingProvider: BatchingLoggerProvider
+ {
+ public List Batches { get; } = new List();
+ public ManualIntervalControl IntervalControl { get; } = new ManualIntervalControl();
+
+ public TestBatchingLoggingProvider(TimeSpan? interval = null, int? maxBatchSize = null, int? maxQueueSize = null, bool includeScopes = false)
+ : base(new OptionsWrapperMonitor(new BatchingLoggerOptions
+ {
+ FlushPeriod = interval ?? TimeSpan.FromSeconds(1),
+ BatchSize = maxBatchSize,
+ BackgroundQueueSize = maxQueueSize,
+ IsEnabled = true,
+ IncludeScopes = includeScopes
+ }))
+ {
+ }
+
+ internal override Task WriteMessagesAsync(IEnumerable messages, CancellationToken token)
+ {
+ Batches.Add(messages.ToArray());
+ return Task.CompletedTask;
+ }
+
+ protected override Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
+ {
+ return IntervalControl.IntervalAsync();
+ }
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs b/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs
new file mode 100644
index 0000000000..46b72c7a0d
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/ConfigureOptionsTests.cs
@@ -0,0 +1,71 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.Extensions.Configuration;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class ConfigureOptionsTests
+ {
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ [InlineData(null)]
+ public void InitializesIsEnabled(bool? enabled)
+ {
+ var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[]
+ {
+ new KeyValuePair("IsEnabledKey", Convert.ToString(enabled))
+ }).Build();
+
+ var options = new BatchingLoggerOptions();
+ new BatchLoggerConfigureOptions(configuration, "IsEnabledKey").Configure(options);
+
+ Assert.Equal(enabled ?? false, options.IsEnabled);
+ }
+
+ [Fact]
+ public void InitializesLogDirectory()
+ {
+ var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[]
+ {
+ new KeyValuePair("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url")
+ }).Build();
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder).Returns("Home");
+
+ var options = new AzureFileLoggerOptions();
+ new FileLoggerConfigureOptions(configuration, contextMock.Object).Configure(options);
+
+ Assert.Equal(Path.Combine("Home", "LogFiles", "Application"), options.LogDirectory);
+ }
+
+ [Fact]
+ public void InitializesBlobUriSiteInstanceAndName()
+ {
+ var configuration = new ConfigurationBuilder().AddInMemoryCollection(new []
+ {
+ new KeyValuePair("APPSETTING_DIAGNOSTICS_AZUREBLOBCONTAINERSASURL", "http://container/url")
+ }).Build();
+
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.HomeFolder).Returns("Home");
+ contextMock.SetupGet(c => c.SiteInstanceId).Returns("InstanceId");
+ contextMock.SetupGet(c => c.SiteName).Returns("Name");
+
+ var options = new AzureBlobLoggerOptions();
+ new BlobLoggerConfigureOptions(configuration, contextMock.Object).Configure(options);
+
+ Assert.Equal("http://container/url", options.ContainerUrl);
+ Assert.Equal("InstanceId", options.ApplicationInstanceId);
+ Assert.Equal("Name", options.ApplicationName);
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/FileLoggerTests.cs b/src/Logging.AzureAppServices/test/FileLoggerTests.cs
new file mode 100644
index 0000000000..a3fcd2587d
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/FileLoggerTests.cs
@@ -0,0 +1,122 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class FileLoggerTests: IDisposable
+ {
+ DateTimeOffset _timestampOne = new DateTimeOffset(2016, 05, 04, 03, 02, 01, TimeSpan.Zero);
+
+ public FileLoggerTests()
+ {
+ TempPath = Path.GetTempFileName() + "_";
+ }
+
+ public string TempPath { get; }
+
+ public void Dispose()
+ {
+ try
+ {
+ if (Directory.Exists(TempPath))
+ {
+ Directory.Delete(TempPath, true);
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+ }
+
+ [Fact]
+ public async Task WritesToTextFile()
+ {
+ var provider = new TestFileLoggerProvider(TempPath);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine +
+ "2016-05-04 04:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine,
+ File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt")));
+ }
+
+ [Fact]
+ public async Task RollsTextFile()
+ {
+ var provider = new TestFileLoggerProvider(TempPath);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+
+ logger.Log(_timestampOne, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(_timestampOne.AddDays(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ Assert.Equal(
+ "2016-05-04 03:02:01.000 +00:00 [Information] Cat: Info message" + Environment.NewLine,
+ File.ReadAllText(Path.Combine(TempPath, "LogFile.20160504.txt")));
+
+ Assert.Equal(
+ "2016-05-05 03:02:01.000 +00:00 [Error] Cat: Error message" + Environment.NewLine,
+ File.ReadAllText(Path.Combine(TempPath, "LogFile.20160505.txt")));
+ }
+
+ [Fact]
+ public async Task RespectsMaxFileCount()
+ {
+ Directory.CreateDirectory(TempPath);
+ File.WriteAllText(Path.Combine(TempPath, "randomFile.txt"), "Text");
+
+ var provider = new TestFileLoggerProvider(TempPath, maxRetainedFiles: 5);
+ var logger = (BatchingLogger)provider.CreateLogger("Cat");
+
+ await provider.IntervalControl.Pause;
+ var timestamp = _timestampOne;
+
+ for (int i = 0; i < 10; i++)
+ {
+ logger.Log(timestamp, LogLevel.Information, 0, "Info message", null, (state, ex) => state);
+ logger.Log(timestamp.AddHours(1), LogLevel.Error, 0, "Error message", null, (state, ex) => state);
+
+ timestamp = timestamp.AddDays(1);
+ }
+
+ provider.IntervalControl.Resume();
+ await provider.IntervalControl.Pause;
+
+ var actualFiles = new DirectoryInfo(TempPath)
+ .GetFiles()
+ .Select(f => f.Name)
+ .OrderBy(f => f)
+ .ToArray();
+
+ Assert.Equal(6, actualFiles.Length);
+ Assert.Equal(new[] {
+ "LogFile.20160509.txt",
+ "LogFile.20160510.txt",
+ "LogFile.20160511.txt",
+ "LogFile.20160512.txt",
+ "LogFile.20160513.txt",
+ "randomFile.txt"
+ }, actualFiles);
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs b/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs
new file mode 100644
index 0000000000..cf8bede1a5
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/LoggerBuilderExtensionsTests.cs
@@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Linq;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ public class LoggerBuilderExtensionsTests
+ {
+ private IWebAppContext _appContext;
+
+ public LoggerBuilderExtensionsTests()
+ {
+ var contextMock = new Mock();
+ contextMock.SetupGet(c => c.IsRunningInAzureWebApp).Returns(true);
+ contextMock.SetupGet(c => c.HomeFolder).Returns(".");
+ _appContext = contextMock.Object;
+ }
+
+ [Fact]
+ public void BuilderExtensionAddsSingleSetOfServicesWhenCalledTwice()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+ var count = serviceCollection.Count;
+
+ Assert.NotEqual(0, count);
+
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ Assert.Equal(count, serviceCollection.Count);
+ }
+
+ [Fact]
+ public void BuilderExtensionAddsConfigurationChangeTokenSource()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddConfiguration(new ConfigurationBuilder().Build()));
+
+ // Tracking for main configuration
+ Assert.Equal(1, serviceCollection.Count(d => d.ServiceType == typeof(IOptionsChangeTokenSource)));
+
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ // Make sure we add another config change token for azure diagnostic configuration
+ Assert.Equal(2, serviceCollection.Count(d => d.ServiceType == typeof(IOptionsChangeTokenSource)));
+ }
+
+ [Fact]
+ public void BuilderExtensionAddsIConfigureOptions()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddConfiguration(new ConfigurationBuilder().Build()));
+
+ // Tracking for main configuration
+ Assert.Equal(2, serviceCollection.Count(d => d.ServiceType == typeof(IConfigureOptions)));
+
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ Assert.Equal(4, serviceCollection.Count(d => d.ServiceType == typeof(IConfigureOptions)));
+ }
+
+ [Fact]
+ public void LoggerProviderIsResolvable()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddLogging(builder => builder.AddAzureWebAppDiagnostics(_appContext));
+
+ var serviceProvider = serviceCollection.BuildServiceProvider();
+ var loggerFactory = serviceProvider.GetService();
+ }
+ }
+}
diff --git a/src/Logging.AzureAppServices/test/ManualIntervalControl.cs b/src/Logging.AzureAppServices/test/ManualIntervalControl.cs
new file mode 100644
index 0000000000..29cc883a28
--- /dev/null
+++ b/src/Logging.AzureAppServices/test/ManualIntervalControl.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.Logging.AzureAppServices.Test
+{
+ internal class ManualIntervalControl
+ {
+
+ private TaskCompletionSource
diff --git a/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs b/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs
index 3b290d712b..bab3515211 100644
--- a/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs
+++ b/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs
@@ -1,199 +1,199 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+//// Copyright (c) .NET Foundation. All rights reserved.
+//// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
-using Microsoft.Extensions.Primitives;
-using Xunit;
+//using System;
+//using System.Collections.Generic;
+//using System.Linq;
+//using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
+//using Microsoft.Extensions.Primitives;
+//using Xunit;
-namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
-{
- public class HPackHeaderWriterTests
- {
- public static TheoryData[], byte[], int?> SinglePayloadData
- {
- get
- {
- var data = new TheoryData[], byte[], int?>();
+//namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
+//{
+// public class HPackHeaderWriterTests
+// {
+// public static TheoryData[], byte[], int?> SinglePayloadData
+// {
+// get
+// {
+// var data = new TheoryData[], byte[], int?>();
- // Lowercase header name letters only
- data.Add(
- new[]
- {
- new KeyValuePair("CustomHeader", "CustomValue"),
- },
- new byte[]
- {
- // 0 12 c u s t o m
- 0x00, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
- // h e a d e r 11 C
- 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0b, 0x43,
- // u s t o m V a l
- 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c,
- // u e
- 0x75, 0x65
- },
- null);
- // Lowercase header name letters only
- data.Add(
- new[]
- {
- new KeyValuePair("CustomHeader!#$%&'*+-.^_`|~", "CustomValue"),
- },
- new byte[]
- {
- // 0 27 c u s t o m
- 0x00, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
- // h e a d e r ! #
- 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x21, 0x23,
- // $ % & ' * + - .
- 0x24, 0x25, 0x26, 0x27, 0x2a, 0x2b, 0x2d, 0x2e,
- // ^ _ ` | ~ 11 C u
- 0x5e, 0x5f, 0x60, 0x7c, 0x7e, 0x0b, 0x43, 0x75,
- // s t o m V a l u
- 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75,
- // e
- 0x65
- },
- null);
- // Single Payload
- data.Add(
- new[]
- {
- new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"),
- new KeyValuePair("content-type", "text/html; charset=utf-8"),
- new KeyValuePair("server", "Kestrel")
- },
- new byte[]
- {
- 0x88, 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d,
- 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20,
- 0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37,
- 0x20, 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33,
- 0x30, 0x20, 0x47, 0x4d, 0x54, 0x00, 0x0c, 0x63,
- 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74,
- 0x79, 0x70, 0x65, 0x18, 0x74, 0x65, 0x78, 0x74,
- 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x20, 0x63,
- 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x75,
- 0x74, 0x66, 0x2d, 0x38, 0x00, 0x06, 0x73, 0x65,
- 0x72, 0x76, 0x65, 0x72, 0x07, 0x4b, 0x65, 0x73,
- 0x74, 0x72, 0x65, 0x6c
- },
- 200);
+// // Lowercase header name letters only
+// data.Add(
+// new[]
+// {
+// new KeyValuePair("CustomHeader", "CustomValue"),
+// },
+// new byte[]
+// {
+// // 0 12 c u s t o m
+// 0x00, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
+// // h e a d e r 11 C
+// 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0b, 0x43,
+// // u s t o m V a l
+// 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c,
+// // u e
+// 0x75, 0x65
+// },
+// null);
+// // Lowercase header name letters only
+// data.Add(
+// new[]
+// {
+// new KeyValuePair("CustomHeader!#$%&'*+-.^_`|~", "CustomValue"),
+// },
+// new byte[]
+// {
+// // 0 27 c u s t o m
+// 0x00, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
+// // h e a d e r ! #
+// 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x21, 0x23,
+// // $ % & ' * + - .
+// 0x24, 0x25, 0x26, 0x27, 0x2a, 0x2b, 0x2d, 0x2e,
+// // ^ _ ` | ~ 11 C u
+// 0x5e, 0x5f, 0x60, 0x7c, 0x7e, 0x0b, 0x43, 0x75,
+// // s t o m V a l u
+// 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75,
+// // e
+// 0x65
+// },
+// null);
+// // Single Payload
+// data.Add(
+// new[]
+// {
+// new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"),
+// new KeyValuePair("content-type", "text/html; charset=utf-8"),
+// new KeyValuePair("server", "Kestrel")
+// },
+// new byte[]
+// {
+// 0x88, 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d,
+// 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20,
+// 0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37,
+// 0x20, 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33,
+// 0x30, 0x20, 0x47, 0x4d, 0x54, 0x00, 0x0c, 0x63,
+// 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74,
+// 0x79, 0x70, 0x65, 0x18, 0x74, 0x65, 0x78, 0x74,
+// 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x20, 0x63,
+// 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x75,
+// 0x74, 0x66, 0x2d, 0x38, 0x00, 0x06, 0x73, 0x65,
+// 0x72, 0x76, 0x65, 0x72, 0x07, 0x4b, 0x65, 0x73,
+// 0x74, 0x72, 0x65, 0x6c
+// },
+// 200);
- return data;
- }
- }
+// return data;
+// }
+// }
- [Theory]
- [MemberData(nameof(SinglePayloadData))]
- public void EncodesHeadersInSinglePayloadWhenSpaceAvailable(KeyValuePair[] headers, byte[] expectedPayload, int? statusCode)
- {
- var payload = new byte[1024];
- var length = 0;
- if (statusCode.HasValue)
- {
- Assert.True(HPackHeaderWriter.BeginEncodeHeaders(statusCode.Value, GetHeadersEnumerator(headers), payload, out length));
- }
- else
- {
- Assert.True(HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out length));
- }
- Assert.Equal(expectedPayload.Length, length);
+// [Theory]
+// [MemberData(nameof(SinglePayloadData))]
+// public void EncodesHeadersInSinglePayloadWhenSpaceAvailable(KeyValuePair[] headers, byte[] expectedPayload, int? statusCode)
+// {
+// var payload = new byte[1024];
+// var length = 0;
+// if (statusCode.HasValue)
+// {
+// Assert.True(HPackHeaderWriter.BeginEncodeHeaders(statusCode.Value, GetHeadersEnumerator(headers), payload, out length));
+// }
+// else
+// {
+// Assert.True(HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out length));
+// }
+// Assert.Equal(expectedPayload.Length, length);
- for (var i = 0; i < length; i++)
- {
- Assert.True(expectedPayload[i] == payload[i], $"{expectedPayload[i]} != {payload[i]} at {i} (len {length})");
- }
+// for (var i = 0; i < length; i++)
+// {
+// Assert.True(expectedPayload[i] == payload[i], $"{expectedPayload[i]} != {payload[i]} at {i} (len {length})");
+// }
- Assert.Equal(expectedPayload, new ArraySegment(payload, 0, length));
- }
+// Assert.Equal(expectedPayload, new ArraySegment(payload, 0, length));
+// }
- [Theory]
- [InlineData(true)]
- [InlineData(false)]
- public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize)
- {
- var statusCode = 200;
- var headers = new[]
- {
- new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"),
- new KeyValuePair("content-type", "text/html; charset=utf-8"),
- new KeyValuePair("server", "Kestrel")
- };
+// [Theory]
+// [InlineData(true)]
+// [InlineData(false)]
+// public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize)
+// {
+// var statusCode = 200;
+// var headers = new[]
+// {
+// new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"),
+// new KeyValuePair("content-type", "text/html; charset=utf-8"),
+// new KeyValuePair("server", "Kestrel")
+// };
- var expectedStatusCodePayload = new byte[]
- {
- 0x88
- };
+// var expectedStatusCodePayload = new byte[]
+// {
+// 0x88
+// };
- var expectedDateHeaderPayload = new byte[]
- {
- 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, 0x4d,
- 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, 0x4a,
- 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, 0x20,
- 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x30,
- 0x20, 0x47, 0x4d, 0x54
- };
+// var expectedDateHeaderPayload = new byte[]
+// {
+// 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, 0x4d,
+// 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, 0x4a,
+// 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, 0x20,
+// 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x30,
+// 0x20, 0x47, 0x4d, 0x54
+// };
- var expectedContentTypeHeaderPayload = new byte[]
- {
- 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
- 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x18, 0x74,
- 0x65, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c,
- 0x3b, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65,
- 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38
- };
+// var expectedContentTypeHeaderPayload = new byte[]
+// {
+// 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
+// 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x18, 0x74,
+// 0x65, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c,
+// 0x3b, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65,
+// 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38
+// };
- var expectedServerHeaderPayload = new byte[]
- {
- 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
- 0x07, 0x4b, 0x65, 0x73, 0x74, 0x72, 0x65, 0x6c
- };
+// var expectedServerHeaderPayload = new byte[]
+// {
+// 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
+// 0x07, 0x4b, 0x65, 0x73, 0x74, 0x72, 0x65, 0x6c
+// };
- Span payload = new byte[1024];
- var offset = 0;
- var headerEnumerator = GetHeadersEnumerator(headers);
+// Span payload = new byte[1024];
+// var offset = 0;
+// var headerEnumerator = GetHeadersEnumerator(headers);
- // When !exactSize, slices are one byte short of fitting the next header
- var sliceLength = expectedStatusCodePayload.Length + (exactSize ? 0 : expectedDateHeaderPayload.Length - 1);
- Assert.False(HPackHeaderWriter.BeginEncodeHeaders(statusCode, headerEnumerator, payload.Slice(offset, sliceLength), out var length));
- Assert.Equal(expectedStatusCodePayload.Length, length);
- Assert.Equal(expectedStatusCodePayload, payload.Slice(0, length).ToArray());
+// // When !exactSize, slices are one byte short of fitting the next header
+// var sliceLength = expectedStatusCodePayload.Length + (exactSize ? 0 : expectedDateHeaderPayload.Length - 1);
+// Assert.False(HPackHeaderWriter.BeginEncodeHeaders(statusCode, headerEnumerator, payload.Slice(offset, sliceLength), out var length));
+// Assert.Equal(expectedStatusCodePayload.Length, length);
+// Assert.Equal(expectedStatusCodePayload, payload.Slice(0, length).ToArray());
- offset += length;
+// offset += length;
- sliceLength = expectedDateHeaderPayload.Length + (exactSize ? 0 : expectedContentTypeHeaderPayload.Length - 1);
- Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length));
- Assert.Equal(expectedDateHeaderPayload.Length, length);
- Assert.Equal(expectedDateHeaderPayload, payload.Slice(offset, length).ToArray());
+// sliceLength = expectedDateHeaderPayload.Length + (exactSize ? 0 : expectedContentTypeHeaderPayload.Length - 1);
+// Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length));
+// Assert.Equal(expectedDateHeaderPayload.Length, length);
+// Assert.Equal(expectedDateHeaderPayload, payload.Slice(offset, length).ToArray());
- offset += length;
+// offset += length;
- sliceLength = expectedContentTypeHeaderPayload.Length + (exactSize ? 0 : expectedServerHeaderPayload.Length - 1);
- Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length));
- Assert.Equal(expectedContentTypeHeaderPayload.Length, length);
- Assert.Equal(expectedContentTypeHeaderPayload, payload.Slice(offset, length).ToArray());
+// sliceLength = expectedContentTypeHeaderPayload.Length + (exactSize ? 0 : expectedServerHeaderPayload.Length - 1);
+// Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length));
+// Assert.Equal(expectedContentTypeHeaderPayload.Length, length);
+// Assert.Equal(expectedContentTypeHeaderPayload, payload.Slice(offset, length).ToArray());
- offset += length;
+// offset += length;
- sliceLength = expectedServerHeaderPayload.Length;
- Assert.True(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length));
- Assert.Equal(expectedServerHeaderPayload.Length, length);
- Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray());
- }
+// sliceLength = expectedServerHeaderPayload.Length;
+// Assert.True(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length));
+// Assert.Equal(expectedServerHeaderPayload.Length, length);
+// Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray());
+// }
- private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers)
- {
- var groupedHeaders = headers
- .GroupBy(k => k.Key)
- .ToDictionary(g => g.Key, g => new StringValues(g.Select(gg => gg.Value).ToArray()));
+// private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers)
+// {
+// var groupedHeaders = headers
+// .GroupBy(k => k.Key)
+// .ToDictionary(g => g.Key, g => new StringValues(g.Select(gg => gg.Value).ToArray()));
- var enumerator = new Http2HeadersEnumerator();
- enumerator.Initialize(groupedHeaders);
- return enumerator;
- }
- }
-}
+// var enumerator = new Http2HeadersEnumerator();
+// enumerator.Initialize(groupedHeaders);
+// return enumerator;
+// }
+// }
+//}
diff --git a/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs b/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs
index 6300fcf85b..30913d952e 100644
--- a/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs
+++ b/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs
@@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
// Arrange
var pipe = new Pipe(new PipeOptions(_dirtyMemoryPool, PipeScheduler.Inline, PipeScheduler.Inline));
- var frameWriter = new Http2FrameWriter(pipe.Writer, null, null, null, null, null, null, _dirtyMemoryPool, new Mock().Object);
+ var frameWriter = CreateFrameWriter(pipe);
// Act
await frameWriter.WriteWindowUpdateAsync(1, 1);
@@ -52,12 +52,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(new byte[] { 0x00, 0x00, 0x00, 0x01 }, payload.Skip(Http2FrameReader.HeaderLength).Take(4).ToArray());
}
+ private Http2FrameWriter CreateFrameWriter(Pipe pipe)
+ {
+ var serviceContext = new Internal.ServiceContext
+ {
+ ServerOptions = new KestrelServerOptions(),
+ Log = new Mock().Object
+ };
+ return new Http2FrameWriter(pipe.Writer, null, null, null, null, null, null, _dirtyMemoryPool, serviceContext);
+ }
+
[Fact]
public async Task WriteGoAway_UnsetsReservedBit()
{
// Arrange
var pipe = new Pipe(new PipeOptions(_dirtyMemoryPool, PipeScheduler.Inline, PipeScheduler.Inline));
- var frameWriter = new Http2FrameWriter(pipe.Writer, null, null, null, null, null, null, _dirtyMemoryPool, new Mock().Object);
+ var frameWriter = CreateFrameWriter(pipe);
// Act
await frameWriter.WriteGoAwayAsync(1, Http2ErrorCode.NO_ERROR);
diff --git a/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs
new file mode 100644
index 0000000000..c9dcdb00ec
--- /dev/null
+++ b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs
@@ -0,0 +1,479 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http.HPack;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
+{
+ public class Http2HPackEncoderTests
+ {
+ [Fact]
+ public void BeginEncodeHeaders_Status302_NewIndexValue()
+ {
+ Span buffer = new byte[1024 * 16];
+
+ var headers = new HttpResponseHeaders();
+ var enumerator = new Http2HeadersEnumerator();
+ enumerator.Initialize(headers);
+
+ var hpackEncoder = new HPackEncoder();
+ Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length));
+
+ var result = buffer.Slice(0, length).ToArray();
+ var hex = BitConverter.ToString(result);
+ Assert.Equal("48-03-33-30-32", hex);
+
+ var statusHeader = GetHeaderEntry(hpackEncoder, 0);
+ Assert.Equal(":status", statusHeader.Name);
+ Assert.Equal("302", statusHeader.Value);
+ }
+
+ [Fact]
+ public void BeginEncodeHeaders_CacheControlPrivate_NewIndexValue()
+ {
+ Span buffer = new byte[1024 * 16];
+
+ var headers = new HttpResponseHeaders();
+ headers.HeaderCacheControl = "private";
+
+ var enumerator = new Http2HeadersEnumerator();
+ enumerator.Initialize(headers);
+
+ var hpackEncoder = new HPackEncoder();
+ Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length));
+
+ var result = buffer.Slice(5, length - 5).ToArray();
+ var hex = BitConverter.ToString(result);
+ Assert.Equal("58-07-70-72-69-76-61-74-65", hex);
+
+ var statusHeader = GetHeaderEntry(hpackEncoder, 0);
+ Assert.Equal("Cache-Control", statusHeader.Name);
+ Assert.Equal("private", statusHeader.Value);
+ }
+
+ [Fact]
+ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit()
+ {
+ // Test follows example https://tools.ietf.org/html/rfc7541#appendix-C.5
+
+ Span buffer = new byte[1024 * 16];
+
+ var headers = new HttpResponseHeaders();
+ headers.HeaderCacheControl = "private";
+ headers.HeaderDate = "Mon, 21 Oct 2013 20:13:21 GMT";
+ headers.HeaderLocation = "https://www.example.com";
+
+ var enumerator = new Http2HeadersEnumerator();
+
+ var hpackEncoder = new HPackEncoder(maxHeaderTableSize: 256);
+
+ // First response
+ enumerator.Initialize(headers);
+ Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length));
+
+ var result = buffer.Slice(0, length).ToArray();
+ var hex = BitConverter.ToString(result);
+ Assert.Equal(
+ "48-03-33-30-32-58-07-70-72-69-76-61-74-65-61-1D-" +
+ "4D-6F-6E-2C-20-32-31-20-4F-63-74-20-32-30-31-33-" +
+ "20-32-30-3A-31-33-3A-32-31-20-47-4D-54-6E-17-68-" +
+ "74-74-70-73-3A-2F-2F-77-77-77-2E-65-78-61-6D-70-" +
+ "6C-65-2E-63-6F-6D", hex);
+
+ var entries = GetHeaderEntries(hpackEncoder);
+ Assert.Collection(entries,
+ e =>
+ {
+ Assert.Equal("Location", e.Name);
+ Assert.Equal("https://www.example.com", e.Value);
+ },
+ e =>
+ {
+ Assert.Equal("Date", e.Name);
+ Assert.Equal("Mon, 21 Oct 2013 20:13:21 GMT", e.Value);
+ },
+ e =>
+ {
+ Assert.Equal("Cache-Control", e.Name);
+ Assert.Equal("private", e.Value);
+ },
+ e =>
+ {
+ Assert.Equal(":status", e.Name);
+ Assert.Equal("302", e.Value);
+ });
+
+ // Second response
+ enumerator.Initialize(headers);
+ Assert.True(HPackHeaderWriter.BeginEncodeHeaders(307, hpackEncoder, enumerator, buffer, out length));
+
+ result = buffer.Slice(0, length).ToArray();
+ hex = BitConverter.ToString(result);
+ Assert.Equal("48-03-33-30-37-C1-C0-BF", hex);
+
+ entries = GetHeaderEntries(hpackEncoder);
+ Assert.Collection(entries,
+ e =>
+ {
+ Assert.Equal(":status", e.Name);
+ Assert.Equal("307", e.Value);
+ },
+ e =>
+ {
+ Assert.Equal("Location", e.Name);
+ Assert.Equal("https://www.example.com", e.Value);
+ },
+ e =>
+ {
+ Assert.Equal("Date", e.Name);
+ Assert.Equal("Mon, 21 Oct 2013 20:13:21 GMT", e.Value);
+ },
+ e =>
+ {
+ Assert.Equal("Cache-Control", e.Name);
+ Assert.Equal("private", e.Value);
+ });
+
+ // Third response
+ headers.HeaderDate = "Mon, 21 Oct 2013 20:13:22 GMT";
+ headers.HeaderContentEncoding = "gzip";
+ headers.HeaderSetCookie = "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1";
+
+ enumerator.Initialize(headers);
+ Assert.True(HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out length));
+
+ result = buffer.Slice(0, length).ToArray();
+ hex = BitConverter.ToString(result);
+ Assert.Equal(
+ "88-C1-61-1D-4D-6F-6E-2C-20-32-31-20-4F-63-74-20-" +
+ "32-30-31-33-20-32-30-3A-31-33-3A-32-32-20-47-4D-" +
+ "54-5A-04-67-7A-69-70-C1-1F-28-38-66-6F-6F-3D-41-" +
+ "53-44-4A-4B-48-51-4B-42-5A-58-4F-51-57-45-4F-50-" +
+ "49-55-41-58-51-57-45-4F-49-55-3B-20-6D-61-78-2D-" +
+ "61-67-65-3D-33-36-30-30-3B-20-76-65-72-73-69-6F-" +
+ "6E-3D-31", hex);
+
+ entries = GetHeaderEntries(hpackEncoder);
+ Assert.Collection(entries,
+ e =>
+ {
+ Assert.Equal("Content-Encoding", e.Name);
+ Assert.Equal("gzip", e.Value);
+ },
+ e =>
+ {
+ Assert.Equal("Date", e.Name);
+ Assert.Equal("Mon, 21 Oct 2013 20:13:22 GMT", e.Value);
+ },
+ e =>
+ {
+ Assert.Equal(":status", e.Name);
+ Assert.Equal("307", e.Value);
+ },
+ e =>
+ {
+ Assert.Equal("Location", e.Name);
+ Assert.Equal("https://www.example.com", e.Value);
+ });
+ }
+
+ [Theory]
+ [InlineData("Set-Cookie", true)]
+ [InlineData("Content-Disposition", true)]
+ [InlineData("Content-Length", false)]
+ public void BeginEncodeHeaders_ExcludedHeaders_NotAddedToTable(string headerName, bool neverIndex)
+ {
+ Span buffer = new byte[1024 * 16];
+
+ var headers = new HttpResponseHeaders();
+ headers.Append(headerName, "1");
+
+ var enumerator = new Http2HeadersEnumerator();
+ enumerator.Initialize(headers);
+
+ var hpackEncoder = new HPackEncoder(maxHeaderTableSize: Http2PeerSettings.DefaultHeaderTableSize);
+ Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out _));
+
+ if (neverIndex)
+ {
+ Assert.Equal(0x10, buffer[0] & 0x10);
+ }
+ else
+ {
+ Assert.Equal(0, buffer[0] & 0x40);
+ }
+
+ Assert.Empty(GetHeaderEntries(hpackEncoder));
+ }
+
+ [Fact]
+ public void BeginEncodeHeaders_HeaderExceedHeaderTableSize_NoIndexAndNoHeaderEntry()
+ {
+ Span buffer = new byte[1024 * 16];
+
+ var headers = new HttpResponseHeaders();
+ headers.Append("x-Custom", new string('!', (int)Http2PeerSettings.DefaultHeaderTableSize));
+
+ var enumerator = new Http2HeadersEnumerator();
+ enumerator.Initialize(headers);
+
+ var hpackEncoder = new HPackEncoder();
+ Assert.True(HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out var length));
+
+ Assert.Empty(GetHeaderEntries(hpackEncoder));
+ }
+
+ public static TheoryData[], byte[], int?> SinglePayloadData
+ {
+ get
+ {
+ var data = new TheoryData[], byte[], int?>();
+
+ // Lowercase header name letters only
+ data.Add(
+ new[]
+ {
+ new KeyValuePair("CustomHeader", "CustomValue"),
+ },
+ new byte[]
+ {
+ // 12 c u s t o m
+ 0x40, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
+ // h e a d e r 11 C
+ 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0b, 0x43,
+ // u s t o m V a l
+ 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c,
+ // u e
+ 0x75, 0x65
+ },
+ null);
+ // Lowercase header name letters only
+ data.Add(
+ new[]
+ {
+ new KeyValuePair("CustomHeader!#$%&'*+-.^_`|~", "CustomValue"),
+ },
+ new byte[]
+ {
+ // 27 c u s t o m
+ 0x40, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
+ // h e a d e r ! #
+ 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x21, 0x23,
+ // $ % & ' * + - .
+ 0x24, 0x25, 0x26, 0x27, 0x2a, 0x2b, 0x2d, 0x2e,
+ // ^ _ ` | ~ 11 C u
+ 0x5e, 0x5f, 0x60, 0x7c, 0x7e, 0x0b, 0x43, 0x75,
+ // s t o m V a l u
+ 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75,
+ // e
+ 0x65
+ },
+ null);
+ // Single Payload
+ data.Add(
+ new[]
+ {
+ new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"),
+ new KeyValuePair("content-type", "text/html; charset=utf-8"),
+ new KeyValuePair("server", "Kestrel")
+ },
+ new byte[]
+ {
+ 0x88, 0x40, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d,
+ 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20,
+ 0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37,
+ 0x20, 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33,
+ 0x30, 0x20, 0x47, 0x4d, 0x54, 0x40, 0x0c, 0x63,
+ 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74,
+ 0x79, 0x70, 0x65, 0x18, 0x74, 0x65, 0x78, 0x74,
+ 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x20, 0x63,
+ 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x75,
+ 0x74, 0x66, 0x2d, 0x38, 0x40, 0x06, 0x73, 0x65,
+ 0x72, 0x76, 0x65, 0x72, 0x07, 0x4b, 0x65, 0x73,
+ 0x74, 0x72, 0x65, 0x6c
+ },
+ 200);
+
+ return data;
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(SinglePayloadData))]
+ public void EncodesHeadersInSinglePayloadWhenSpaceAvailable(KeyValuePair[] headers, byte[] expectedPayload, int? statusCode)
+ {
+ HPackEncoder hpackEncoder = new HPackEncoder();
+
+ var payload = new byte[1024];
+ var length = 0;
+ if (statusCode.HasValue)
+ {
+ Assert.True(HPackHeaderWriter.BeginEncodeHeaders(statusCode.Value, hpackEncoder, GetHeadersEnumerator(headers), payload, out length));
+ }
+ else
+ {
+ Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, GetHeadersEnumerator(headers), payload, out length));
+ }
+ Assert.Equal(expectedPayload.Length, length);
+
+ for (var i = 0; i < length; i++)
+ {
+ Assert.True(expectedPayload[i] == payload[i], $"{expectedPayload[i]} != {payload[i]} at {i} (len {length})");
+ }
+
+ Assert.Equal(expectedPayload, new ArraySegment(payload, 0, length));
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize)
+ {
+ var statusCode = 200;
+ var headers = new[]
+ {
+ new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"),
+ new KeyValuePair("content-type", "text/html; charset=utf-8"),
+ new KeyValuePair("server", "Kestrel")
+ };
+
+ var expectedStatusCodePayload = new byte[]
+ {
+ 0x88
+ };
+
+ var expectedDateHeaderPayload = new byte[]
+ {
+ 0x40, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, 0x4d,
+ 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, 0x4a,
+ 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, 0x20,
+ 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x30,
+ 0x20, 0x47, 0x4d, 0x54
+ };
+
+ var expectedContentTypeHeaderPayload = new byte[]
+ {
+ 0x40, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e,
+ 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x18, 0x74,
+ 0x65, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c,
+ 0x3b, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65,
+ 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38
+ };
+
+ var expectedServerHeaderPayload = new byte[]
+ {
+ 0x40, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
+ 0x07, 0x4b, 0x65, 0x73, 0x74, 0x72, 0x65, 0x6c
+ };
+
+ var hpackEncoder = new HPackEncoder();
+
+ Span payload = new byte[1024];
+ var offset = 0;
+ var headerEnumerator = GetHeadersEnumerator(headers);
+
+ // When !exactSize, slices are one byte short of fitting the next header
+ var sliceLength = expectedStatusCodePayload.Length + (exactSize ? 0 : expectedDateHeaderPayload.Length - 1);
+ Assert.False(HPackHeaderWriter.BeginEncodeHeaders(statusCode, hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out var length));
+ Assert.Equal(expectedStatusCodePayload.Length, length);
+ Assert.Equal(expectedStatusCodePayload, payload.Slice(0, length).ToArray());
+
+ offset += length;
+
+ sliceLength = expectedDateHeaderPayload.Length + (exactSize ? 0 : expectedContentTypeHeaderPayload.Length - 1);
+ Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length));
+ Assert.Equal(expectedDateHeaderPayload.Length, length);
+ Assert.Equal(expectedDateHeaderPayload, payload.Slice(offset, length).ToArray());
+
+ offset += length;
+
+ sliceLength = expectedContentTypeHeaderPayload.Length + (exactSize ? 0 : expectedServerHeaderPayload.Length - 1);
+ Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length));
+ Assert.Equal(expectedContentTypeHeaderPayload.Length, length);
+ Assert.Equal(expectedContentTypeHeaderPayload, payload.Slice(offset, length).ToArray());
+
+ offset += length;
+
+ sliceLength = expectedServerHeaderPayload.Length;
+ Assert.True(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length));
+ Assert.Equal(expectedServerHeaderPayload.Length, length);
+ Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray());
+ }
+
+ [Fact]
+ public void BeginEncodeHeaders_MaxHeaderTableSizeUpdated_SizeUpdateInHeaders()
+ {
+ Span buffer = new byte[1024 * 16];
+
+ var hpackEncoder = new HPackEncoder();
+ hpackEncoder.UpdateMaxHeaderTableSize(100);
+
+ var enumerator = new Http2HeadersEnumerator();
+
+ // First request
+ enumerator.Initialize(new Dictionary());
+ Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out var length));
+
+ Assert.Equal(2, length);
+
+ const byte DynamicTableSizeUpdateMask = 0xe0;
+
+ var integerDecoder = new IntegerDecoder();
+ Assert.False(integerDecoder.BeginTryDecode((byte)(buffer[0] & ~DynamicTableSizeUpdateMask), prefixLength: 5, out _));
+ Assert.True(integerDecoder.TryDecode(buffer[1], out var result));
+
+ Assert.Equal(100, result);
+
+ // Second request
+ enumerator.Initialize(new Dictionary());
+ Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out length));
+
+ Assert.Equal(0, length);
+ }
+
+ private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers)
+ {
+ var groupedHeaders = headers
+ .GroupBy(k => k.Key)
+ .ToDictionary(g => g.Key, g => new StringValues(g.Select(gg => gg.Value).ToArray()));
+
+ var enumerator = new Http2HeadersEnumerator();
+ enumerator.Initialize(groupedHeaders);
+ return enumerator;
+ }
+
+ private EncoderHeaderEntry GetHeaderEntry(HPackEncoder encoder, int index)
+ {
+ var entry = encoder.Head;
+ while (index-- >= 0)
+ {
+ entry = entry.Before;
+ }
+ return entry;
+ }
+
+ private List GetHeaderEntries(HPackEncoder encoder)
+ {
+ var headers = new List();
+
+ var entry = encoder.Head;
+ while (entry.Before != encoder.Head)
+ {
+ entry = entry.Before;
+ headers.Add(entry);
+ };
+
+ return headers;
+ }
+ }
+}
diff --git a/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs
index 5c7623337c..fd8146804e 100644
--- a/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs
+++ b/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs
@@ -333,11 +333,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Theory]
[InlineData(int.MinValue)]
[InlineData(-1)]
- [InlineData(0)]
public void Http2HeaderTableSizeInvalid(int value)
{
var ex = Assert.Throws(() => new KestrelServerLimits().Http2.HeaderTableSize = value);
- Assert.StartsWith(CoreStrings.GreaterThanZeroRequired, ex.Message);
+ Assert.StartsWith(CoreStrings.GreaterThanOrEqualToZeroRequired, ex.Message);
}
[Fact]
diff --git a/src/Servers/Kestrel/Directory.Build.props b/src/Servers/Kestrel/Directory.Build.props
index 05b71894da..1b0e999039 100644
--- a/src/Servers/Kestrel/Directory.Build.props
+++ b/src/Servers/Kestrel/Directory.Build.props
@@ -20,7 +20,5 @@
-
-
diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs
similarity index 92%
rename from src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs
rename to src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs
index b3e8f15403..3886a38b09 100644
--- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs
+++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs
@@ -8,6 +8,7 @@ using System.Diagnostics;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
+using System.Net.Http.HPack;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Http;
@@ -24,28 +25,25 @@ using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
- public class Http2ConnectionBenchmark
+ public abstract class Http2ConnectionBenchmarkBase
{
private MemoryPool _memoryPool;
private HttpRequestHeaders _httpRequestHeaders;
private Http2Connection _connection;
+ private HPackEncoder _hpackEncoder;
private Http2HeadersEnumerator _requestHeadersEnumerator;
private int _currentStreamId;
private byte[] _headersBuffer;
private DuplexPipe.DuplexPipePair _connectionPair;
private Http2Frame _httpFrame;
- private string _responseData;
private int _dataWritten;
- [Params(0, 10, 1024 * 1024)]
- public int ResponseDataLength { get; set; }
+ protected abstract Task ProcessRequest(HttpContext httpContext);
- [GlobalSetup]
- public void GlobalSetup()
+ public virtual void GlobalSetup()
{
_memoryPool = SlabMemoryPoolFactory.Create();
_httpFrame = new Http2Frame();
- _responseData = new string('!', ResponseDataLength);
var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
@@ -58,6 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
_httpRequestHeaders.Append(HeaderNames.Authority, new StringValues("localhost:80"));
_headersBuffer = new byte[1024 * 16];
+ _hpackEncoder = new HPackEncoder();
var serviceContext = new ServiceContext
{
@@ -83,7 +82,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
_currentStreamId = 1;
- _ = _connection.ProcessRequestsAsync(new DummyApplication(c => ResponseDataLength == 0 ? Task.CompletedTask : c.Response.WriteAsync(_responseData), new MockHttpContextFactory()));
+ _ = _connection.ProcessRequestsAsync(new DummyApplication(ProcessRequest, new MockHttpContextFactory()));
_connectionPair.Application.Output.Write(Http2Connection.ClientPreface);
_connectionPair.Application.Output.WriteSettings(new Http2PeerSettings
@@ -102,11 +101,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
}
[Benchmark]
- public async Task EmptyRequest()
+ public async Task MakeRequest()
{
_requestHeadersEnumerator.Initialize(_httpRequestHeaders);
_requestHeadersEnumerator.MoveNext();
- _connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _httpFrame);
+ _connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _hpackEncoder, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _httpFrame);
await _connectionPair.Application.Output.FlushAsync();
while (true)
diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionEmptyBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionEmptyBenchmark.cs
new file mode 100644
index 0000000000..6859b5b675
--- /dev/null
+++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionEmptyBenchmark.cs
@@ -0,0 +1,29 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Performance
+{
+ public class Http2ConnectionBenchmark : Http2ConnectionBenchmarkBase
+ {
+ [Params(0, 128, 1024)]
+ public int ResponseDataLength { get; set; }
+
+ private string _responseData;
+
+ [GlobalSetup]
+ public override void GlobalSetup()
+ {
+ base.GlobalSetup();
+ _responseData = new string('!', ResponseDataLength);
+ }
+
+ protected override Task ProcessRequest(HttpContext httpContext)
+ {
+ return ResponseDataLength == 0 ? Task.CompletedTask : httpContext.Response.WriteAsync(_responseData);
+ }
+ }
+}
diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionHeadersBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionHeadersBenchmark.cs
new file mode 100644
index 0000000000..5ac19dd0b9
--- /dev/null
+++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionHeadersBenchmark.cs
@@ -0,0 +1,48 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Performance
+{
+ public class Http2ConnectionHeadersBenchmark : Http2ConnectionBenchmarkBase
+ {
+ [Params(1, 4, 32)]
+ public int HeadersCount { get; set; }
+
+ [Params(true, false)]
+ public bool HeadersChange { get; set; }
+
+ private int _headerIndex;
+ private string[] _headerNames;
+
+ [GlobalSetup]
+ public override void GlobalSetup()
+ {
+ base.GlobalSetup();
+
+ _headerNames = new string[HeadersCount * (HeadersChange ? 1000 : 1)];
+ for (var i = 0; i < _headerNames.Length; i++)
+ {
+ _headerNames[i] = "CustomHeader" + i;
+ }
+ }
+
+ protected override Task ProcessRequest(HttpContext httpContext)
+ {
+ for (var i = 0; i < HeadersCount; i++)
+ {
+ var headerName = _headerNames[_headerIndex % HeadersCount];
+ httpContext.Response.Headers[headerName] = "The quick brown fox jumps over the lazy dog.";
+ if (HeadersChange)
+ {
+ _headerIndex++;
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs
index 839558d1a3..ff58ed573d 100644
--- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs
+++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs
@@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
minResponseDataRate: null,
"TestConnectionId",
_memoryPool,
- new KestrelTrace(NullLogger.Instance));
+ new Core.Internal.ServiceContext());
_responseHeaders = new HttpResponseHeaders();
_responseHeaders.HeaderContentType = "application/json";
diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs
index 3960ebe388..8a38f97709 100644
--- a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs
+++ b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs
@@ -26,6 +26,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
}
}
+ [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
+ public void JsonTechEmpower()
+ {
+ for (var i = 0; i < RequestParsingData.InnerLoopCount; i++)
+ {
+ InsertData(RequestParsingData.JsonTechEmpowerRequest);
+ ParseData();
+ }
+ }
+
[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)]
public void LiveAspNet()
{
diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs
index 5c496960bb..7b50a6fba0 100644
--- a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs
+++ b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingData.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
@@ -19,6 +19,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
"Connection: keep-alive\r\n" +
"\r\n";
+ private const string _jsonTechEmpowerRequest =
+ "GET /json HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Accept: Accept:application/json,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7\r\n" +
+ "Connection: keep-alive\r\n" +
+ "\r\n";
+
// edge-casey - client's don't normally send this
private const string _plaintextAbsoluteUriRequest =
"GET http://localhost/plaintext HTTP/1.1\r\n" +
@@ -59,6 +66,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
public static readonly byte[] PlaintextTechEmpowerPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_plaintextTechEmpowerRequest, Pipelining)));
public static readonly byte[] PlaintextTechEmpowerRequest = Encoding.ASCII.GetBytes(_plaintextTechEmpowerRequest);
+ public static readonly byte[] JsonTechEmpowerRequest = Encoding.ASCII.GetBytes(_jsonTechEmpowerRequest);
+
public static readonly byte[] PlaintextAbsoluteUriRequest = Encoding.ASCII.GetBytes(_plaintextAbsoluteUriRequest);
public static readonly byte[] LiveaspnetPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_liveaspnetRequest, Pipelining)));
diff --git a/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs b/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs
index 481278ae42..a99db7dfe4 100644
--- a/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs
+++ b/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs
@@ -25,13 +25,13 @@ namespace Microsoft.AspNetCore.Testing
writer.Write(payload);
}
- public static void WriteStartStream(this PipeWriter writer, int streamId, Http2HeadersEnumerator headers, byte[] headerEncodingBuffer, bool endStream, Http2Frame frame = null)
+ public static void WriteStartStream(this PipeWriter writer, int streamId, HPackEncoder hpackEncoder, Http2HeadersEnumerator headers, byte[] headerEncodingBuffer, bool endStream, Http2Frame frame = null)
{
frame ??= new Http2Frame();
frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId);
var buffer = headerEncodingBuffer.AsSpan();
- var done = HPackHeaderWriter.BeginEncodeHeaders(headers, buffer, out var length);
+ var done = HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, headers, buffer, out var length);
frame.PayloadLength = length;
if (done)
@@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Testing
{
frame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId);
- done = HPackHeaderWriter.ContinueEncodeHeaders(headers, buffer, out length);
+ done = HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headers, buffer, out length);
frame.PayloadLength = length;
if (done)
diff --git a/src/Servers/Kestrel/stress/.vsconfig b/src/Servers/Kestrel/stress/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Servers/Kestrel/stress/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs
index 9ce2aa0563..afb1e83ca3 100644
--- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs
+++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs
@@ -100,6 +100,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
}
[ConditionalFact]
+ [QuarantinedTest]
public async Task GracefulTurnsAbortiveIfRequestsDoNotFinish()
{
var requestStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs
index 6fd7ce1d8b..d5a40a1127 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs
@@ -14,10 +14,12 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
@@ -30,10 +32,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[QuarantinedTest]
public async Task FlowControl_ParallelStreams_FirstInFirstOutOrder()
{
- // Increase response buffer size so there is no delay in writing to it.
- // We only want to hit flow control back-pressure and not pipe back-pressure.
- // This fixes flakyness https://github.com/dotnet/aspnetcore/pull/19949
- _serviceContext.ServerOptions.Limits.MaxResponseBufferSize = 128 * 1024;
+ // The test will:
+ // 1. Create a stream with a large response. It will use up the connection window and complete.
+ // 2. Create two streams will smaller responses.
+ // 3. Update the connection window one byte at a time.
+ // 4. Read from them in a FIFO order until they are each complete.
var writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -42,8 +45,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// Send headers
await c.Response.Body.FlushAsync();
- // Send large data (3 larger than window size)
- var writeTask = c.Response.Body.WriteAsync(new byte[65538]);
+ var responseBodySize = Convert.ToInt32(c.Request.Headers["ResponseBodySize"]);
+ var writeTask = c.Response.Body.WriteAsync(new byte[responseBodySize]);
// Notify test that write has started
writeTcs.SetResult(null);
@@ -52,12 +55,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await writeTask;
});
- await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+ await StartStreamAsync(1, GetHeaders(responseBodySize: 65535), endStream: true);
// Ensure the stream window size is large enough
- await SendWindowUpdateAsync(streamId: 1, 65538);
+ await SendWindowUpdateAsync(streamId: 1, 65535);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -79,68 +82,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withLength: 16383,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
-
- // 3 byte is remaining on stream 1
-
- writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
-
- await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
- // Ensure the stream window size is large enough
- await SendWindowUpdateAsync(streamId: 3, 65538);
-
- await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
- withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
- withStreamId: 3);
-
- await writeTcs.Task;
-
- await SendWindowUpdateAsync(streamId: 0, 1);
-
- // FIFO means stream 1 returns data first
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 1,
- withFlags: (byte)Http2DataFrameFlags.NONE,
- withStreamId: 1);
-
- await SendWindowUpdateAsync(streamId: 0, 1);
-
- // Stream 3 data
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 1,
- withFlags: (byte)Http2DataFrameFlags.NONE,
- withStreamId: 3);
-
- await SendWindowUpdateAsync(streamId: 0, 1);
-
- // Stream 1 data
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 1,
- withFlags: (byte)Http2DataFrameFlags.NONE,
- withStreamId: 1);
-
- await SendWindowUpdateAsync(streamId: 0, 1);
-
- // Stream 3 data
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 1,
- withFlags: (byte)Http2DataFrameFlags.NONE,
- withStreamId: 3);
-
- await SendWindowUpdateAsync(streamId: 0, 1);
-
- // Stream 1 data
- await ExpectAsync(Http2FrameType.DATA,
- withLength: 1,
- withFlags: (byte)Http2DataFrameFlags.NONE,
- withStreamId: 1);
-
- // Stream 1 ends
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
+ writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ await StartStreamAsync(3, GetHeaders(responseBodySize: 3), endStream: true);
+
+ await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 2,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 3);
+
+ await writeTcs.Task;
+
+ writeTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ await StartStreamAsync(5, GetHeaders(responseBodySize: 3), endStream: true);
+
+ await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 2,
+ withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+ withStreamId: 5);
+
+ await writeTcs.Task;
+
+ await SendWindowUpdateAsync(streamId: 0, 1);
+
+ // FIFO means stream 3 returns data first
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 1,
+ withFlags: (byte)Http2DataFrameFlags.NONE,
+ withStreamId: 3);
+
+ await SendWindowUpdateAsync(streamId: 0, 1);
+
+ // Stream 5 data
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 1,
+ withFlags: (byte)Http2DataFrameFlags.NONE,
+ withStreamId: 5);
+
await SendWindowUpdateAsync(streamId: 0, 1);
// Stream 3 data
@@ -149,7 +133,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 3);
- await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
+ await SendWindowUpdateAsync(streamId: 0, 1);
+
+ // Stream 5 data
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 1,
+ withFlags: (byte)Http2DataFrameFlags.NONE,
+ withStreamId: 5);
+
+ await SendWindowUpdateAsync(streamId: 0, 1);
+
+ // Stream 3 data
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 1,
+ withFlags: (byte)Http2DataFrameFlags.NONE,
+ withStreamId: 3);
+
+ // Stream 3 ends
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 3);
+
+ await SendWindowUpdateAsync(streamId: 0, 1);
+
+ // Stream 5 data
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 1,
+ withFlags: (byte)Http2DataFrameFlags.NONE,
+ withStreamId: 5);
+
+ await ExpectAsync(Http2FrameType.DATA,
+ withLength: 0,
+ withFlags: (byte)Http2DataFrameFlags.END_STREAM,
+ withStreamId: 5);
+
+ await StopConnectionAsync(expectedLastStreamId: 5, ignoreNonGoAwayFrames: false);
+
+ IEnumerable> GetHeaders(int responseBodySize)
+ {
+ foreach (var header in _browserRequestHeaders)
+ {
+ yield return header;
+ }
+
+ yield return new KeyValuePair("ResponseBodySize", responseBodySize.ToString());
+ }
}
[Fact]
@@ -170,7 +199,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -247,7 +276,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, requestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -266,7 +295,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(3, requestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 6,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 3);
@@ -296,7 +325,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
serverTcs.SetResult(null);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -329,7 +358,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloBytes, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -344,7 +373,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(3, _helloBytes, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 2,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
@@ -385,22 +414,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
appDelegateTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+ // Get the in progress stream
+ var stream = _connection._streams[1];
+
appDelegateTcs.TrySetResult(null);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
- // Ping will trigger the stream to be returned to the pool so we can assert it
- await SendPingAsync(Http2PingFrameFlags.NONE);
- await ExpectAsync(Http2FrameType.PING,
- withLength: 8,
- withFlags: (byte)Http2PingFrameFlags.ACK,
- withStreamId: 0);
-
// Stream has been returned to the pool
- Assert.Equal(1, _connection.StreamPool.Count);
+ await PingUntilStreamPooled(expectedCount: 1).DefaultTimeout();
+ Assert.True(_connection.StreamPool.TryPeek(out var pooledStream));
+ Assert.Equal(stream, pooledStream);
appDelegateTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
@@ -411,21 +438,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
appDelegateTcs.TrySetResult(null);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 6,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 3);
- // Ping will trigger the stream to be returned to the pool so we can assert it
- await SendPingAsync(Http2PingFrameFlags.NONE);
- await ExpectAsync(Http2FrameType.PING,
- withLength: 8,
- withFlags: (byte)Http2PingFrameFlags.ACK,
- withStreamId: 0);
-
// Stream was reused and returned to the pool
- Assert.Equal(1, _connection.StreamPool.Count);
+ await PingUntilStreamPooled(expectedCount: 1).DefaultTimeout();
+ Assert.True(_connection.StreamPool.TryPeek(out pooledStream));
+ Assert.Equal(stream, pooledStream);
await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
+
+ async Task PingUntilStreamPooled(int expectedCount)
+ {
+ do
+ {
+ // Ping will trigger the stream to be returned to the pool so we can assert it
+ await SendPingAsync(Http2PingFrameFlags.NONE);
+ await ExpectAsync(Http2FrameType.PING,
+ withLength: 8,
+ withFlags: (byte)Http2PingFrameFlags.ACK,
+ withStreamId: 0);
+ } while (_connection.StreamPool.Count != expectedCount);
+ }
}
[Fact]
@@ -436,7 +471,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await InitializeConnectionAsync(async context =>
{
- await serverTcs.Task;
+ await serverTcs.Task.DefaultTimeout();
await context.Response.WriteAsync("Content");
throw new InvalidOperationException("Put the stream into an invalid state by throwing after writing to response.");
@@ -448,7 +483,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
serverTcs.SetResult(null);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -457,20 +492,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withStreamId: 1);
await WaitForStreamErrorAsync(1, Http2ErrorCode.INTERNAL_ERROR, null);
- // Ping will trigger the stream to be returned to the pool so we can assert it
- await SendPingAsync(Http2PingFrameFlags.NONE);
- await ExpectAsync(Http2FrameType.PING,
- withLength: 8,
- withFlags: (byte)Http2PingFrameFlags.ACK,
- withStreamId: 0);
+ await PingUntilStreamDisposed(stream).DefaultTimeout();
// Stream is not returned to the pool
Assert.Equal(0, _connection.StreamPool.Count);
var output = (Http2OutputProducer)stream.Output;
- await output._dataWriteProcessingTask;
+ await output._dataWriteProcessingTask.DefaultTimeout();
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+ async Task PingUntilStreamDisposed(Http2Stream stream)
+ {
+ var output = (Http2OutputProducer)stream.Output;
+
+ do
+ {
+ // Ping will trigger the stream to be returned to the pool so we can assert it
+ await SendPingAsync(Http2PingFrameFlags.NONE);
+ await ExpectAsync(Http2FrameType.PING,
+ withLength: 8,
+ withFlags: (byte)Http2PingFrameFlags.ACK,
+ withStreamId: 0);
+ } while (!output._disposed);
+ }
}
[Fact]
@@ -566,7 +611,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[length], endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
// The client's settings is still defaulted to Http2PeerSettings.MinAllowedMaxFrameSize so the echo response will come back in two separate frames
@@ -595,7 +640,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloWorldBytes, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -621,7 +666,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _maxData, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -664,7 +709,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -785,7 +830,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _noData, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -813,7 +858,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloBytes, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var stream1DataFrame1 = await ExpectAsync(Http2FrameType.DATA,
@@ -824,7 +869,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(3, _helloBytes, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 2,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
var stream3DataFrame1 = await ExpectAsync(Http2FrameType.DATA,
@@ -893,7 +938,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -945,7 +990,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withStreamId: 0);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 2,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
@@ -1023,7 +1068,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
stream3ReadFinished.TrySetResult(null);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
@@ -1038,7 +1083,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
stream1ReadFinished.TrySetResult(null);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 2,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -1065,7 +1110,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataWithPaddingAsync(1, _helloWorldBytes, padLength, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -1110,7 +1155,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -1169,7 +1214,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1238,7 +1283,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _maxData, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -1413,7 +1458,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _postRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1455,7 +1500,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 3);
@@ -1505,7 +1550,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 3);
@@ -1609,7 +1654,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -1630,7 +1675,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 6,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 3);
@@ -1664,7 +1709,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1679,7 +1724,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1699,7 +1744,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendHeadersWithPaddingAsync(1, _browserRequestHeaders, padLength, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1716,7 +1761,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendHeadersWithPriorityAsync(1, _browserRequestHeaders, priority: 42, streamDependency: 0, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1736,7 +1781,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendHeadersWithPaddingAndPriorityAsync(1, _browserRequestHeaders, padLength, priority: 42, streamDependency: 0, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1762,7 +1807,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// The second stream should end first, since the first one is waiting for the request body.
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 3);
@@ -1774,7 +1819,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, _requestTrailers);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 6,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1808,7 +1853,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloBytes, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -1858,17 +1903,163 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
finishSecondRequest.TrySetResult(null);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 3);
finishFirstRequest.TrySetResult(null);
+ await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 6,
+ withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+ withStreamId: 1);
+
+ await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task HEADERS_HeaderTableSizeLimitZero_Received_DynamicTableUpdate()
+ {
+ _serviceContext.ServerOptions.Limits.Http2.HeaderTableSize = 0;
+
+ await InitializeConnectionAsync(_noopApplication, expectedSettingsCount: 4);
+
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+
+ _hpackEncoder.UpdateMaxHeaderTableSize(0);
+
+ var headerFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 38,
+ withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+ withStreamId: 1);
+
+ const byte DynamicTableSizeUpdateMask = 0xe0;
+
+ var integerDecoder = new IntegerDecoder();
+ Assert.True(integerDecoder.BeginTryDecode((byte)(headerFrame.Payload.Span[0] & ~DynamicTableSizeUpdateMask), prefixLength: 5, out var result));
+
+ // Dynamic table update from the server
+ Assert.Equal(0, result);
+
+ await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
+
+ await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 37,
+ withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+ withStreamId: 3);
+
+ await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task HEADERS_ResponseSetsIgnoreIndexAndNeverIndexValues_HeadersParsed()
+ {
+ await InitializeConnectionAsync(c =>
+ {
+ c.Response.ContentLength = 0;
+ c.Response.Headers[HeaderNames.SetCookie] = "SetCookie!";
+ c.Response.Headers[HeaderNames.ContentDisposition] = "ContentDisposition!";
+
+ return Task.CompletedTask;
+ });
+
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+
+ var frame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 90,
+ withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+ withStreamId: 1);
+
+ var handler = new TestHttpHeadersHandler();
+
+ var hpackDecoder = new HPackDecoder();
+ hpackDecoder.Decode(new ReadOnlySequence(frame.Payload), endHeaders: true, handler);
+ hpackDecoder.CompleteDecode();
+
+ Assert.Equal("200", handler.Headers[":status"]);
+ Assert.Equal("SetCookie!", handler.Headers[HeaderNames.SetCookie]);
+ Assert.Equal("ContentDisposition!", handler.Headers[HeaderNames.ContentDisposition]);
+ Assert.Equal("0", handler.Headers[HeaderNames.ContentLength]);
+
+ await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
+
+ frame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 60,
+ withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+ withStreamId: 3);
+
+ handler = new TestHttpHeadersHandler();
+
+ hpackDecoder.Decode(new ReadOnlySequence(frame.Payload), endHeaders: true, handler);
+ hpackDecoder.CompleteDecode();
+
+ Assert.Equal("200", handler.Headers[":status"]);
+ Assert.Equal("SetCookie!", handler.Headers[HeaderNames.SetCookie]);
+ Assert.Equal("ContentDisposition!", handler.Headers[HeaderNames.ContentDisposition]);
+ Assert.Equal("0", handler.Headers[HeaderNames.ContentLength]);
+
+ await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
+ }
+
+ private class TestHttpHeadersHandler : IHttpHeadersHandler
+ {
+ public readonly Dictionary Headers = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ public void OnHeader(ReadOnlySpan name, ReadOnlySpan value)
+ {
+ var nameString = Encoding.ASCII.GetString(name);
+ var valueString = Encoding.ASCII.GetString(value);
+
+ if (Headers.TryGetValue(nameString, out var values))
+ {
+ var l = values.ToList();
+ l.Add(valueString);
+
+ Headers[nameString] = new StringValues(l.ToArray());
+ }
+ else
+ {
+ Headers[nameString] = new StringValues(valueString);
+ }
+ }
+
+ public void OnHeadersComplete(bool endStream)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void OnStaticIndexedHeader(int index)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void OnStaticIndexedHeader(int index, ReadOnlySpan value)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [Fact]
+ public async Task HEADERS_DisableDynamicHeaderCompression_HeadersNotCompressed()
+ {
+ _serviceContext.ServerOptions.AllowResponseHeaderCompression = false;
+
+ await InitializeConnectionAsync(_noopApplication);
+
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
+ await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
+
+ await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 37,
+ withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+ withStreamId: 3);
+
await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
}
@@ -1891,7 +2082,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
requestBlocker.SetResult(0);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1934,7 +2125,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1977,7 +2168,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 3);
@@ -2201,7 +2392,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -2346,7 +2537,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -2484,7 +2675,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -2509,7 +2700,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// The headers, but not the data for stream 3, can be sent prior to any window updates.
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 2,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
@@ -2588,12 +2779,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
});
- async Task VerifyStreamBackpressure(int streamId)
+ async Task VerifyStreamBackpressure(int streamId, int headersLength)
{
await StartStreamAsync(streamId, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: headersLength,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: streamId);
@@ -2606,9 +2797,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.False(writeTasks[streamId].IsCompleted);
}
- await VerifyStreamBackpressure(1);
- await VerifyStreamBackpressure(3);
- await VerifyStreamBackpressure(5);
+ await VerifyStreamBackpressure(1, 32);
+ await VerifyStreamBackpressure(3, 2);
+ await VerifyStreamBackpressure(5, 2);
await SendRstStreamAsync(1);
await writeTasks[1].DefaultTimeout();
@@ -2886,6 +3077,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
CreateConnection();
+ _connection.ServerSettings.HeaderTableSize = 0;
_connection.ServerSettings.MaxConcurrentStreams = 1;
_connection.ServerSettings.MaxHeaderListSize = 4 * 1024;
_connection.ServerSettings.InitialWindowSize = 1024 * 1024 * 10;
@@ -2896,23 +3088,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendSettingsAsync();
var frame = await ExpectAsync(Http2FrameType.SETTINGS,
- withLength: Http2FrameReader.SettingSize * 3,
+ withLength: Http2FrameReader.SettingSize * 4,
withFlags: 0,
withStreamId: 0);
// Only non protocol defaults are sent
var settings = Http2FrameReader.ReadSettings(frame.PayloadSequence);
- Assert.Equal(3, settings.Count);
+ Assert.Equal(4, settings.Count);
var setting = settings[0];
+ Assert.Equal(Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE, setting.Parameter);
+ Assert.Equal(0u, setting.Value);
+
+ setting = settings[1];
Assert.Equal(Http2SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS, setting.Parameter);
Assert.Equal(1u, setting.Value);
- setting = settings[1];
+ setting = settings[2];
Assert.Equal(Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE, setting.Parameter);
Assert.Equal(1024 * 1024 * 10u, setting.Value);
- setting = settings[2];
+ setting = settings[3];
Assert.Equal(Http2SettingsParameter.SETTINGS_MAX_HEADER_LIST_SIZE, setting.Parameter);
Assert.Equal(4 * 1024u, setting.Value);
@@ -3073,7 +3269,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_connection.ServerSettings.MaxFrameSize = Http2PeerSettings.MaxAllowedMaxFrameSize;
// This includes the default response headers such as :status, etc
- var defaultResponseHeaderLength = 33;
+ var defaultResponseHeaderLength = 32;
var headerValueLength = Http2PeerSettings.MinAllowedMaxFrameSize;
// First byte is always 0
// Second byte is the length of header name which is 1
@@ -3143,7 +3339,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -3177,7 +3373,56 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: (byte)Http2SettingsFrameFlags.ACK,
withStreamId: 0);
- await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false);
+ // Start request
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+
+ var headerFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 36,
+ withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+ withStreamId: 1);
+
+ // Headers start with :status = 200
+ Assert.Equal(0x88, headerFrame.Payload.Span[0]);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
+ [Fact]
+ public async Task SETTINGS_Received_WithLargeHeaderTableSizeLimit_ChangesHeaderTableSize()
+ {
+ _serviceContext.ServerOptions.Limits.Http2.HeaderTableSize = 40000;
+
+ await InitializeConnectionAsync(_noopApplication, expectedSettingsCount: 4);
+
+ // Update client settings
+ _clientSettings.HeaderTableSize = 65536; // Chrome's default, larger than the 4kb spec default
+ await SendSettingsAsync();
+
+ // ACK
+ await ExpectAsync(Http2FrameType.SETTINGS,
+ withLength: 0,
+ withFlags: (byte)Http2SettingsFrameFlags.ACK,
+ withStreamId: 0);
+
+ // Start request
+ await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
+
+ var headerFrame = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 40,
+ withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+ withStreamId: 1);
+
+ const byte DynamicTableSizeUpdateMask = 0xe0;
+
+ var integerDecoder = new IntegerDecoder();
+ Assert.False(integerDecoder.BeginTryDecode((byte)(headerFrame.Payload.Span[0] & ~DynamicTableSizeUpdateMask), prefixLength: 5, out _));
+ Assert.False(integerDecoder.TryDecode(headerFrame.Payload.Span[1], out _));
+ Assert.False(integerDecoder.TryDecode(headerFrame.Payload.Span[2], out _));
+ Assert.True(integerDecoder.TryDecode(headerFrame.Payload.Span[3], out var result));
+
+ Assert.Equal(40000, result);
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
}
[Fact]
@@ -3292,7 +3537,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloBytes, true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -3305,7 +3550,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withStreamId: 1);
await SendDataAsync(3, _helloBytes, true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 2,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
@@ -3378,7 +3623,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -3401,13 +3646,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// The headers, but not the data for the stream, can still be sent.
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 2,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await StartStreamAsync(5, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 2,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 5);
@@ -3466,12 +3711,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
});
- async Task VerifyStreamBackpressure(int streamId)
+ async Task VerifyStreamBackpressure(int streamId, int headersLength)
{
await StartStreamAsync(streamId, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: headersLength,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: streamId);
@@ -3484,9 +3729,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.False(writeTasks[streamId].IsCompleted);
}
- await VerifyStreamBackpressure(1);
- await VerifyStreamBackpressure(3);
- await VerifyStreamBackpressure(5);
+ await VerifyStreamBackpressure(1, 32);
+ await VerifyStreamBackpressure(3, 2);
+ await VerifyStreamBackpressure(5, 2);
// Close all pipes and wait for response to drain
_pair.Application.Output.Complete();
@@ -3704,7 +3949,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -3761,7 +4006,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloWorldBytes, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -3800,7 +4045,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloWorldBytes, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -3852,7 +4097,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _twoContinuationsRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -3879,7 +4124,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// The second stream should end first, since the first one is waiting for the request body.
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 3);
@@ -3902,7 +4147,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendContinuationAsync(1, Http2ContinuationFrameFlags.END_HEADERS, trailers);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 6,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -4000,7 +4245,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendEmptyContinuationFrameAsync(1, Http2ContinuationFrameFlags.END_HEADERS);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -4015,7 +4260,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 12343,
+ withLength: 12342,
withFlags: (byte)Http2HeadersFrameFlags.END_STREAM,
withStreamId: 1);
var continuationFrame1 = await ExpectAsync(Http2FrameType.CONTINUATION,
@@ -4174,7 +4419,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -4224,7 +4469,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloBytes, true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -4257,8 +4502,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(3, _browserRequestHeaders, endStream: false);
await SendDataAsync(1, _helloBytes, true);
- await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ var f = await ExpectAsync(Http2FrameType.HEADERS,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -4271,7 +4516,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withStreamId: 1);
await SendDataAsync(3, _helloBytes, true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 2,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
@@ -4361,7 +4606,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
index a59207ccda..efed04f302 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
@@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 52,
+ withLength: 51,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 53,
+ withLength: 52,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 57,
+ withLength: 56,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 58,
+ withLength: 57,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 100,
+ withLength: 99,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -235,7 +235,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -326,7 +326,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -355,7 +355,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 47,
+ withLength: 46,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -386,7 +386,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 47,
+ withLength: 46,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -417,7 +417,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 47,
+ withLength: 46,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -448,7 +448,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 47,
+ withLength: 46,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -570,7 +570,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[12], endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -611,7 +611,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[12], endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -655,7 +655,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[8], endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -698,7 +698,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[8], endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -751,7 +751,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[8], endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -983,7 +983,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[12], endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1015,7 +1015,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 38,
+ withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.RST_STREAM,
@@ -1054,7 +1054,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 38,
+ withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -1092,7 +1092,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1125,7 +1125,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -1160,7 +1160,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -1198,7 +1198,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -1236,7 +1236,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -1276,7 +1276,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -1323,7 +1323,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -1361,7 +1361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -1397,7 +1397,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -1441,7 +1441,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 38,
+ withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -1475,7 +1475,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -1508,7 +1508,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 38,
+ withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -1552,7 +1552,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[12], endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1591,7 +1591,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: false);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 41,
+ withLength: 40,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1634,7 +1634,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[12], endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1674,7 +1674,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[6], endStream: false);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 41,
+ withLength: 40,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1733,7 +1733,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[6], endStream: false);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 41,
+ withLength: 40,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1788,7 +1788,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, new byte[12], endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -1814,7 +1814,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -1852,7 +1852,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_STREAM | Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
@@ -1883,7 +1883,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
var headersFrame1 = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var trailersFrame1 = await ExpectAsync(Http2FrameType.HEADERS,
@@ -1894,12 +1894,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
var headersFrame2 = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 6,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 3);
var trailersFrame2 = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 25,
+ withLength: 1,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 3);
@@ -1930,7 +1930,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -1980,7 +1980,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -2039,7 +2039,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -2074,7 +2074,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -2124,7 +2124,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true).DefaultTimeout();
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1).DefaultTimeout();
@@ -2189,7 +2189,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -2235,7 +2235,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -2269,7 +2269,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -2532,7 +2532,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -2623,7 +2623,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -2671,7 +2671,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// Just the StatusCode gets written before aborting in the continuation frame
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.NONE,
withStreamId: 1);
@@ -2700,7 +2700,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -2743,7 +2743,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -2789,7 +2789,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -2835,7 +2835,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -2884,7 +2884,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -2937,7 +2937,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -2987,7 +2987,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -3037,7 +3037,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -3080,7 +3080,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -3126,7 +3126,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
var dataFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -3168,7 +3168,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -3213,7 +3213,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -3279,7 +3279,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 38,
+ withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -3325,7 +3325,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 38,
+ withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -3361,7 +3361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 38,
+ withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -3413,7 +3413,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 38,
+ withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -3465,7 +3465,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -3498,7 +3498,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// Don't receive content length because we called WriteAsync which caused an invalid response
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS | (byte)Http2HeadersFrameFlags.END_STREAM,
withStreamId: 1);
@@ -3531,7 +3531,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -3583,7 +3583,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -3639,7 +3639,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
@@ -3705,7 +3705,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -3761,7 +3761,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -3826,7 +3826,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -3885,7 +3885,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -3941,7 +3941,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -4003,7 +4003,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -4077,7 +4077,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -4153,7 +4153,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 38,
+ withLength: 37,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -4224,7 +4224,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 38,
+ withLength: 37,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -4296,7 +4296,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -4380,7 +4380,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: false);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -4461,7 +4461,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -4548,7 +4548,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, headers, endStream: false);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
withStreamId: 1);
var bodyFrame = await ExpectAsync(Http2FrameType.DATA,
@@ -4608,7 +4608,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(1, LatinHeaderData, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
index f80e5ad386..03b5e277ed 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
@@ -121,6 +121,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
internal readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();
internal readonly HPackDecoder _hpackDecoder;
+ internal readonly HPackEncoder _hpackEncoder;
private readonly byte[] _headerEncodingBuffer = new byte[Http2PeerSettings.MinAllowedMaxFrameSize];
internal readonly TimeoutControl _timeoutControl;
@@ -165,6 +166,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public Http2TestBase()
{
_hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize);
+ _hpackEncoder = new HPackEncoder();
_timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object);
_mockTimeoutControl = new Mock(_timeoutControl) { CallBase = true };
@@ -501,7 +503,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
_runningStreams[streamId] = tcs;
- writableBuffer.WriteStartStream(streamId, GetHeadersEnumerator(headers), _headerEncodingBuffer, endStream);
+ writableBuffer.WriteStartStream(streamId, _hpackEncoder, GetHeadersEnumerator(headers), _headerEncodingBuffer, endStream);
return FlushAsync(writableBuffer);
}
@@ -541,7 +543,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
extendedHeader[0] = padLength;
var payload = buffer.Slice(extendedHeaderLength, buffer.Length - padLength - extendedHeaderLength);
- HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out var length);
+ HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, GetHeadersEnumerator(headers), payload, out var length);
var padding = buffer.Slice(extendedHeaderLength + length, padLength);
padding.Fill(0);
@@ -584,7 +586,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
extendedHeader[4] = priority;
var payload = buffer.Slice(extendedHeaderLength);
- HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out var length);
+ HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, GetHeadersEnumerator(headers), payload, out var length);
frame.PayloadLength = extendedHeaderLength + length;
@@ -631,7 +633,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
extendedHeader[5] = priority;
var payload = buffer.Slice(extendedHeaderLength, buffer.Length - padLength - extendedHeaderLength);
- HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out var length);
+ HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, GetHeadersEnumerator(headers), payload, out var length);
var padding = buffer.Slice(extendedHeaderLength + length, padLength);
padding.Fill(0);
@@ -745,7 +747,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
frame.PrepareHeaders(flags, streamId);
var buffer = _headerEncodingBuffer.AsMemory();
- var done = HPackHeaderWriter.BeginEncodeHeaders(headersEnumerator, buffer.Span, out var length);
+ var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, headersEnumerator, buffer.Span, out var length);
frame.PayloadLength = length;
Http2FrameWriter.WriteHeader(frame, outputWriter);
@@ -815,7 +817,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
frame.PrepareContinuation(flags, streamId);
var buffer = _headerEncodingBuffer.AsMemory();
- var done = HPackHeaderWriter.ContinueEncodeHeaders(headersEnumerator, buffer.Span, out var length);
+ var done = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, headersEnumerator, buffer.Span, out var length);
frame.PayloadLength = length;
Http2FrameWriter.WriteHeader(frame, outputWriter);
@@ -843,7 +845,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
frame.PrepareContinuation(flags, streamId);
var buffer = _headerEncodingBuffer.AsMemory();
- var done = HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), buffer.Span, out var length);
+ var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, GetHeadersEnumerator(headers), buffer.Span, out var length);
frame.PayloadLength = length;
Http2FrameWriter.WriteHeader(frame, outputWriter);
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs
index 087664a7e0..4432e85dc6 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs
@@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.RequestHeaders), Times.Once);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 36,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
@@ -283,7 +283,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloWorldBytes, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -336,7 +336,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _maxData, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -390,7 +390,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloWorldBytes, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -445,7 +445,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _maxData, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -501,7 +501,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _maxData, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
@@ -513,7 +513,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(3, _maxData, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
@@ -567,7 +567,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloWorldBytes, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -616,7 +616,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _maxData, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -669,7 +669,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _maxData, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -682,7 +682,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(3, _maxData, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 2,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
@@ -738,7 +738,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _maxData, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -756,7 +756,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(3, _maxData, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 2,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
@@ -813,7 +813,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(1, _helloWorldBytes, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
@@ -885,7 +885,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await SendDataAsync(3, _helloWorldBytes, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 33,
+ withLength: 32,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
@@ -902,7 +902,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
backpressureTcs.SetResult(null);
await ExpectAsync(Http2FrameType.HEADERS,
- withLength: 37,
+ withLength: 6,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1);
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs
index dafbd6eca7..178a464b44 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs
@@ -285,6 +285,264 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
}
+ [Fact]
+ public async Task ExecutionContextMutationsOfValueTypeDoNotLeakAcrossRequestsOnSameConnection()
+ {
+ var local = new AsyncLocal();
+
+ // It's important this method isn't async as that will revert the ExecutionContext
+ Task ExecuteApplication(HttpContext context)
+ {
+ var value = local.Value;
+ Assert.Equal(0, value);
+
+ context.Response.OnStarting(() =>
+ {
+ local.Value++;
+ return Task.CompletedTask;
+ });
+
+ context.Response.OnCompleted(() =>
+ {
+ local.Value++;
+ return Task.CompletedTask;
+ });
+
+ local.Value++;
+ context.Response.ContentLength = 1;
+ return context.Response.WriteAsync($"{value}");
+ }
+
+ var testContext = new TestServiceContext(LoggerFactory);
+
+ await using var server = new TestServer(ExecuteApplication, testContext);
+ await TestAsyncLocalValues(testContext, server);
+ }
+
+ [Fact]
+ public async Task ExecutionContextMutationsOfReferenceTypeDoNotLeakAcrossRequestsOnSameConnection()
+ {
+ var local = new AsyncLocal();
+
+ // It's important this method isn't async as that will revert the ExecutionContext
+ Task ExecuteApplication(HttpContext context)
+ {
+ Assert.Null(local.Value);
+ local.Value = new IntAsClass();
+
+ var value = local.Value.Value;
+ Assert.Equal(0, value);
+
+ context.Response.OnStarting(() =>
+ {
+ local.Value.Value++;
+ return Task.CompletedTask;
+ });
+
+ context.Response.OnCompleted(() =>
+ {
+ local.Value.Value++;
+ return Task.CompletedTask;
+ });
+
+ local.Value.Value++;
+ context.Response.ContentLength = 1;
+ return context.Response.WriteAsync($"{value}");
+ }
+
+ var testContext = new TestServiceContext(LoggerFactory);
+
+ await using var server = new TestServer(ExecuteApplication, testContext);
+ await TestAsyncLocalValues(testContext, server);
+ }
+
+#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
+ [Fact]
+ public async Task ExecutionContextMutationsDoNotLeakAcrossAwaits()
+ {
+ var local = new AsyncLocal();
+
+ // It's important this method isn't async as that will revert the ExecutionContext
+ Task ExecuteApplication(HttpContext context)
+ {
+ var value = local.Value;
+ Assert.Equal(0, value);
+
+ context.Response.OnStarting(async () =>
+ {
+ local.Value++;
+ Assert.Equal(1, local.Value);
+ });
+
+ context.Response.OnCompleted(async () =>
+ {
+ local.Value++;
+ Assert.Equal(1, local.Value);
+ });
+
+ context.Response.ContentLength = 1;
+ return context.Response.WriteAsync($"{value}");
+ }
+
+ var testContext = new TestServiceContext(LoggerFactory);
+
+ await using var server = new TestServer(ExecuteApplication, testContext);
+ await TestAsyncLocalValues(testContext, server);
+ }
+
+ [Fact]
+ public async Task ExecutionContextMutationsOfValueTypeFlowIntoButNotOutOfAsyncEvents()
+ {
+ var local = new AsyncLocal();
+
+ async Task ExecuteApplication(HttpContext context)
+ {
+ var value = local.Value;
+ Assert.Equal(0, value);
+
+ context.Response.OnStarting(async () =>
+ {
+ local.Value++;
+ Assert.Equal(2, local.Value);
+ });
+
+ context.Response.OnCompleted(async () =>
+ {
+ local.Value++;
+ Assert.Equal(2, local.Value);
+ });
+
+ local.Value++;
+ Assert.Equal(1, local.Value);
+
+ context.Response.ContentLength = 1;
+ await context.Response.WriteAsync($"{value}");
+
+ local.Value++;
+ Assert.Equal(2, local.Value);
+ }
+
+ var testContext = new TestServiceContext(LoggerFactory);
+
+ await using var server = new TestServer(ExecuteApplication, testContext);
+ await TestAsyncLocalValues(testContext, server);
+ }
+
+ [Fact]
+ public async Task ExecutionContextMutationsOfReferenceTypeFlowThroughAsyncEvents()
+ {
+ var local = new AsyncLocal();
+
+ async Task ExecuteApplication(HttpContext context)
+ {
+ Assert.Null(local.Value);
+ local.Value = new IntAsClass();
+
+ var value = local.Value.Value;
+ Assert.Equal(0, value); // Start
+
+ context.Response.OnStarting(async () =>
+ {
+ local.Value.Value++;
+ Assert.Equal(2, local.Value.Value); // Second
+ });
+
+ context.Response.OnCompleted(async () =>
+ {
+ local.Value.Value++;
+ Assert.Equal(4, local.Value.Value); // Fourth
+ });
+
+ local.Value.Value++;
+ Assert.Equal(1, local.Value.Value); // First
+
+ context.Response.ContentLength = 1;
+ await context.Response.WriteAsync($"{value}");
+
+ local.Value.Value++;
+ Assert.Equal(3, local.Value.Value); // Third
+ }
+
+ var testContext = new TestServiceContext(LoggerFactory);
+
+ await using var server = new TestServer(ExecuteApplication, testContext);
+ await TestAsyncLocalValues(testContext, server);
+ }
+#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
+
+ [Fact]
+ public async Task ExecutionContextMutationsOfValueTypeFlowIntoButNotOutOfNonAsyncEvents()
+ {
+ var local = new AsyncLocal();
+
+ async Task ExecuteApplication(HttpContext context)
+ {
+ var value = local.Value;
+ Assert.Equal(0, value);
+
+ context.Response.OnStarting(() =>
+ {
+ local.Value++;
+ Assert.Equal(2, local.Value);
+
+ return Task.CompletedTask;
+ });
+
+ context.Response.OnCompleted(() =>
+ {
+ local.Value++;
+ Assert.Equal(2, local.Value);
+
+ return Task.CompletedTask;
+ });
+
+ local.Value++;
+ Assert.Equal(1, local.Value);
+
+ context.Response.ContentLength = 1;
+ await context.Response.WriteAsync($"{value}");
+
+ local.Value++;
+ Assert.Equal(2, local.Value);
+ }
+
+ var testContext = new TestServiceContext(LoggerFactory);
+
+ await using var server = new TestServer(ExecuteApplication, testContext);
+ await TestAsyncLocalValues(testContext, server);
+ }
+
+ private static async Task TestAsyncLocalValues(TestServiceContext testContext, TestServer server)
+ {
+ using var connection = server.CreateConnection();
+
+ await connection.Send(
+ "GET / HTTP/1.1",
+ "Host:",
+ "",
+ "");
+
+ await connection.Receive(
+ "HTTP/1.1 200 OK",
+ $"Date: {testContext.DateHeaderValue}",
+ "Content-Length: 1",
+ "",
+ "0");
+
+ await connection.Send(
+ "GET / HTTP/1.1",
+ "Host:",
+ "",
+ "");
+
+ await connection.Receive(
+ "HTTP/1.1 200 OK",
+ $"Date: {testContext.DateHeaderValue}",
+ "Content-Length: 1",
+ "",
+ "0");
+ }
+
[Fact]
public async Task AppCanSetTraceIdentifier()
{
@@ -1803,5 +2061,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
}
public static TheoryData HostHeaderData => HttpParsingData.HostHeaderData;
+
+ private class IntAsClass
+ {
+ public int Value;
+ }
}
}
diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs
index 18faacc44d..f53e083bc3 100644
--- a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs
+++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs
@@ -1118,7 +1118,7 @@ namespace Interop.FunctionalTests
Assert.Equal(oneKbString + i, response.Headers.GetValues("header" + i).Single());
}
- Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("sending HEADERS frame for stream ID 1 with length 15612 and flags END_STREAM")));
+ Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("sending HEADERS frame for stream ID 1 with length 15610 and flags END_STREAM")));
Assert.Equal(2, TestSink.Writes.Where(context => context.Message.Contains("sending CONTINUATION frame for stream ID 1 with length 15585 and flags NONE")).Count());
Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("sending CONTINUATION frame for stream ID 1 with length 14546 and flags END_HEADERS")));
diff --git a/src/Servers/test/FunctionalTests/.vsconfig b/src/Servers/test/FunctionalTests/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Servers/test/FunctionalTests/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Shared/.vsconfig b/src/Shared/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Shared/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Shared/Hpack/EncoderHeaderEntry.cs b/src/Shared/Hpack/EncoderHeaderEntry.cs
new file mode 100644
index 0000000000..75a0aebde2
--- /dev/null
+++ b/src/Shared/Hpack/EncoderHeaderEntry.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+
+namespace System.Net.Http.HPack
+{
+ [DebuggerDisplay("Name = {Name} Value = {Value}")]
+ internal class EncoderHeaderEntry
+ {
+ // Header name and value
+ public string Name;
+ public string Value;
+
+ // Chained list of headers in the same bucket
+ public EncoderHeaderEntry Next;
+ public int Hash;
+
+ // Compute dynamic table index
+ public int Index;
+
+ // Doubly linked list
+ public EncoderHeaderEntry Before;
+ public EncoderHeaderEntry After;
+
+ ///
+ /// Initialize header values. An entry will be reinitialized when reused.
+ ///
+ public void Initialize(int hash, string name, string value, int index, EncoderHeaderEntry next)
+ {
+ Debug.Assert(name != null);
+ Debug.Assert(value != null);
+
+ Name = name;
+ Value = value;
+ Index = index;
+ Hash = hash;
+ Next = next;
+ }
+
+ public uint CalculateSize()
+ {
+ return (uint)HeaderField.GetLength(Name.Length, Value.Length);
+ }
+
+ ///
+ /// Remove entry from the linked list and reset header values.
+ ///
+ public void Remove()
+ {
+ Before.After = After;
+ After.Before = Before;
+ Before = null;
+ After = null;
+ Next = null;
+ Hash = 0;
+ Name = null;
+ Value = null;
+ }
+
+ ///
+ /// Add before an entry in the linked list.
+ ///
+ public void AddBefore(EncoderHeaderEntry existingEntry)
+ {
+ After = existingEntry;
+ Before = existingEntry.Before;
+ Before.After = this;
+ After.Before = this;
+ }
+ }
+}
diff --git a/src/Shared/Hpack/HPackEncoder.Dynamic.cs b/src/Shared/Hpack/HPackEncoder.Dynamic.cs
new file mode 100644
index 0000000000..f8e7f4c93d
--- /dev/null
+++ b/src/Shared/Hpack/HPackEncoder.Dynamic.cs
@@ -0,0 +1,295 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+using System.Diagnostics;
+
+namespace System.Net.Http.HPack
+{
+ internal partial class HPackEncoder
+ {
+ public const int DefaultHeaderTableSize = 4096;
+
+ // Internal for testing
+ internal readonly EncoderHeaderEntry Head;
+
+ private readonly bool _allowDynamicCompression;
+ private readonly EncoderHeaderEntry[] _headerBuckets;
+ private readonly byte _hashMask;
+ private uint _headerTableSize;
+ private uint _maxHeaderTableSize;
+ private bool _pendingTableSizeUpdate;
+ private EncoderHeaderEntry? _removed;
+
+ public HPackEncoder(bool allowDynamicCompression = true, uint maxHeaderTableSize = DefaultHeaderTableSize)
+ {
+ _allowDynamicCompression = allowDynamicCompression;
+ _maxHeaderTableSize = maxHeaderTableSize;
+ Head = new EncoderHeaderEntry();
+ Head.Initialize(-1, string.Empty, string.Empty, int.MaxValue, null);
+ // Bucket count balances memory usage and the expected low number of headers (constrained by the header table size).
+ // Performance with different bucket counts hasn't been measured in detail.
+ _headerBuckets = new EncoderHeaderEntry[16];
+ _hashMask = (byte)(_headerBuckets.Length - 1);
+ Head.Before = Head.After = Head;
+ }
+
+ public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize)
+ {
+ if (_maxHeaderTableSize != maxHeaderTableSize)
+ {
+ _maxHeaderTableSize = maxHeaderTableSize;
+
+ // Dynamic table size update will be written next HEADERS frame
+ _pendingTableSizeUpdate = true;
+
+ // Check capacity and remove entries that exceed the new capacity
+ EnsureCapacity(0);
+ }
+ }
+
+ public bool EnsureDynamicTableSizeUpdate(Span buffer, out int length)
+ {
+ // Check if there is a table size update that should be encoded
+ if (_pendingTableSizeUpdate)
+ {
+ bool success = EncodeDynamicTableSizeUpdate((int)_maxHeaderTableSize, buffer, out length);
+ _pendingTableSizeUpdate = false;
+ return success;
+ }
+
+ length = 0;
+ return true;
+ }
+
+ public bool EncodeHeader(Span buffer, int staticTableIndex, HeaderEncodingHint encodingHint, string name, string value, out int bytesWritten)
+ {
+ Debug.Assert(!_pendingTableSizeUpdate, "Dynamic table size update should be encoded before headers.");
+
+ // Never index sensitive value.
+ if (encodingHint == HeaderEncodingHint.NeverIndex)
+ {
+ int index = ResolveDynamicTableIndex(staticTableIndex, name);
+
+ return index == -1
+ ? EncodeLiteralHeaderFieldNeverIndexingNewName(name, value, buffer, out bytesWritten)
+ : EncodeLiteralHeaderFieldNeverIndexing(index, value, buffer, out bytesWritten);
+ }
+
+ // No dynamic table. Only use the static table.
+ if (!_allowDynamicCompression || _maxHeaderTableSize == 0 || encodingHint == HeaderEncodingHint.IgnoreIndex)
+ {
+ return staticTableIndex == -1
+ ? EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten)
+ : EncodeLiteralHeaderFieldWithoutIndexing(staticTableIndex, value, buffer, out bytesWritten);
+ }
+
+ // Header is greater than the maximum table size.
+ // Don't attempt to add dynamic header as all existing dynamic headers will be removed.
+ if (HeaderField.GetLength(name.Length, value.Length) > _maxHeaderTableSize)
+ {
+ int index = ResolveDynamicTableIndex(staticTableIndex, name);
+
+ return index == -1
+ ? EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten)
+ : EncodeLiteralHeaderFieldWithoutIndexing(index, value, buffer, out bytesWritten);
+ }
+
+ return EncodeDynamicHeader(buffer, staticTableIndex, name, value, out bytesWritten);
+ }
+
+ private int ResolveDynamicTableIndex(int staticTableIndex, string name)
+ {
+ if (staticTableIndex != -1)
+ {
+ // Prefer static table index.
+ return staticTableIndex;
+ }
+
+ return CalculateDynamicTableIndex(name);
+ }
+
+ private bool EncodeDynamicHeader(Span buffer, int staticTableIndex, string name, string value, out int bytesWritten)
+ {
+ EncoderHeaderEntry? headerField = GetEntry(name, value);
+ if (headerField != null)
+ {
+ // Already exists in dynamic table. Write index.
+ int index = CalculateDynamicTableIndex(headerField.Index);
+ return EncodeIndexedHeaderField(index, buffer, out bytesWritten);
+ }
+ else
+ {
+ // Doesn't exist in dynamic table. Add new entry to dynamic table.
+ uint headerSize = (uint)HeaderField.GetLength(name.Length, value.Length);
+
+ int index = ResolveDynamicTableIndex(staticTableIndex, name);
+ bool success = index == -1
+ ? EncodeLiteralHeaderFieldIndexingNewName(name, value, buffer, out bytesWritten)
+ : EncodeLiteralHeaderFieldIndexing(index, value, buffer, out bytesWritten);
+
+ if (success)
+ {
+ EnsureCapacity(headerSize);
+ AddHeaderEntry(name, value, headerSize);
+ }
+
+ return success;
+ }
+ }
+
+ ///
+ /// Ensure there is capacity for the new header. If there is not enough capacity then remove
+ /// existing headers until space is available.
+ ///
+ private void EnsureCapacity(uint headerSize)
+ {
+ Debug.Assert(headerSize <= _maxHeaderTableSize, "Header is bigger than dynamic table size.");
+
+ while (_maxHeaderTableSize - _headerTableSize < headerSize)
+ {
+ EncoderHeaderEntry? removed = RemoveHeaderEntry();
+ Debug.Assert(removed != null);
+
+ // Removed entries are tracked to be reused.
+ PushRemovedEntry(removed);
+ }
+ }
+
+ private EncoderHeaderEntry? GetEntry(string name, string value)
+ {
+ if (_headerTableSize == 0)
+ {
+ return null;
+ }
+ int hash = name.GetHashCode();
+ int bucketIndex = CalculateBucketIndex(hash);
+ for (EncoderHeaderEntry? e = _headerBuckets[bucketIndex]; e != null; e = e.Next)
+ {
+ // We've already looked up entries based on a hash of the name.
+ // Compare value before name as it is more likely to be different.
+ if (e.Hash == hash &&
+ string.Equals(value, e.Value, StringComparison.Ordinal) &&
+ string.Equals(name, e.Name, StringComparison.Ordinal))
+ {
+ return e;
+ }
+ }
+ return null;
+ }
+
+ private int CalculateDynamicTableIndex(string name)
+ {
+ if (_headerTableSize == 0)
+ {
+ return -1;
+ }
+ int hash = name.GetHashCode();
+ int bucketIndex = CalculateBucketIndex(hash);
+ for (EncoderHeaderEntry? e = _headerBuckets[bucketIndex]; e != null; e = e.Next)
+ {
+ if (e.Hash == hash && string.Equals(name, e.Name, StringComparison.Ordinal))
+ {
+ return CalculateDynamicTableIndex(e.Index);
+ }
+ }
+ return -1;
+ }
+
+ private int CalculateDynamicTableIndex(int index)
+ {
+ return index == -1 ? -1 : index - Head.Before.Index + 1 + H2StaticTable.Count;
+ }
+
+ private void AddHeaderEntry(string name, string value, uint headerSize)
+ {
+ Debug.Assert(headerSize <= _maxHeaderTableSize, "Header is bigger than dynamic table size.");
+ Debug.Assert(headerSize <= _maxHeaderTableSize - _headerTableSize, "Not enough room in dynamic table.");
+
+ int hash = name.GetHashCode();
+ int bucketIndex = CalculateBucketIndex(hash);
+ EncoderHeaderEntry? oldEntry = _headerBuckets[bucketIndex];
+ // Attempt to reuse removed entry
+ EncoderHeaderEntry? newEntry = PopRemovedEntry() ?? new EncoderHeaderEntry();
+ newEntry.Initialize(hash, name, value, Head.Before.Index - 1, oldEntry);
+ _headerBuckets[bucketIndex] = newEntry;
+ newEntry.AddBefore(Head);
+ _headerTableSize += headerSize;
+ }
+
+ private void PushRemovedEntry(EncoderHeaderEntry removed)
+ {
+ if (_removed != null)
+ {
+ removed.Next = _removed;
+ }
+ _removed = removed;
+ }
+
+ private EncoderHeaderEntry? PopRemovedEntry()
+ {
+ if (_removed != null)
+ {
+ EncoderHeaderEntry? removed = _removed;
+ _removed = _removed.Next;
+ return removed;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Remove the oldest entry.
+ ///
+ private EncoderHeaderEntry? RemoveHeaderEntry()
+ {
+ if (_headerTableSize == 0)
+ {
+ return null;
+ }
+ EncoderHeaderEntry? eldest = Head.After;
+ int hash = eldest.Hash;
+ int bucketIndex = CalculateBucketIndex(hash);
+ EncoderHeaderEntry? prev = _headerBuckets[bucketIndex];
+ EncoderHeaderEntry? e = prev;
+ while (e != null)
+ {
+ EncoderHeaderEntry next = e.Next;
+ if (e == eldest)
+ {
+ if (prev == eldest)
+ {
+ _headerBuckets[bucketIndex] = next;
+ }
+ else
+ {
+ prev.Next = next;
+ }
+ _headerTableSize -= eldest.CalculateSize();
+ eldest.Remove();
+ return eldest;
+ }
+ prev = e;
+ e = next;
+ }
+ return null;
+ }
+
+ private int CalculateBucketIndex(int hash)
+ {
+ return hash & _hashMask;
+ }
+ }
+
+ ///
+ /// Hint for how the header should be encoded as HPack. This value can be overriden.
+ /// For example, a header that is larger than the dynamic table won't be indexed.
+ ///
+ internal enum HeaderEncodingHint
+ {
+ Index,
+ IgnoreIndex,
+ NeverIndex
+ }
+}
diff --git a/src/Shared/Hpack/README.md b/src/Shared/Hpack/README.md
new file mode 100644
index 0000000000..d18485ccea
--- /dev/null
+++ b/src/Shared/Hpack/README.md
@@ -0,0 +1,3 @@
+HPack dynamic compression. These files are kept separate to help avoid ASP.NET Core dependencies being added to them.
+
+Runtime currently doesn't implement HPack dynamic compression. These files will move into runtime shareable code in the future when support is added to runtime.
\ No newline at end of file
diff --git a/src/Shared/Hpack/StatusCodes.cs b/src/Shared/Hpack/StatusCodes.cs
new file mode 100644
index 0000000000..eb67205586
--- /dev/null
+++ b/src/Shared/Hpack/StatusCodes.cs
@@ -0,0 +1,151 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Globalization;
+using System.Text;
+
+namespace System.Net.Http.HPack
+{
+ internal static partial class StatusCodes
+ {
+ public static string ToStatusString(int statusCode)
+ {
+ switch (statusCode)
+ {
+ case (int)HttpStatusCode.Continue:
+ return "100";
+ case (int)HttpStatusCode.SwitchingProtocols:
+ return "101";
+ case (int)HttpStatusCode.Processing:
+ return "102";
+
+ case (int)HttpStatusCode.OK:
+ return "200";
+ case (int)HttpStatusCode.Created:
+ return "201";
+ case (int)HttpStatusCode.Accepted:
+ return "202";
+ case (int)HttpStatusCode.NonAuthoritativeInformation:
+ return "203";
+ case (int)HttpStatusCode.NoContent:
+ return "204";
+ case (int)HttpStatusCode.ResetContent:
+ return "205";
+ case (int)HttpStatusCode.PartialContent:
+ return "206";
+ case (int)HttpStatusCode.MultiStatus:
+ return "207";
+ case (int)HttpStatusCode.AlreadyReported:
+ return "208";
+ case (int)HttpStatusCode.IMUsed:
+ return "226";
+
+ case (int)HttpStatusCode.MultipleChoices:
+ return "300";
+ case (int)HttpStatusCode.MovedPermanently:
+ return "301";
+ case (int)HttpStatusCode.Found:
+ return "302";
+ case (int)HttpStatusCode.SeeOther:
+ return "303";
+ case (int)HttpStatusCode.NotModified:
+ return "304";
+ case (int)HttpStatusCode.UseProxy:
+ return "305";
+ case (int)HttpStatusCode.Unused:
+ return "306";
+ case (int)HttpStatusCode.TemporaryRedirect:
+ return "307";
+ case (int)HttpStatusCode.PermanentRedirect:
+ return "308";
+
+ case (int)HttpStatusCode.BadRequest:
+ return "400";
+ case (int)HttpStatusCode.Unauthorized:
+ return "401";
+ case (int)HttpStatusCode.PaymentRequired:
+ return "402";
+ case (int)HttpStatusCode.Forbidden:
+ return "403";
+ case (int)HttpStatusCode.NotFound:
+ return "404";
+ case (int)HttpStatusCode.MethodNotAllowed:
+ return "405";
+ case (int)HttpStatusCode.NotAcceptable:
+ return "406";
+ case (int)HttpStatusCode.ProxyAuthenticationRequired:
+ return "407";
+ case (int)HttpStatusCode.RequestTimeout:
+ return "408";
+ case (int)HttpStatusCode.Conflict:
+ return "409";
+ case (int)HttpStatusCode.Gone:
+ return "410";
+ case (int)HttpStatusCode.LengthRequired:
+ return "411";
+ case (int)HttpStatusCode.PreconditionFailed:
+ return "412";
+ case (int)HttpStatusCode.RequestEntityTooLarge:
+ return "413";
+ case (int)HttpStatusCode.RequestUriTooLong:
+ return "414";
+ case (int)HttpStatusCode.UnsupportedMediaType:
+ return "415";
+ case (int)HttpStatusCode.RequestedRangeNotSatisfiable:
+ return "416";
+ case (int)HttpStatusCode.ExpectationFailed:
+ return "417";
+ case (int)418:
+ return "418";
+ case (int)419:
+ return "419";
+ case (int)HttpStatusCode.MisdirectedRequest:
+ return "421";
+ case (int)HttpStatusCode.UnprocessableEntity:
+ return "422";
+ case (int)HttpStatusCode.Locked:
+ return "423";
+ case (int)HttpStatusCode.FailedDependency:
+ return "424";
+ case (int)HttpStatusCode.UpgradeRequired:
+ return "426";
+ case (int)HttpStatusCode.PreconditionRequired:
+ return "428";
+ case (int)HttpStatusCode.TooManyRequests:
+ return "429";
+ case (int)HttpStatusCode.RequestHeaderFieldsTooLarge:
+ return "431";
+ case (int)HttpStatusCode.UnavailableForLegalReasons:
+ return "451";
+
+ case (int)HttpStatusCode.InternalServerError:
+ return "500";
+ case (int)HttpStatusCode.NotImplemented:
+ return "501";
+ case (int)HttpStatusCode.BadGateway:
+ return "502";
+ case (int)HttpStatusCode.ServiceUnavailable:
+ return "503";
+ case (int)HttpStatusCode.GatewayTimeout:
+ return "504";
+ case (int)HttpStatusCode.HttpVersionNotSupported:
+ return "505";
+ case (int)HttpStatusCode.VariantAlsoNegotiates:
+ return "506";
+ case (int)HttpStatusCode.InsufficientStorage:
+ return "507";
+ case (int)HttpStatusCode.LoopDetected:
+ return "508";
+ case (int)HttpStatusCode.NotExtended:
+ return "510";
+ case (int)HttpStatusCode.NetworkAuthenticationRequired:
+ return "511";
+
+ default:
+ return statusCode.ToString(CultureInfo.InvariantCulture);
+
+ }
+ }
+ }
+}
diff --git a/src/Shared/Process/ProcessEx.cs b/src/Shared/Process/ProcessEx.cs
index c1743a2f0a..3f5ca0aead 100644
--- a/src/Shared/Process/ProcessEx.cs
+++ b/src/Shared/Process/ProcessEx.cs
@@ -26,9 +26,11 @@ namespace Microsoft.AspNetCore.Internal
private readonly StringBuilder _stderrCapture;
private readonly StringBuilder _stdoutCapture;
private readonly object _pipeCaptureLock = new object();
+ private readonly object _testOutputLock = new object();
private BlockingCollection _stdoutLines;
private TaskCompletionSource _exited;
private CancellationTokenSource _stdoutLinesCancellationSource = new CancellationTokenSource(TimeSpan.FromMinutes(5));
+ private bool _disposed = false;
public ProcessEx(ITestOutputHelper output, Process proc)
{
@@ -135,7 +137,13 @@ namespace Microsoft.AspNetCore.Internal
_stderrCapture.AppendLine(e.Data);
}
- _output.WriteLine("[ERROR] " + e.Data);
+ lock (_testOutputLock)
+ {
+ if (!_disposed)
+ {
+ _output.WriteLine("[ERROR] " + e.Data);
+ }
+ }
}
private void OnOutputData(object sender, DataReceivedEventArgs e)
@@ -150,7 +158,13 @@ namespace Microsoft.AspNetCore.Internal
_stdoutCapture.AppendLine(e.Data);
}
- _output.WriteLine(e.Data);
+ lock (_testOutputLock)
+ {
+ if (!_disposed)
+ {
+ _output.WriteLine(e.Data);
+ }
+ }
if (_stdoutLines != null)
{
@@ -204,6 +218,11 @@ namespace Microsoft.AspNetCore.Internal
public void Dispose()
{
+ lock (_testOutputLock)
+ {
+ _disposed = true;
+ }
+
if (_process != null && !_process.HasExited)
{
_process.KillTree();
diff --git a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs
index d09f184134..97cdea1c50 100644
--- a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs
+++ b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs
@@ -8,7 +8,7 @@ using System.Diagnostics;
namespace System.Net.Http.HPack
{
- internal static class HPackEncoder
+ internal partial class HPackEncoder
{
// Things we should add:
// * Huffman encoding
@@ -109,6 +109,70 @@ namespace System.Net.Http.HPack
return false;
}
+ /// Encodes a "Literal Header Field never Indexing".
+ public static bool EncodeLiteralHeaderFieldNeverIndexing(int index, string value, Span destination, out int bytesWritten)
+ {
+ // From https://tools.ietf.org/html/rfc7541#section-6.2.3
+ // ------------------------------------------------------
+ // 0 1 2 3 4 5 6 7
+ // +---+---+---+---+---+---+---+---+
+ // | 0 | 0 | 0 | 1 | Index (4+) |
+ // +---+---+-----------------------+
+ // | H | Value Length (7+) |
+ // +---+---------------------------+
+ // | Value String (Length octets) |
+ // +-------------------------------+
+
+ if ((uint)destination.Length >= 2)
+ {
+ destination[0] = 0x10;
+ if (IntegerEncoder.Encode(index, 4, destination, out int indexLength))
+ {
+ Debug.Assert(indexLength >= 1);
+ if (EncodeStringLiteral(value, destination.Slice(indexLength), out int nameLength))
+ {
+ bytesWritten = indexLength + nameLength;
+ return true;
+ }
+ }
+ }
+
+ bytesWritten = 0;
+ return false;
+ }
+
+ /// Encodes a "Literal Header Field with Indexing".
+ public static bool EncodeLiteralHeaderFieldIndexing(int index, string value, Span destination, out int bytesWritten)
+ {
+ // From https://tools.ietf.org/html/rfc7541#section-6.2.2
+ // ------------------------------------------------------
+ // 0 1 2 3 4 5 6 7
+ // +---+---+---+---+---+---+---+---+
+ // | 0 | 1 | Index (6+) |
+ // +---+---+-----------------------+
+ // | H | Value Length (7+) |
+ // +---+---------------------------+
+ // | Value String (Length octets) |
+ // +-------------------------------+
+
+ if ((uint)destination.Length >= 2)
+ {
+ destination[0] = 0x40;
+ if (IntegerEncoder.Encode(index, 6, destination, out int indexLength))
+ {
+ Debug.Assert(indexLength >= 1);
+ if (EncodeStringLiteral(value, destination.Slice(indexLength), out int nameLength))
+ {
+ bytesWritten = indexLength + nameLength;
+ return true;
+ }
+ }
+ }
+
+ bytesWritten = 0;
+ return false;
+ }
+
///
/// Encodes a "Literal Header Field without Indexing", but only the index portion;
/// a subsequent call to EncodeStringLiteral must be used to encode the associated value.
@@ -144,6 +208,27 @@ namespace System.Net.Http.HPack
return false;
}
+ /// Encodes a "Literal Header Field with Indexing - New Name".
+ public static bool EncodeLiteralHeaderFieldIndexingNewName(string name, string value, Span destination, out int bytesWritten)
+ {
+ // From https://tools.ietf.org/html/rfc7541#section-6.2.2
+ // ------------------------------------------------------
+ // 0 1 2 3 4 5 6 7
+ // +---+---+---+---+---+---+---+---+
+ // | 0 | 1 | 0 |
+ // +---+---+-----------------------+
+ // | H | Name Length (7+) |
+ // +---+---------------------------+
+ // | Name String (Length octets) |
+ // +---+---------------------------+
+ // | H | Value Length (7+) |
+ // +---+---------------------------+
+ // | Value String (Length octets) |
+ // +-------------------------------+
+
+ return EncodeLiteralHeaderNewNameCore(0x40, name, value, destination, out bytesWritten);
+ }
+
/// Encodes a "Literal Header Field without Indexing - New Name".
public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, string value, Span destination, out int bytesWritten)
{
@@ -162,9 +247,35 @@ namespace System.Net.Http.HPack
// | Value String (Length octets) |
// +-------------------------------+
+ return EncodeLiteralHeaderNewNameCore(0, name, value, destination, out bytesWritten);
+ }
+
+ /// Encodes a "Literal Header Field never Indexing - New Name".
+ public static bool EncodeLiteralHeaderFieldNeverIndexingNewName(string name, string value, Span destination, out int bytesWritten)
+ {
+ // From https://tools.ietf.org/html/rfc7541#section-6.2.3
+ // ------------------------------------------------------
+ // 0 1 2 3 4 5 6 7
+ // +---+---+---+---+---+---+---+---+
+ // | 0 | 0 | 0 | 1 | 0 |
+ // +---+---+-----------------------+
+ // | H | Name Length (7+) |
+ // +---+---------------------------+
+ // | Name String (Length octets) |
+ // +---+---------------------------+
+ // | H | Value Length (7+) |
+ // +---+---------------------------+
+ // | Value String (Length octets) |
+ // +-------------------------------+
+
+ return EncodeLiteralHeaderNewNameCore(0x10, name, value, destination, out bytesWritten);
+ }
+
+ private static bool EncodeLiteralHeaderNewNameCore(byte mask, string name, string value, Span destination, out int bytesWritten)
+ {
if ((uint)destination.Length >= 3)
{
- destination[0] = 0;
+ destination[0] = mask;
if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength) &&
EncodeStringLiteral(value, destination.Slice(1 + nameLength), out int valueLength))
{
@@ -372,6 +483,25 @@ namespace System.Net.Http.HPack
return false;
}
+ public static bool EncodeDynamicTableSizeUpdate(int value, Span destination, out int bytesWritten)
+ {
+ // From https://tools.ietf.org/html/rfc7541#section-6.3
+ // ----------------------------------------------------
+ // 0 1 2 3 4 5 6 7
+ // +---+---+---+---+---+---+---+---+
+ // | 0 | 0 | 1 | Max size (5+) |
+ // +---+---------------------------+
+
+ if (destination.Length != 0)
+ {
+ destination[0] = 0x20;
+ return IntegerEncoder.Encode(value, 5, destination, out bytesWritten);
+ }
+
+ bytesWritten = 0;
+ return false;
+ }
+
public static bool EncodeStringLiterals(ReadOnlySpan values, string? separator, Span destination, out int bytesWritten)
{
bytesWritten = 0;
diff --git a/src/Shared/runtime/Http2/Hpack/StatusCodes.cs b/src/Shared/runtime/Http2/Hpack/StatusCodes.cs
index b701fa79f4..01c42abbc5 100644
--- a/src/Shared/runtime/Http2/Hpack/StatusCodes.cs
+++ b/src/Shared/runtime/Http2/Hpack/StatusCodes.cs
@@ -7,7 +7,7 @@ using System.Text;
namespace System.Net.Http.HPack
{
- internal static class StatusCodes
+ internal static partial class StatusCodes
{
// This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static
diff --git a/src/SignalR/.vsconfig b/src/SignalR/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/SignalR/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/SignalR/Directory.Build.props b/src/SignalR/Directory.Build.props
index 27a3751cd8..74b4c7adee 100644
--- a/src/SignalR/Directory.Build.props
+++ b/src/SignalR/Directory.Build.props
@@ -21,7 +21,6 @@
PreserveNewest
-
diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs
index 2220685e82..afe4254ded 100644
--- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs
+++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs
@@ -1335,6 +1335,102 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
}
}
+ [Theory]
+ [QuarantinedTest]
+ [MemberData(nameof(HubProtocolsList))]
+ public async Task ServerLogsErrorIfClientInvokeCannotBeSerialized(string protocolName)
+ {
+ // Just to help sanity check that the right exception is thrown
+ var exceptionSubstring = protocolName switch
+ {
+ "json" => "A possible object cycle was detected.",
+ "newtonsoft-json" => "A possible object cycle was detected.",
+ "messagepack" => "Failed to serialize Microsoft.AspNetCore.SignalR.Client.FunctionalTests.TestHub+Unserializable value.",
+ var x => throw new Exception($"The test does not have an exception string for the protocol '{x}'!"),
+ };
+
+ var protocol = HubProtocols[protocolName];
+ using (var server = await StartServer(write => write.EventId.Name == "FailedWritingMessage"))
+ {
+ var connection = CreateHubConnection(server.Url, "/default", HttpTransportType.WebSockets, protocol, LoggerFactory);
+ var closedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ connection.Closed += (ex) => { closedTcs.TrySetResult(ex); return Task.CompletedTask; };
+ try
+ {
+ await connection.StartAsync().OrTimeout();
+
+ var result = connection.InvokeAsync(nameof(TestHub.CallWithUnserializableObject));
+
+ // The connection should close.
+ Assert.Null(await closedTcs.Task.OrTimeout());
+
+ await Assert.ThrowsAsync(() => result).OrTimeout();
+ }
+ catch (Exception ex)
+ {
+ LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
+ throw;
+ }
+ finally
+ {
+ await connection.DisposeAsync().OrTimeout();
+ }
+
+ var errorLog = server.GetLogs().SingleOrDefault(r => r.Write.EventId.Name == "FailedWritingMessage");
+ Assert.NotNull(errorLog);
+ Assert.Contains(exceptionSubstring, errorLog.Write.Exception.Message);
+ Assert.Equal(LogLevel.Error, errorLog.Write.LogLevel);
+ }
+ }
+
+ [Theory]
+ [QuarantinedTest]
+ [MemberData(nameof(HubProtocolsList))]
+ public async Task ServerLogsErrorIfReturnValueCannotBeSerialized(string protocolName)
+ {
+ // Just to help sanity check that the right exception is thrown
+ var exceptionSubstring = protocolName switch
+ {
+ "json" => "A possible object cycle was detected.",
+ "newtonsoft-json" => "A possible object cycle was detected.",
+ "messagepack" => "Failed to serialize Microsoft.AspNetCore.SignalR.Client.FunctionalTests.TestHub+Unserializable value.",
+ var x => throw new Exception($"The test does not have an exception string for the protocol '{x}'!"),
+ };
+
+ var protocol = HubProtocols[protocolName];
+ using (var server = await StartServer(write => write.EventId.Name == "FailedWritingMessage"))
+ {
+ var connection = CreateHubConnection(server.Url, "/default", HttpTransportType.LongPolling, protocol, LoggerFactory);
+ var closedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ connection.Closed += (ex) => { closedTcs.TrySetResult(ex); return Task.CompletedTask; };
+ try
+ {
+ await connection.StartAsync().OrTimeout();
+
+ var result = connection.InvokeAsync(nameof(TestHub.GetUnserializableObject)).OrTimeout();
+
+ // The connection should close.
+ Assert.Null(await closedTcs.Task.OrTimeout());
+
+ await Assert.ThrowsAsync(() => result).OrTimeout();
+ }
+ catch (Exception ex)
+ {
+ LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
+ throw;
+ }
+ finally
+ {
+ await connection.DisposeAsync().OrTimeout();
+ }
+
+ var errorLog = server.GetLogs().SingleOrDefault(r => r.Write.EventId.Name == "FailedWritingMessage");
+ Assert.NotNull(errorLog);
+ Assert.Contains(exceptionSubstring, errorLog.Write.Exception.Message);
+ Assert.Equal(LogLevel.Error, errorLog.Write.LogLevel);
+ }
+ }
+
[Fact]
public async Task RandomGenericIsNotTreatedAsStream()
{
diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs
index 01ce4198c4..90328a6b57 100644
--- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs
+++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs
@@ -95,6 +95,33 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
{
return Context.Features.Get().TransportType.ToString();
}
+
+ public async Task CallWithUnserializableObject()
+ {
+ await Clients.All.SendAsync("Foo", Unserializable.Create());
+ }
+
+ public Unserializable GetUnserializableObject()
+ {
+ return Unserializable.Create();
+ }
+
+ public class Unserializable
+ {
+ public Unserializable Child { get; private set; }
+
+ private Unserializable()
+ {
+ }
+
+ internal static Unserializable Create()
+ {
+ // Loops throw off every serializer ;).
+ var o = new Unserializable();
+ o.Child = o;
+ return o;
+ }
+ }
}
public class DynamicTestHub : DynamicHub
diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs
index abf6b69524..c9495d7156 100644
--- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs
+++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs
@@ -84,13 +84,6 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
Features.Set(this);
}
- internal HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
- : this(id, null, logger)
- {
- Transport = transport;
- Application = application;
- }
-
public CancellationTokenSource Cancellation { get; set; }
public HttpTransportType TransportType { get; set; }
diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs
index b0f4b079fb..aec84c5dab 100644
--- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs
+++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs
@@ -10,7 +10,6 @@ using System.IO.Pipelines;
using System.Net.WebSockets;
using System.Security.Cryptography;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Internal;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Internal;
@@ -31,24 +30,14 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
private readonly TimerAwaitable _nextHeartbeat;
private readonly ILogger _logger;
private readonly ILogger _connectionLogger;
- private readonly bool _useSendTimeout = true;
private readonly TimeSpan _disconnectTimeout;
- public HttpConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime appLifetime)
- : this(loggerFactory, appLifetime, Options.Create(new ConnectionOptions() { DisconnectTimeout = ConnectionOptionsSetup.DefaultDisconectTimeout }))
- {
- }
-
public HttpConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime appLifetime, IOptions connectionOptions)
{
_logger = loggerFactory.CreateLogger();
_connectionLogger = loggerFactory.CreateLogger();
_nextHeartbeat = new TimerAwaitable(_heartbeatTickRate, _heartbeatTickRate);
_disconnectTimeout = connectionOptions.Value.DisconnectTimeout ?? ConnectionOptionsSetup.DefaultDisconectTimeout;
- if (AppContext.TryGetSwitch("Microsoft.AspNetCore.Http.Connections.DoNotUseSendTimeout", out var timeoutDisabled))
- {
- _useSendTimeout = !timeoutDisabled;
- }
// Register these last as the callbacks could run immediately
appLifetime.ApplicationStarted.Register(() => Start());
@@ -176,7 +165,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
}
else
{
- if (!Debugger.IsAttached && _useSendTimeout)
+ if (!Debugger.IsAttached)
{
connection.TryCancelSend(utcNow.Ticks);
}
diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs
index 34626ca05b..5f084a232f 100644
--- a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs
+++ b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs
@@ -2347,10 +2347,10 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory)
{
- return new HttpConnectionManager(loggerFactory ?? new LoggerFactory(), new EmptyApplicationLifetime());
+ return CreateConnectionManager(loggerFactory, null);
}
- private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory, TimeSpan disconnectTimeout)
+ private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory, TimeSpan? disconnectTimeout)
{
var connectionOptions = new ConnectionOptions();
connectionOptions.DisconnectTimeout = disconnectTimeout;
diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs
index 05a29f0e73..dcf8dfefa9 100644
--- a/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs
+++ b/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs
@@ -7,6 +7,7 @@ using System.IO.Pipelines;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Connections.Internal;
+using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Tests;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -411,7 +412,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
private static HttpConnectionManager CreateConnectionManager(ILoggerFactory loggerFactory, IHostApplicationLifetime lifetime = null)
{
lifetime = lifetime ?? new EmptyApplicationLifetime();
- return new HttpConnectionManager(loggerFactory, lifetime);
+ return new HttpConnectionManager(loggerFactory, lifetime, Options.Create(new ConnectionOptions()));
}
[Flags]
diff --git a/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs b/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs
index 27ad53d9d9..b5e50d7894 100644
--- a/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs
+++ b/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs
@@ -31,11 +31,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
- var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application, LoggerFactory.CreateLogger("HttpConnectionContext1"));
+ var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1"))
+ {
+ Transport = pair.Transport,
+ Application = pair.Application,
+ };
using (var feature = new TestWebSocketConnectionFeature())
{
- var connectionContext = new HttpConnectionContext(string.Empty, null, null, LoggerFactory.CreateLogger("HttpConnectionContext2"));
+ var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2"));
var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory);
// Give the server socket to the transport and run it
@@ -79,11 +83,15 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
- var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application, LoggerFactory.CreateLogger("HttpConnectionContext1"));
+ var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1"))
+ {
+ Transport = pair.Transport,
+ Application = pair.Application,
+ };
using (var feature = new TestWebSocketConnectionFeature())
{
- var connectionContext = new HttpConnectionContext(string.Empty, null, null, LoggerFactory.CreateLogger("HttpConnectionContext2"));
+ var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2"));
connectionContext.ActiveFormat = transferFormat;
var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory);
@@ -116,7 +124,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
- var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application, LoggerFactory.CreateLogger("HttpConnectionContext1"));
+ var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext1"))
+ {
+ Transport = pair.Transport,
+ Application = pair.Application,
+ };
using (var feature = new TestWebSocketConnectionFeature())
{
@@ -139,7 +151,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
}
}
- var connectionContext = new HttpConnectionContext(string.Empty, null, null, LoggerFactory.CreateLogger("HttpConnectionContext2"));
+ var connectionContext = new HttpConnectionContext(string.Empty, connectionToken: null, LoggerFactory.CreateLogger("HttpConnectionContext2"));
var ws = new WebSocketsServerTransport(new WebSocketOptions(), connection.Application, connectionContext, LoggerFactory);
// Give the server socket to the transport and run it
@@ -169,7 +181,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
- var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application);
+ var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
+ {
+ Transport = pair.Transport,
+ Application = pair.Application,
+ };
using (var feature = new TestWebSocketConnectionFeature())
{
@@ -201,7 +217,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
- var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application);
+ var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
+ {
+ Transport = pair.Transport,
+ Application = pair.Application,
+ };
using (var feature = new TestWebSocketConnectionFeature())
{
@@ -236,7 +256,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
- var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application);
+ var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
+ {
+ Transport = pair.Transport,
+ Application = pair.Application,
+ };
using (var feature = new TestWebSocketConnectionFeature())
{
@@ -271,7 +295,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
- var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application);
+ var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
+ {
+ Transport = pair.Transport,
+ Application = pair.Application,
+ };
using (var feature = new TestWebSocketConnectionFeature())
{
@@ -311,7 +339,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
- var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application);
+ var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
+ {
+ Transport = pair.Transport,
+ Application = pair.Application,
+ };
using (var feature = new TestWebSocketConnectionFeature())
{
@@ -354,7 +386,11 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
using (StartVerifiableLog())
{
var pair = DuplexPipe.CreateConnectionPair(PipeOptions.Default, PipeOptions.Default);
- var connection = new HttpConnectionContext("foo", pair.Transport, pair.Application);
+ var connection = new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(nameof(HttpConnectionContext)))
+ {
+ Transport = pair.Transport,
+ Application = pair.Application,
+ };
using (var feature = new TestWebSocketConnectionFeature())
{
diff --git a/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs b/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs
index 85025b6444..bd2ad8f788 100644
--- a/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs
+++ b/src/SignalR/common/Protocols.MessagePack/src/MessagePackHubProtocolOptions.cs
@@ -9,24 +9,31 @@ namespace Microsoft.AspNetCore.SignalR
{
public class MessagePackHubProtocolOptions
{
- private IList _formatterResolvers;
+ private MessagePackSerializerOptions _messagePackSerializerOptions;
- public IList FormatterResolvers
+ ///
+ /// Gets or sets the used internally by the .
+ /// If you override the default value, we strongly recommend that you set to by calling:
+ /// customMessagePackSerializerOptions = customMessagePackSerializerOptions.WithSecurity(MessagePackSecurity.UntrustedData)
+ /// If you modify the default options you must also assign the updated options back to the property:
+ /// options.SerializerOptions = options.SerializerOptions.WithResolver(new CustomResolver());
+ ///
+ public MessagePackSerializerOptions SerializerOptions
{
get
{
- if (_formatterResolvers == null)
+ if (_messagePackSerializerOptions == null)
{
// The default set of resolvers trigger a static constructor that throws on AOT environments.
// This gives users the chance to use an AOT friendly formatter.
- _formatterResolvers = MessagePackHubProtocol.CreateDefaultFormatterResolvers();
+ _messagePackSerializerOptions = MessagePackHubProtocol.CreateDefaultMessagePackSerializerOptions();
}
- return _formatterResolvers;
+ return _messagePackSerializerOptions;
}
set
{
- _formatterResolvers = value;
+ _messagePackSerializerOptions = value;
}
}
}
diff --git a/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs b/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs
index e1a762937b..ffa814da22 100644
--- a/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs
+++ b/src/SignalR/common/Protocols.MessagePack/src/Protocol/MessagePackHubProtocol.cs
@@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
private const int NonVoidResult = 3;
private readonly MessagePackSerializerOptions _msgPackSerializerOptions;
+
private static readonly string ProtocolName = "messagepack";
private static readonly int ProtocolVersion = 1;
@@ -52,37 +53,7 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
/// The options used to initialize the protocol.
public MessagePackHubProtocol(IOptions options)
{
- var msgPackOptions = options.Value;
- var resolver = SignalRResolver.Instance;
- var hasCustomFormatterResolver = false;
-
- // if counts don't match then we know users customized resolvers so we set up the options with the provided resolvers
- if (msgPackOptions.FormatterResolvers.Count != SignalRResolver.Resolvers.Count)
- {
- hasCustomFormatterResolver = true;
- }
- else
- {
- // Compare each "reference" in the FormatterResolvers IList<> against the default "SignalRResolver.Resolvers" IList<>
- for (var i = 0; i < msgPackOptions.FormatterResolvers.Count; i++)
- {
- // check if the user customized the resolvers
- if (msgPackOptions.FormatterResolvers[i] != SignalRResolver.Resolvers[i])
- {
- hasCustomFormatterResolver = true;
- break;
- }
- }
- }
-
- if (hasCustomFormatterResolver)
- {
- resolver = CompositeResolver.Create(Array.Empty(), (IReadOnlyList)msgPackOptions.FormatterResolvers);
- }
-
- _msgPackSerializerOptions = MessagePackSerializerOptions.Standard
- .WithResolver(resolver)
- .WithSecurity(MessagePackSecurity.UntrustedData);
+ _msgPackSerializerOptions = options.Value.SerializerOptions;
}
///
@@ -656,17 +627,17 @@ namespace Microsoft.AspNetCore.SignalR.Protocol
}
}
- internal static List CreateDefaultFormatterResolvers()
- {
- // Copy to allow users to add/remove resolvers without changing the static SignalRResolver list
- return new List(SignalRResolver.Resolvers);
- }
+ internal static MessagePackSerializerOptions CreateDefaultMessagePackSerializerOptions() =>
+ MessagePackSerializerOptions
+ .Standard
+ .WithResolver(SignalRResolver.Instance)
+ .WithSecurity(MessagePackSecurity.UntrustedData);
internal class SignalRResolver : IFormatterResolver
{
public static readonly IFormatterResolver Instance = new SignalRResolver();
- public static readonly IList Resolvers = new IFormatterResolver[]
+ public static readonly IReadOnlyList Resolvers = new IFormatterResolver[]
{
DynamicEnumAsStringResolver.Instance,
ContractlessStandardResolver.Instance,
diff --git a/src/SignalR/common/Shared/ISystemClock.cs b/src/SignalR/common/Shared/ISystemClock.cs
new file mode 100644
index 0000000000..86e2b035ea
--- /dev/null
+++ b/src/SignalR/common/Shared/ISystemClock.cs
@@ -0,0 +1,26 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNetCore.Internal
+{
+ internal interface ISystemClock
+ {
+ ///
+ /// Retrieves the current UTC system time.
+ ///
+ DateTimeOffset UtcNow { get; }
+
+ ///
+ /// Retrieves ticks for the current UTC system time.
+ ///
+ long UtcNowTicks { get; }
+
+ ///
+ /// Retrieves the current UTC system time.
+ /// This is only safe to use from code called by the Heartbeat.
+ ///
+ DateTimeOffset UtcNowUnsynchronized { get; }
+ }
+}
diff --git a/src/SignalR/common/Shared/SystemClock.cs b/src/SignalR/common/Shared/SystemClock.cs
new file mode 100644
index 0000000000..de5a9b711c
--- /dev/null
+++ b/src/SignalR/common/Shared/SystemClock.cs
@@ -0,0 +1,28 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNetCore.Internal
+{
+ ///
+ /// Provides access to the normal system clock.
+ ///
+ internal class SystemClock : ISystemClock
+ {
+ ///
+ /// Retrieves the current UTC system time.
+ ///
+ public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
+
+ ///
+ /// Retrieves ticks for the current UTC system time.
+ ///
+ public long UtcNowTicks => DateTimeOffset.UtcNow.Ticks;
+
+ ///
+ /// Retrieves the current UTC system time.
+ ///
+ public DateTimeOffset UtcNowUnsynchronized => DateTimeOffset.UtcNow;
+ }
+}
diff --git a/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs b/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs
index a02e4bb1de..631220c7de 100644
--- a/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs
+++ b/src/SignalR/common/testassets/Tests.Utils/InProcessTestServer.cs
@@ -84,6 +84,8 @@ namespace Microsoft.AspNetCore.SignalR.Tests
_logger = _loggerFactory.CreateLogger>();
}
+ public IList GetLogs() => _logSinkProvider.GetLogs();
+
private async Task StartServerInner()
{
// We're using 127.0.0.1 instead of localhost to ensure that we use IPV4 across different OSes
diff --git a/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs b/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs
index 9193044270..7c9d9f071c 100644
--- a/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs
+++ b/src/SignalR/common/testassets/Tests.Utils/LogRecord.cs
@@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging.Testing;
namespace Microsoft.AspNetCore.SignalR.Tests
{
// WriteContext, but with a timestamp...
- internal class LogRecord
+ public class LogRecord
{
public DateTime Timestamp { get; }
public WriteContext Write { get; }
diff --git a/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj b/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj
index 9e69675720..782c8410b8 100644
--- a/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj
+++ b/src/SignalR/common/testassets/Tests.Utils/Microsoft.AspNetCore.SignalR.Tests.Utils.csproj
@@ -21,7 +21,6 @@
-
diff --git a/src/SignalR/server/Core/src/HubConnectionContext.cs b/src/SignalR/server/Core/src/HubConnectionContext.cs
index 1dc7ad89c4..fcd704c428 100644
--- a/src/SignalR/server/Core/src/HubConnectionContext.cs
+++ b/src/SignalR/server/Core/src/HubConnectionContext.cs
@@ -13,6 +13,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.Logging;
@@ -34,13 +35,11 @@ namespace Microsoft.AspNetCore.SignalR
private readonly long _keepAliveInterval;
private readonly long _clientTimeoutInterval;
private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1);
- private readonly bool _useAbsoluteClientTimeout;
private readonly object _receiveMessageTimeoutLock = new object();
+ private readonly ISystemClock _systemClock;
private StreamTracker _streamTracker;
- private long _lastSendTimeStamp = DateTime.UtcNow.Ticks;
- private long _lastReceivedTimeStamp = DateTime.UtcNow.Ticks;
- private bool _receivedMessageThisInterval = false;
+ private long _lastSendTimeStamp;
private ReadOnlyMemory _cachedPingMessage;
private bool _clientTimeoutActive;
private volatile bool _connectionAborted;
@@ -70,10 +69,8 @@ namespace Microsoft.AspNetCore.SignalR
HubCallerContext = new DefaultHubCallerContext(this);
- if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SignalR.UseAbsoluteClientTimeout", out var useAbsoluteClientTimeout))
- {
- _useAbsoluteClientTimeout = useAbsoluteClientTimeout;
- }
+ _systemClock = contextOptions.SystemClock ?? new SystemClock();
+ _lastSendTimeStamp = _systemClock.UtcNowTicks;
}
internal StreamTracker StreamTracker
@@ -558,7 +555,7 @@ namespace Microsoft.AspNetCore.SignalR
private void KeepAliveTick()
{
- var currentTime = DateTime.UtcNow.Ticks;
+ var currentTime = _systemClock.UtcNowTicks;
// Implements the keep-alive tick behavior
// Each tick, we check if the time since the last send is larger than the keep alive duration (in ticks).
@@ -597,35 +594,17 @@ namespace Microsoft.AspNetCore.SignalR
return;
}
- if (_useAbsoluteClientTimeout)
+ lock (_receiveMessageTimeoutLock)
{
- // If it's been too long since we've heard from the client, then close this
- if (DateTime.UtcNow.Ticks - Volatile.Read(ref _lastReceivedTimeStamp) > _clientTimeoutInterval)
+ if (_receivedMessageTimeoutEnabled)
{
- if (!_receivedMessageThisInterval)
+ _receivedMessageElapsedTicks = _systemClock.UtcNowTicks - _receivedMessageTimestamp;
+
+ if (_receivedMessageElapsedTicks >= _clientTimeoutInterval)
{
Log.ClientTimeout(_logger, TimeSpan.FromTicks(_clientTimeoutInterval));
AbortAllowReconnect();
}
-
- _receivedMessageThisInterval = false;
- Volatile.Write(ref _lastReceivedTimeStamp, DateTime.UtcNow.Ticks);
- }
- }
- else
- {
- lock (_receiveMessageTimeoutLock)
- {
- if (_receivedMessageTimeoutEnabled)
- {
- _receivedMessageElapsedTicks = DateTime.UtcNow.Ticks - _receivedMessageTimestamp;
-
- if (_receivedMessageElapsedTicks >= _clientTimeoutInterval)
- {
- Log.ClientTimeout(_logger, TimeSpan.FromTicks(_clientTimeoutInterval));
- AbortAllowReconnect();
- }
- }
}
}
}
@@ -670,37 +649,24 @@ namespace Microsoft.AspNetCore.SignalR
}
}
- internal void ResetClientTimeout()
- {
- _receivedMessageThisInterval = true;
- }
-
internal void BeginClientTimeout()
{
- // check if new timeout behavior is in use
- if (!_useAbsoluteClientTimeout)
+ lock (_receiveMessageTimeoutLock)
{
- lock (_receiveMessageTimeoutLock)
- {
- _receivedMessageTimeoutEnabled = true;
- _receivedMessageTimestamp = DateTime.UtcNow.Ticks;
- }
+ _receivedMessageTimeoutEnabled = true;
+ _receivedMessageTimestamp = _systemClock.UtcNowTicks;
}
}
internal void StopClientTimeout()
{
- // check if new timeout behavior is in use
- if (!_useAbsoluteClientTimeout)
+ lock (_receiveMessageTimeoutLock)
{
- lock (_receiveMessageTimeoutLock)
- {
- // we received a message so stop the timer and reset it
- // it will resume after the message has been processed
- _receivedMessageElapsedTicks = 0;
- _receivedMessageTimestamp = 0;
- _receivedMessageTimeoutEnabled = false;
- }
+ // we received a message so stop the timer and reset it
+ // it will resume after the message has been processed
+ _receivedMessageElapsedTicks = 0;
+ _receivedMessageTimestamp = 0;
+ _receivedMessageTimeoutEnabled = false;
}
}
@@ -723,7 +689,7 @@ namespace Microsoft.AspNetCore.SignalR
LoggerMessage.Define(LogLevel.Debug, new EventId(5, "HandshakeFailed"), "Failed connection handshake.");
private static readonly Action _failedWritingMessage =
- LoggerMessage.Define(LogLevel.Debug, new EventId(6, "FailedWritingMessage"), "Failed writing message. Aborting connection.");
+ LoggerMessage.Define(LogLevel.Error, new EventId(6, "FailedWritingMessage"), "Failed writing message. Aborting connection.");
private static readonly Action _protocolVersionFailed =
LoggerMessage.Define(LogLevel.Debug, new EventId(7, "ProtocolVersionFailed"), "Server does not support version {Version} of the {Protocol} protocol.");
diff --git a/src/SignalR/server/Core/src/HubConnectionContextOptions.cs b/src/SignalR/server/Core/src/HubConnectionContextOptions.cs
index c95c58afe6..fc1d34383e 100644
--- a/src/SignalR/server/Core/src/HubConnectionContextOptions.cs
+++ b/src/SignalR/server/Core/src/HubConnectionContextOptions.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using Microsoft.AspNetCore.Internal;
namespace Microsoft.AspNetCore.SignalR
{
@@ -29,5 +30,7 @@ namespace Microsoft.AspNetCore.SignalR
/// Gets or sets the maximum message size the client can send.
///
public long? MaximumReceiveMessageSize { get; set; }
+
+ internal ISystemClock SystemClock { get; set; }
}
}
diff --git a/src/SignalR/server/Core/src/HubConnectionHandler.cs b/src/SignalR/server/Core/src/HubConnectionHandler.cs
index 0a8f3380f9..6667890496 100644
--- a/src/SignalR/server/Core/src/HubConnectionHandler.cs
+++ b/src/SignalR/server/Core/src/HubConnectionHandler.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Extensions.DependencyInjection;
@@ -31,6 +32,9 @@ namespace Microsoft.AspNetCore.SignalR
private readonly bool _enableDetailedErrors;
private readonly long? _maximumMessageSize;
+ // Internal for testing
+ internal ISystemClock SystemClock { get; set; } = new SystemClock();
+
///
/// Initializes a new instance of the class.
///
@@ -98,6 +102,7 @@ namespace Microsoft.AspNetCore.SignalR
ClientTimeoutInterval = _hubOptions.ClientTimeoutInterval ?? _globalHubOptions.ClientTimeoutInterval ?? HubOptionsSetup.DefaultClientTimeoutInterval,
StreamBufferCapacity = _hubOptions.StreamBufferCapacity ?? _globalHubOptions.StreamBufferCapacity ?? HubOptionsSetup.DefaultStreamBufferCapacity,
MaximumReceiveMessageSize = _maximumMessageSize,
+ SystemClock = SystemClock,
};
Log.ConnectedStarting(_logger);
@@ -223,8 +228,6 @@ namespace Microsoft.AspNetCore.SignalR
var result = await input.ReadAsync();
var buffer = result.Buffer;
- connection.ResetClientTimeout();
-
try
{
if (result.IsCanceled)
diff --git a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj
index 9a18d8ac74..385a449849 100644
--- a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj
+++ b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj
@@ -14,6 +14,8 @@
+
+
diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/MockSystemClock.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/MockSystemClock.cs
new file mode 100644
index 0000000000..2cd2eb617d
--- /dev/null
+++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/MockSystemClock.cs
@@ -0,0 +1,47 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading;
+using Microsoft.AspNetCore.Internal;
+
+namespace Microsoft.AspNetCore.SignalR.Tests
+{
+ public class MockSystemClock : ISystemClock
+ {
+ private static Random _random = new Random();
+
+ private long _utcNowTicks;
+
+ public MockSystemClock()
+ {
+ // Use a random DateTimeOffset to ensure tests that incorrectly use the current DateTimeOffset fail always instead of only rarely.
+ // Pick a date between the min DateTimeOffset and a day before the max DateTimeOffset so there's room to advance the clock.
+ _utcNowTicks = NextLong(DateTimeOffset.MinValue.Ticks, DateTimeOffset.MaxValue.Ticks - TimeSpan.FromDays(1).Ticks);
+ }
+
+ public DateTimeOffset UtcNow
+ {
+ get
+ {
+ UtcNowCalled++;
+ return new DateTimeOffset(Interlocked.Read(ref _utcNowTicks), TimeSpan.Zero);
+ }
+ set
+ {
+ Interlocked.Exchange(ref _utcNowTicks, value.Ticks);
+ }
+ }
+
+ public long UtcNowTicks => UtcNow.Ticks;
+
+ public DateTimeOffset UtcNowUnsynchronized => UtcNow;
+
+ public int UtcNowCalled { get; private set; }
+
+ private long NextLong(long minValue, long maxValue)
+ {
+ return (long)(_random.NextDouble() * (maxValue - minValue) + minValue);
+ }
+ }
+}
diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs
index aaf4cfa582..eb6d11a9dc 100644
--- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs
+++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs
@@ -14,10 +14,12 @@ using System.Threading;
using System.Threading.Tasks;
using MessagePack;
using MessagePack.Formatters;
+using MessagePack.Resolvers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Connections.Features;
+using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Internal;
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.AspNetCore.Testing;
@@ -2370,7 +2372,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
services.AddSignalR()
.AddMessagePackProtocol(options =>
{
- options.FormatterResolvers.Insert(0, new CustomFormatter());
+ options.SerializerOptions = MessagePackSerializerOptions.Standard.WithResolver(CompositeResolver.Create(new CustomFormatter(), options.SerializerOptions.Resolver));
});
}, LoggerFactory);
@@ -2659,24 +2661,25 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{
using (StartVerifiableLog())
{
+ var interval = 100;
+ var clock = new MockSystemClock();
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
services.Configure(options =>
- options.KeepAliveInterval = TimeSpan.FromMilliseconds(100)), LoggerFactory);
+ options.KeepAliveInterval = TimeSpan.FromMilliseconds(interval)), LoggerFactory);
var connectionHandler = serviceProvider.GetService>();
+ connectionHandler.SystemClock = clock;
using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
{
var connectionHandlerTask = await client.ConnectAsync(connectionHandler);
await client.Connected.OrTimeout();
- // Wait 500 ms, but make sure to yield some time up to unblock concurrent threads
- // This is useful on AppVeyor because it's slow enough to end up with no time
- // being available for the endpoint to run.
- for (var i = 0; i < 50; i += 1)
+ // Trigger multiple keep alives
+ var heartbeatCount = 5;
+ for (var i = 0; i < heartbeatCount; i++)
{
+ clock.UtcNow = clock.UtcNow.AddMilliseconds(interval + 1);
client.TickHeartbeat();
- await Task.Yield();
- await Task.Delay(10);
}
// Shut down
@@ -2710,7 +2713,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
break;
}
}
- Assert.InRange(pingCounter, 1, Int32.MaxValue);
+ Assert.Equal(heartbeatCount, pingCounter);
}
}
}
@@ -2720,10 +2723,13 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{
using (StartVerifiableLog())
{
+ var timeout = 100;
+ var clock = new MockSystemClock();
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
services.Configure(options =>
- options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(100)), LoggerFactory);
+ options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeout)), LoggerFactory);
var connectionHandler = serviceProvider.GetService>();
+ connectionHandler.SystemClock = clock;
using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
{
@@ -2731,9 +2737,16 @@ namespace Microsoft.AspNetCore.SignalR.Tests
await client.Connected.OrTimeout();
// This is a fake client -- it doesn't auto-ping to signal
- // We go over the 100 ms timeout interval...
- await Task.Delay(120);
- client.TickHeartbeat();
+ // We go over the 100 ms timeout interval multiple times
+ for (var i = 0; i < 3; i++)
+ {
+ clock.UtcNow = clock.UtcNow.AddMilliseconds(timeout + 1);
+ client.TickHeartbeat();
+ }
+
+ // Invoke a Hub method and wait for the result to reliably test if the connection is still active
+ var id = await client.SendInvocationAsync(nameof(MethodHub.ValueMethod)).OrTimeout();
+ var result = await client.ReadAsync().OrTimeout();
// but client should still be open, since it never pinged to activate the timeout checking
Assert.False(connectionHandlerTask.IsCompleted);
@@ -2746,10 +2759,13 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{
using (StartVerifiableLog())
{
+ var timeout = 100;
+ var clock = new MockSystemClock();
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
services.Configure(options =>
- options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(100)), LoggerFactory);
+ options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeout)), LoggerFactory);
var connectionHandler = serviceProvider.GetService>();
+ connectionHandler.SystemClock = clock;
using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
{
@@ -2757,10 +2773,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
await client.Connected.OrTimeout();
await client.SendHubMessageAsync(PingMessage.Instance);
- await Task.Delay(300);
- client.TickHeartbeat();
-
- await Task.Delay(300);
+ clock.UtcNow = clock.UtcNow.AddMilliseconds(timeout + 1);
client.TickHeartbeat();
await connectionHandlerTask.OrTimeout();
@@ -2774,10 +2787,13 @@ namespace Microsoft.AspNetCore.SignalR.Tests
{
using (StartVerifiableLog())
{
+ var timeout = 300;
+ var clock = new MockSystemClock();
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
services.Configure(options =>
- options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(300)), LoggerFactory);
+ options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeout)), LoggerFactory);
var connectionHandler = serviceProvider.GetService>();
+ connectionHandler.SystemClock = clock;
using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
{
@@ -2787,11 +2803,17 @@ namespace Microsoft.AspNetCore.SignalR.Tests
for (int i = 0; i < 10; i++)
{
- await Task.Delay(100);
+ clock.UtcNow = clock.UtcNow.AddMilliseconds(timeout - 1);
client.TickHeartbeat();
await client.SendHubMessageAsync(PingMessage.Instance);
}
+ // Invoke a Hub method and wait for the result to reliably test if the connection is still active
+ var id = await client.SendInvocationAsync(nameof(MethodHub.ValueMethod)).OrTimeout();
+ var result = await client.ReadAsync().OrTimeout();
+
+ Assert.IsType(result);
+
Assert.False(connectionHandlerTask.IsCompleted);
}
}
@@ -3277,7 +3299,7 @@ namespace Microsoft.AspNetCore.SignalR.Tests
[Fact]
public async Task ConnectionAbortedIfSendFailsWithProtocolError()
{
- using (StartVerifiableLog())
+ using (StartVerifiableLog(write => write.EventId.Name == "FailedWritingMessage"))
{
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
{
diff --git a/src/Testing/src/Logging/BeginScopeContext.cs b/src/Testing/src/Logging/BeginScopeContext.cs
new file mode 100644
index 0000000000..14ef991e0d
--- /dev/null
+++ b/src/Testing/src/Logging/BeginScopeContext.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.Extensions.Logging.Testing
+{
+ public class BeginScopeContext
+ {
+ public object Scope { get; set; }
+
+ public string LoggerName { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Testing/src/Logging/ITestSink.cs b/src/Testing/src/Logging/ITestSink.cs
new file mode 100644
index 0000000000..b328e5c595
--- /dev/null
+++ b/src/Testing/src/Logging/ITestSink.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Concurrent;
+
+namespace Microsoft.Extensions.Logging.Testing
+{
+ public interface ITestSink
+ {
+ event Action MessageLogged;
+
+ event Action ScopeStarted;
+
+ Func WriteEnabled { get; set; }
+
+ Func BeginEnabled { get; set; }
+
+ IProducerConsumerCollection Scopes { get; set; }
+
+ IProducerConsumerCollection Writes { get; set; }
+
+ void Write(WriteContext context);
+
+ void Begin(BeginScopeContext context);
+ }
+}
diff --git a/src/Testing/src/Logging/LogLevelAttribute.cs b/src/Testing/src/Logging/LogLevelAttribute.cs
new file mode 100644
index 0000000000..74aa395d4b
--- /dev/null
+++ b/src/Testing/src/Logging/LogLevelAttribute.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.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.Testing
+{
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false)]
+ public class LogLevelAttribute : Attribute
+ {
+ public LogLevelAttribute(LogLevel logLevel)
+ {
+ LogLevel = logLevel;
+ }
+
+ public LogLevel LogLevel { get; }
+ }
+}
diff --git a/src/Testing/src/Logging/LogValuesAssert.cs b/src/Testing/src/Logging/LogValuesAssert.cs
new file mode 100644
index 0000000000..ef2ff1f406
--- /dev/null
+++ b/src/Testing/src/Logging/LogValuesAssert.cs
@@ -0,0 +1,81 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit.Sdk;
+
+namespace Microsoft.Extensions.Logging.Testing
+{
+ public static class LogValuesAssert
+ {
+ ///
+ /// Asserts that the given key and value are present in the actual values.
+ ///
+ /// The key of the item to be found.
+ /// The value of the item to be found.
+ /// The actual values.
+ public static void Contains(
+ string key,
+ object value,
+ IEnumerable> actualValues)
+ {
+ Contains(new[] { new KeyValuePair(key, value) }, actualValues);
+ }
+
+ ///
+ /// Asserts that all the expected values are present in the actual values by ignoring
+ /// the order of values.
+ ///
+ /// Expected subset of values
+ /// Actual set of values
+ public static void Contains(
+ IEnumerable> expectedValues,
+ IEnumerable> actualValues)
+ {
+ if (expectedValues == null)
+ {
+ throw new ArgumentNullException(nameof(expectedValues));
+ }
+
+ if (actualValues == null)
+ {
+ throw new ArgumentNullException(nameof(actualValues));
+ }
+
+ var comparer = new LogValueComparer();
+
+ foreach (var expectedPair in expectedValues)
+ {
+ if (!actualValues.Contains(expectedPair, comparer))
+ {
+ throw new EqualException(
+ expected: GetString(expectedValues),
+ actual: GetString(actualValues));
+ }
+ }
+ }
+
+ private static string GetString(IEnumerable> logValues)
+ {
+ return string.Join(",", logValues.Select(kvp => $"[{kvp.Key} {kvp.Value}]"));
+ }
+
+ private class LogValueComparer : IEqualityComparer>
+ {
+ public bool Equals(KeyValuePair x, KeyValuePair y)
+ {
+ return string.Equals(x.Key, y.Key) && object.Equals(x.Value, y.Value);
+ }
+
+ public int GetHashCode(KeyValuePair obj)
+ {
+ // We are never going to put this KeyValuePair in a hash table,
+ // so this is ok.
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/src/Testing/src/Logging/TestLogger.cs b/src/Testing/src/Logging/TestLogger.cs
new file mode 100644
index 0000000000..1f1b1d6aba
--- /dev/null
+++ b/src/Testing/src/Logging/TestLogger.cs
@@ -0,0 +1,77 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.Testing
+{
+ public class TestLogger : ILogger
+ {
+ private object _scope;
+ private readonly ITestSink _sink;
+ private readonly string _name;
+ private readonly Func _filter;
+
+ public TestLogger(string name, ITestSink sink, bool enabled)
+ : this(name, sink, _ => enabled)
+ {
+ }
+
+ public TestLogger(string name, ITestSink sink, Func filter)
+ {
+ _sink = sink;
+ _name = name;
+ _filter = filter;
+ }
+
+ public string Name { get; set; }
+
+ public IDisposable BeginScope(TState state)
+ {
+ _scope = state;
+
+ _sink.Begin(new BeginScopeContext()
+ {
+ LoggerName = _name,
+ Scope = state,
+ });
+
+ return TestDisposable.Instance;
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel))
+ {
+ return;
+ }
+
+ _sink.Write(new WriteContext()
+ {
+ LogLevel = logLevel,
+ EventId = eventId,
+ State = state,
+ Exception = exception,
+ Formatter = (s, e) => formatter((TState)s, e),
+ LoggerName = _name,
+ Scope = _scope
+ });
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return logLevel != LogLevel.None && _filter(logLevel);
+ }
+
+ private class TestDisposable : IDisposable
+ {
+ public static readonly TestDisposable Instance = new TestDisposable();
+
+ public void Dispose()
+ {
+ // intentionally does nothing
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Testing/src/Logging/TestLoggerFactory.cs b/src/Testing/src/Logging/TestLoggerFactory.cs
new file mode 100644
index 0000000000..a7f2f1398c
--- /dev/null
+++ b/src/Testing/src/Logging/TestLoggerFactory.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.Extensions.Logging.Testing
+{
+ public class TestLoggerFactory : ILoggerFactory
+ {
+ private readonly ITestSink _sink;
+ private readonly bool _enabled;
+
+ public TestLoggerFactory(ITestSink sink, bool enabled)
+ {
+ _sink = sink;
+ _enabled = enabled;
+ }
+
+ public ILogger CreateLogger(string name)
+ {
+ return new TestLogger(name, _sink, _enabled);
+ }
+
+ public void AddProvider(ILoggerProvider provider)
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/src/Testing/src/Logging/TestLoggerProvider.cs b/src/Testing/src/Logging/TestLoggerProvider.cs
new file mode 100644
index 0000000000..e604bda36e
--- /dev/null
+++ b/src/Testing/src/Logging/TestLoggerProvider.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.Extensions.Logging.Testing
+{
+ public class TestLoggerProvider : ILoggerProvider
+ {
+ private readonly ITestSink _sink;
+
+ public TestLoggerProvider(ITestSink sink)
+ {
+ _sink = sink;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new TestLogger(categoryName, _sink, enabled: true);
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/src/Testing/src/Logging/TestLoggerT.cs b/src/Testing/src/Logging/TestLoggerT.cs
new file mode 100644
index 0000000000..096bb96535
--- /dev/null
+++ b/src/Testing/src/Logging/TestLoggerT.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.Testing
+{
+ public class TestLogger : ILogger
+ {
+ private readonly ILogger _logger;
+
+ public TestLogger(TestLoggerFactory factory)
+ {
+ _logger = factory.CreateLogger();
+ }
+
+ public IDisposable BeginScope(TState state)
+ {
+ return _logger.BeginScope(state);
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return _logger.IsEnabled(logLevel);
+ }
+
+ public void Log(
+ LogLevel logLevel,
+ EventId eventId,
+ TState state,
+ Exception exception,
+ Func formatter)
+ {
+ _logger.Log(logLevel, eventId, state, exception, formatter);
+ }
+ }
+}
diff --git a/src/Testing/src/Logging/TestSink.cs b/src/Testing/src/Logging/TestSink.cs
new file mode 100644
index 0000000000..5285b3068f
--- /dev/null
+++ b/src/Testing/src/Logging/TestSink.cs
@@ -0,0 +1,66 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Concurrent;
+
+namespace Microsoft.Extensions.Logging.Testing
+{
+ public class TestSink : ITestSink
+ {
+ private ConcurrentQueue _scopes;
+ private ConcurrentQueue _writes;
+
+ public TestSink(
+ Func writeEnabled = null,
+ Func beginEnabled = null)
+ {
+ WriteEnabled = writeEnabled;
+ BeginEnabled = beginEnabled;
+
+ _scopes = new ConcurrentQueue();
+ _writes = new ConcurrentQueue();
+ }
+
+ public Func WriteEnabled { get; set; }
+
+ public Func BeginEnabled { get; set; }
+
+ public IProducerConsumerCollection Scopes { get => _scopes; set => _scopes = new ConcurrentQueue(value); }
+
+ public IProducerConsumerCollection Writes { get => _writes; set => _writes = new ConcurrentQueue(value); }
+
+ public event Action MessageLogged;
+
+ public event Action ScopeStarted;
+
+ public void Write(WriteContext context)
+ {
+ if (WriteEnabled == null || WriteEnabled(context))
+ {
+ _writes.Enqueue(context);
+ }
+ MessageLogged?.Invoke(context);
+ }
+
+ public void Begin(BeginScopeContext context)
+ {
+ if (BeginEnabled == null || BeginEnabled(context))
+ {
+ _scopes.Enqueue(context);
+ }
+ ScopeStarted?.Invoke(context);
+ }
+
+ public static bool EnableWithTypeName(WriteContext context)
+ {
+ return context.LoggerName.Equals(typeof(T).FullName);
+ }
+
+ public static bool EnableWithTypeName(BeginScopeContext context)
+ {
+ return context.LoggerName.Equals(typeof(T).FullName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Testing/src/Logging/WriteContext.cs b/src/Testing/src/Logging/WriteContext.cs
new file mode 100644
index 0000000000..0ecfc8f1a9
--- /dev/null
+++ b/src/Testing/src/Logging/WriteContext.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.Testing
+{
+ public class WriteContext
+ {
+ public LogLevel LogLevel { get; set; }
+
+ public EventId EventId { get; set; }
+
+ public object State { get; set; }
+
+ public Exception Exception { get; set; }
+
+ public Func Formatter { get; set; }
+
+ public object Scope { get; set; }
+
+ public string LoggerName { get; set; }
+
+ public string Message
+ {
+ get
+ {
+ return Formatter(State, Exception);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Testing/src/Logging/XunitLoggerFactoryExtensions.cs b/src/Testing/src/Logging/XunitLoggerFactoryExtensions.cs
new file mode 100644
index 0000000000..7d053d45dd
--- /dev/null
+++ b/src/Testing/src/Logging/XunitLoggerFactoryExtensions.cs
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Testing;
+using Xunit.Abstractions;
+
+namespace Microsoft.Extensions.Logging
+{
+ public static class XunitLoggerFactoryExtensions
+ {
+ public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output)
+ {
+ builder.Services.AddSingleton(new XunitLoggerProvider(output));
+ return builder;
+ }
+
+ public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel)
+ {
+ builder.Services.AddSingleton(new XunitLoggerProvider(output, minLevel));
+ return builder;
+ }
+
+ public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart)
+ {
+ builder.Services.AddSingleton(new XunitLoggerProvider(output, minLevel, logStart));
+ return builder;
+ }
+
+ public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output)
+ {
+ loggerFactory.AddProvider(new XunitLoggerProvider(output));
+ return loggerFactory;
+ }
+
+ public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel)
+ {
+ loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel));
+ return loggerFactory;
+ }
+
+ public static ILoggerFactory AddXunit(this ILoggerFactory loggerFactory, ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart)
+ {
+ loggerFactory.AddProvider(new XunitLoggerProvider(output, minLevel, logStart));
+ return loggerFactory;
+ }
+ }
+}
diff --git a/src/Testing/src/Logging/XunitLoggerProvider.cs b/src/Testing/src/Logging/XunitLoggerProvider.cs
new file mode 100644
index 0000000000..3a1d751413
--- /dev/null
+++ b/src/Testing/src/Logging/XunitLoggerProvider.cs
@@ -0,0 +1,127 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Text;
+using Xunit.Abstractions;
+
+namespace Microsoft.Extensions.Logging.Testing
+{
+ public class XunitLoggerProvider : ILoggerProvider
+ {
+ private readonly ITestOutputHelper _output;
+ private readonly LogLevel _minLevel;
+ private readonly DateTimeOffset? _logStart;
+
+ public XunitLoggerProvider(ITestOutputHelper output)
+ : this(output, LogLevel.Trace)
+ {
+ }
+
+ public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel)
+ : this(output, minLevel, null)
+ {
+ }
+
+ public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart)
+ {
+ _output = output;
+ _minLevel = minLevel;
+ _logStart = logStart;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new XunitLogger(_output, categoryName, _minLevel, _logStart);
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+
+ public class XunitLogger : ILogger
+ {
+ private static readonly string[] NewLineChars = new[] { Environment.NewLine };
+ private readonly string _category;
+ private readonly LogLevel _minLogLevel;
+ private readonly ITestOutputHelper _output;
+ private DateTimeOffset? _logStart;
+
+ public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel, DateTimeOffset? logStart)
+ {
+ _minLogLevel = minLogLevel;
+ _category = category;
+ _output = output;
+ _logStart = logStart;
+ }
+
+ public void Log(
+ LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel))
+ {
+ return;
+ }
+
+ // Buffer the message into a single string in order to avoid shearing the message when running across multiple threads.
+ var messageBuilder = new StringBuilder();
+
+ var timestamp = _logStart.HasValue ? $"{(DateTimeOffset.UtcNow - _logStart.Value).TotalSeconds.ToString("N3")}s" : DateTimeOffset.UtcNow.ToString("s");
+
+ var firstLinePrefix = $"| [{timestamp}] {_category} {logLevel}: ";
+ var lines = formatter(state, exception).Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries);
+ messageBuilder.AppendLine(firstLinePrefix + lines.FirstOrDefault() ?? string.Empty);
+
+ var additionalLinePrefix = "|" + new string(' ', firstLinePrefix.Length - 1);
+ foreach (var line in lines.Skip(1))
+ {
+ messageBuilder.AppendLine(additionalLinePrefix + line);
+ }
+
+ if (exception != null)
+ {
+ lines = exception.ToString().Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries);
+ additionalLinePrefix = "| ";
+ foreach (var line in lines)
+ {
+ messageBuilder.AppendLine(additionalLinePrefix + line);
+ }
+ }
+
+ // Remove the last line-break, because ITestOutputHelper only has WriteLine.
+ var message = messageBuilder.ToString();
+ if (message.EndsWith(Environment.NewLine))
+ {
+ message = message.Substring(0, message.Length - Environment.NewLine.Length);
+ }
+
+ try
+ {
+ _output.WriteLine(message);
+ }
+ catch (Exception)
+ {
+ // We could fail because we're on a background thread and our captured ITestOutputHelper is
+ // busted (if the test "completed" before the background thread fired).
+ // So, ignore this. There isn't really anything we can do but hope the
+ // caller has additional loggers registered
+ }
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ => logLevel >= _minLogLevel;
+
+ public IDisposable BeginScope(TState state)
+ => new NullScope();
+
+ private class NullScope : IDisposable
+ {
+ public void Dispose()
+ {
+ }
+ }
+ }
+}
diff --git a/src/Testing/src/Microsoft.AspNetCore.Testing.csproj b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj
index 5ddad7b645..7d95e026a7 100644
--- a/src/Testing/src/Microsoft.AspNetCore.Testing.csproj
+++ b/src/Testing/src/Microsoft.AspNetCore.Testing.csproj
@@ -19,7 +19,9 @@
-
+
+
+
diff --git a/src/Testing/test/LogValuesAssertTest.cs b/src/Testing/test/LogValuesAssertTest.cs
new file mode 100644
index 0000000000..dc2db9d83d
--- /dev/null
+++ b/src/Testing/test/LogValuesAssertTest.cs
@@ -0,0 +1,222 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Microsoft.Extensions.Logging.Testing.Tests
+{
+ public class LogValuesAssertTest
+ {
+ public static TheoryData<
+ IEnumerable>,
+ IEnumerable>> ExpectedValues_SubsetOf_ActualValuesData
+ {
+ get
+ {
+ return new TheoryData<
+ IEnumerable>,
+ IEnumerable>>()
+ {
+ {
+ new KeyValuePair[] { },
+ new KeyValuePair[] { }
+ },
+ {
+ // subset
+ new KeyValuePair[] { },
+ new[]
+ {
+ new KeyValuePair("RouteValue", "Failure"),
+ new KeyValuePair("RouteKey", "id")
+ }
+ },
+ {
+ // subset
+ new[]
+ {
+ new KeyValuePair("RouteValue", "Failure"),
+ new KeyValuePair("RouteKey", "id")
+ },
+ new[]
+ {
+ new KeyValuePair("RouteValue", "Failure"),
+ new KeyValuePair("RouteKey", "id"),
+ new KeyValuePair("RouteConstraint", "Something")
+ }
+ },
+ {
+ // equal number of values
+ new[]
+ {
+ new KeyValuePair("RouteValue", "Failure"),
+ new KeyValuePair("RouteKey", "id")
+ },
+ new[]
+ {
+ new KeyValuePair("RouteValue", "Failure"),
+ new KeyValuePair("RouteKey", "id"),
+ }
+ }
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ExpectedValues_SubsetOf_ActualValuesData))]
+ public void Asserts_Success_ExpectedValues_SubsetOf_ActualValues(
+ IEnumerable> expectedValues,
+ IEnumerable> actualValues)
+ {
+ // Act && Assert
+ LogValuesAssert.Contains(expectedValues, actualValues);
+ }
+
+ public static TheoryData<
+ IEnumerable>,
+ IEnumerable>> ExpectedValues_MoreThan_ActualValuesData
+ {
+ get
+ {
+ return new TheoryData<
+ IEnumerable>,
+ IEnumerable>>()
+ {
+ {
+ new[]
+ {
+ new KeyValuePair("RouteValue", "Failure"),
+ new KeyValuePair("RouteKey", "id")
+ },
+ new KeyValuePair[] { }
+ },
+ {
+ new[]
+ {
+ new KeyValuePair("RouteValue", "Failure"),
+ new KeyValuePair("RouteKey", "id"),
+ new KeyValuePair("RouteConstraint", "Something")
+ },
+ new[]
+ {
+ new KeyValuePair("RouteValue", "Failure"),
+ new KeyValuePair("RouteKey", "id")
+ }
+ }
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(ExpectedValues_MoreThan_ActualValuesData))]
+ public void Asserts_Failure_ExpectedValues_MoreThan_ActualValues(
+ IEnumerable> expectedValues,
+ IEnumerable> actualValues)
+ {
+ // Act && Assert
+ var equalException = Assert.Throws(
+ () => LogValuesAssert.Contains(expectedValues, actualValues));
+
+ Assert.Equal(GetString(expectedValues), equalException.Expected);
+ Assert.Equal(GetString(actualValues), equalException.Actual);
+ }
+
+ [Fact]
+ public void Asserts_Success_IgnoringOrderOfItems()
+ {
+ // Arrange
+ var expectedLogValues = new[]
+ {
+ new KeyValuePair("RouteConstraint", "Something"),
+ new KeyValuePair("RouteValue", "Failure"),
+ new KeyValuePair("RouteKey", "id")
+ };
+ var actualLogValues = new[]
+ {
+ new KeyValuePair("RouteKey", "id"),
+ new KeyValuePair("RouteConstraint", "Something"),
+ new KeyValuePair("RouteValue", "Failure"),
+ };
+
+ // Act && Assert
+ LogValuesAssert.Contains(expectedLogValues, actualLogValues);
+ }
+
+ [Fact]
+ public void Asserts_Success_OnSpecifiedKeyAndValue()
+ {
+ // Arrange
+ var actualLogValues = new[]
+ {
+ new KeyValuePair("RouteConstraint", "Something"),
+ new KeyValuePair("RouteKey", "id"),
+ new KeyValuePair("RouteValue", "Failure"),
+ };
+
+ // Act && Assert
+ LogValuesAssert.Contains("RouteKey", "id", actualLogValues);
+ }
+
+ public static TheoryData<
+ IEnumerable>,
+ IEnumerable>> CaseSensitivityComparisionData
+ {
+ get
+ {
+ return new TheoryData<
+ IEnumerable>,
+ IEnumerable>>()
+ {
+ {
+ new[]
+ {
+ new KeyValuePair("RouteKey", "id"),
+ new KeyValuePair("RouteValue", "Failure"),
+ },
+ new[]
+ {
+ new KeyValuePair("ROUTEKEY", "id"),
+ new KeyValuePair("RouteValue", "Failure"),
+ }
+ },
+ {
+ new[]
+ {
+ new KeyValuePair("RouteKey", "id"),
+ new KeyValuePair("RouteValue", "Failure"),
+ },
+ new[]
+ {
+ new KeyValuePair("RouteKey", "id"),
+ new KeyValuePair("RouteValue", "FAILURE"),
+ }
+ }
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(CaseSensitivityComparisionData))]
+ public void DefaultComparer_Performs_CaseSensitiveComparision(
+ IEnumerable> expectedValues,
+ IEnumerable> actualValues)
+ {
+ // Act && Assert
+ var equalException = Assert.Throws(
+ () => LogValuesAssert.Contains(expectedValues, actualValues));
+
+ Assert.Equal(GetString(expectedValues), equalException.Expected);
+ Assert.Equal(GetString(actualValues), equalException.Actual);
+ }
+
+ private string GetString(IEnumerable> logValues)
+ {
+ return logValues == null ?
+ "Null" :
+ string.Join(",", logValues.Select(kvp => $"[{kvp.Key} {kvp.Value}]"));
+ }
+ }
+}
diff --git a/src/Testing/test/XunitLoggerProviderTest.cs b/src/Testing/test/XunitLoggerProviderTest.cs
new file mode 100644
index 0000000000..e43447c465
--- /dev/null
+++ b/src/Testing/test/XunitLoggerProviderTest.cs
@@ -0,0 +1,95 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Text.RegularExpressions;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.Testing.Tests
+{
+ public class XunitLoggerProviderTest
+ {
+ [Fact]
+ public void LoggerProviderWritesToTestOutputHelper()
+ {
+ var testTestOutputHelper = new TestTestOutputHelper();
+
+ var loggerFactory = CreateTestLogger(builder => builder
+ .SetMinimumLevel(LogLevel.Trace)
+ .AddXunit(testTestOutputHelper));
+
+ var logger = loggerFactory.CreateLogger("TestCategory");
+ logger.LogInformation("This is some great information");
+ logger.LogTrace("This is some unimportant information");
+
+ var expectedOutput =
+ "| [TIMESTAMP] TestCategory Information: This is some great information" + Environment.NewLine +
+ "| [TIMESTAMP] TestCategory Trace: This is some unimportant information" + Environment.NewLine;
+
+ Assert.Equal(expectedOutput, MakeConsistent(testTestOutputHelper.Output));
+ }
+
+ [Fact]
+ public void LoggerProviderDoesNotWriteLogMessagesBelowMinimumLevel()
+ {
+ var testTestOutputHelper = new TestTestOutputHelper();
+ var loggerFactory = CreateTestLogger(builder => builder
+ .AddXunit(testTestOutputHelper, LogLevel.Warning));
+
+ var logger = loggerFactory.CreateLogger("TestCategory");
+ logger.LogInformation("This is some great information");
+ logger.LogError("This is a bad error");
+
+ Assert.Equal("| [TIMESTAMP] TestCategory Error: This is a bad error" + Environment.NewLine, MakeConsistent(testTestOutputHelper.Output));
+ }
+
+ [Fact]
+ public void LoggerProviderPrependsPrefixToEachLine()
+ {
+ var testTestOutputHelper = new TestTestOutputHelper();
+ var loggerFactory = CreateTestLogger(builder => builder
+ .AddXunit(testTestOutputHelper));
+
+ var logger = loggerFactory.CreateLogger("TestCategory");
+ logger.LogInformation("This is a" + Environment.NewLine + "multi-line" + Environment.NewLine + "message");
+
+ // The lines after the first one are indented more because the indentation was calculated based on the timestamp's actual length.
+ var expectedOutput =
+ "| [TIMESTAMP] TestCategory Information: This is a" + Environment.NewLine +
+ "| multi-line" + Environment.NewLine +
+ "| message" + Environment.NewLine;
+
+ Assert.Equal(expectedOutput, MakeConsistent(testTestOutputHelper.Output));
+ }
+
+ [Fact]
+ public void LoggerProviderDoesNotThrowIfOutputHelperThrows()
+ {
+ var testTestOutputHelper = new TestTestOutputHelper();
+ var loggerFactory = CreateTestLogger(builder => builder
+
+ .AddXunit(testTestOutputHelper));
+
+ testTestOutputHelper.Throw = true;
+
+ var logger = loggerFactory.CreateLogger("TestCategory");
+ logger.LogInformation("This is a" + Environment.NewLine + "multi-line" + Environment.NewLine + "message");
+
+ Assert.Equal(0, testTestOutputHelper.Output.Length);
+ }
+
+ private static readonly Regex TimestampRegex = new Regex(@"\d+-\d+-\d+T\d+:\d+:\d+");
+
+ private string MakeConsistent(string input) => TimestampRegex.Replace(input, "TIMESTAMP");
+
+ private static ILoggerFactory CreateTestLogger(Action configure)
+ {
+ return new ServiceCollection()
+ .AddLogging(configure)
+ .BuildServiceProvider()
+ .GetRequiredService();
+ }
+ }
+}
diff --git a/src/Tools/.vsconfig b/src/Tools/.vsconfig
new file mode 100644
index 0000000000..7a520fe61c
--- /dev/null
+++ b/src/Tools/.vsconfig
@@ -0,0 +1,12 @@
+{
+ "version": "1.0",
+ "components": [
+ "Microsoft.Net.Component.4.6.1.TargetingPack",
+ "Microsoft.Net.Component.4.7.2.SDK",
+ "Microsoft.Net.Component.4.7.2.TargetingPack",
+ "Microsoft.VisualStudio.Workload.ManagedDesktop",
+ "Microsoft.VisualStudio.Workload.NetCoreTools",
+ "Microsoft.VisualStudio.Workload.NetWeb",
+ "Microsoft.VisualStudio.Workload.VisualStudioExtension"
+ ]
+}
diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs
index d5fa7c9f50..6e25f0ccd6 100644
--- a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs
+++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiAddURLTests.cs
@@ -424,7 +424,7 @@ namespace Microsoft.DotNet.OpenApi.Add.Tests
{
var project = CreateBasicProject(withOpenApi: false);
- var app = GetApplication(realHttp: true);
+ var app = GetApplication();
var url = BrokenUrl;
var run = app.Execute(new[] { "add", "url", url });
@@ -446,34 +446,5 @@ namespace Microsoft.DotNet.OpenApi.Add.Tests
var jsonFile = Path.Combine(_tempDir.Root, expectedJsonName);
Assert.False(File.Exists(jsonFile));
}
-
- [Fact]
- public void OpenApi_Add_URL_ActualResponse()
- {
- var project = CreateBasicProject(withOpenApi: false);
-
- var app = GetApplication(realHttp: true);
- var url = ActualUrl;
- var run = app.Execute(new[] { "add", "url", url });
-
- AssertNoErrors(run);
-
- app = GetApplication(realHttp: true);
- run = app.Execute(new[] { "add", "url", url });
-
- AssertNoErrors(run);
-
- // csproj contents
- var csproj = new FileInfo(project.Project.Path);
- using var csprojStream = csproj.OpenRead();
- using var reader = new StreamReader(csprojStream);
- var content = reader.ReadToEnd();
- var escapedPkgRef = Regex.Escape("", content);
- }
}
}
diff --git a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs
index fb228eeb96..b8bf1cb8c3 100644
--- a/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs
+++ b/src/Tools/Microsoft.dotnet-openapi/test/OpenApiTestBase.cs
@@ -106,7 +106,8 @@ namespace Microsoft.DotNet.OpenApi.Tests
{ PackageUrl, Tuple.Create(PackageUrlContent, null) },
{ NoDispositionUrl, Tuple.Create(Content, null) },
{ NoExtensionUrl, Tuple.Create(Content, noExtension) },
- { NoSegmentUrl, Tuple.Create(Content, justAttachments) }
+ { NoSegmentUrl, Tuple.Create(Content, justAttachments) },
+ { BrokenUrl, null }
};
}
@@ -139,10 +140,14 @@ namespace Microsoft.DotNet.OpenApi.Tests
public Task GetResponseAsync(string url)
{
var result = _results[url];
- byte[] byteArray = Encoding.ASCII.GetBytes(result.Item1);
- var stream = new MemoryStream(byteArray);
+ MemoryStream stream = null;
+ if(result != null)
+ {
+ byte[] byteArray = Encoding.ASCII.GetBytes(result.Item1);
+ stream = new MemoryStream(byteArray);
+ }
- return Task.FromResult(new TestHttpResponseMessageWrapper(stream, result.Item2));
+ return Task.FromResult(new TestHttpResponseMessageWrapper(stream, result?.Item2));
}
}
@@ -154,7 +159,17 @@ namespace Microsoft.DotNet.OpenApi.Tests
public bool IsSuccessCode()
{
- return true;
+ switch(StatusCode)
+ {
+ case HttpStatusCode.OK:
+ case HttpStatusCode.Created:
+ case HttpStatusCode.NoContent:
+ case HttpStatusCode.Accepted:
+ return true;
+ case HttpStatusCode.NotFound:
+ default:
+ return false;
+ }
}
private readonly ContentDispositionHeaderValue _contentDisposition;
@@ -164,6 +179,10 @@ namespace Microsoft.DotNet.OpenApi.Tests
ContentDispositionHeaderValue header)
{
Stream = Task.FromResult(stream);
+ if (header is null && stream is null)
+ {
+ StatusCode = HttpStatusCode.NotFound;
+ }
_contentDisposition = header;
}
diff --git a/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs b/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs
index 5b8b038596..cbbcee8233 100644
--- a/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs
+++ b/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs
@@ -76,7 +76,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
var projectPath = ResolveProjectPath(ProjectPath, WorkingDirectory);
// Load the project file as XML
- var projectDocument = XDocument.Load(projectPath);
+ var projectDocument = XDocument.Load(projectPath, LoadOptions.PreserveWhitespace);
// Accept the `--id` CLI option to the main app
string newSecretsId = string.IsNullOrWhiteSpace(OverrideId)
@@ -120,19 +120,18 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
}
// Add UserSecretsId element
+ propertyGroup.Add(" ");
propertyGroup.Add(new XElement("UserSecretsId", newSecretsId));
+ propertyGroup.Add($"{Environment.NewLine} ");
}
var settings = new XmlWriterSettings
{
- Indent = true,
OmitXmlDeclaration = true,
};
- using (var xw = XmlWriter.Create(projectPath, settings))
- {
- projectDocument.Save(xw);
- }
+ using var xw = XmlWriter.Create(projectPath, settings);
+ projectDocument.Save(xw);
context.Reporter.Output(Resources.FormatMessage_SetUserSecretsIdForProject(newSecretsId, projectPath));
}
diff --git a/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs b/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs
index d299c208ca..acfda466b7 100644
--- a/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs
+++ b/src/Tools/dotnet-user-secrets/test/InitCommandTest.cs
@@ -3,6 +3,7 @@
using System;
using System.IO;
+using System.Linq;
using System.Text;
using System.Xml.Linq;
using Microsoft.AspNetCore.Testing;
@@ -72,6 +73,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
}
[Fact]
+ [QuarantinedTest]
public void DoesNotGenerateIdForProjectWithSecretId()
{
const string SecretId = "AlreadyExists";
@@ -97,6 +99,22 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
Assert.Null(projectDocument.Declaration);
}
+ [Fact]
+ public void DoesNotRemoveBlankLines()
+ {
+ var projectDir = _fixture.CreateProject(null);
+ var projectFile = Path.Combine(projectDir, "TestProject.csproj");
+ var projectDocumentWithoutSecret = XDocument.Load(projectFile, LoadOptions.PreserveWhitespace);
+ var lineCountWithoutSecret = projectDocumentWithoutSecret.ToString().Split(Environment.NewLine).Length;
+
+ new InitCommand(null, null).Execute(MakeCommandContext(), projectDir);
+
+ var projectDocumentWithSecret = XDocument.Load(projectFile, LoadOptions.PreserveWhitespace);
+ var lineCountWithSecret = projectDocumentWithSecret.ToString().Split(Environment.NewLine).Length;
+
+ Assert.True(lineCountWithSecret == lineCountWithoutSecret + 1);
+ }
+
[Fact]
public void OverridesIdForProjectWithSecretId()
{
diff --git a/src/Tools/dotnet-watch/test/ProgramTests.cs b/src/Tools/dotnet-watch/test/ProgramTests.cs
index 40c2af5214..478fc6c846 100644
--- a/src/Tools/dotnet-watch/test/ProgramTests.cs
+++ b/src/Tools/dotnet-watch/test/ProgramTests.cs
@@ -24,6 +24,7 @@ namespace Microsoft.DotNet.Watcher.Tools.Tests
}
[Fact]
+ [QuarantinedTest]
public async Task ConsoleCancelKey()
{
_tempDir