From 1ccd62bcd29019d65e37863a2d466fedf402fc38 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 28 Jun 2018 16:48:50 -0700 Subject: [PATCH 01/41] Bumping version from 2.2.0 to 3.0.0 --- version.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.props b/version.props index 44985cedb3..71a78cddd8 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ - + - 2.2.0 - preview1 + 3.0.0 + alpha1 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 From b141c676cab7b10ea37dd39eb211a1ab0b1ee6e8 Mon Sep 17 00:00:00 2001 From: "Nate McMaster (automated)" Date: Mon, 2 Jul 2018 12:40:22 -0700 Subject: [PATCH 02/41] [automated] Change default branch to master --- .appveyor.yml | 2 +- .travis.yml | 2 +- .vsts-pipelines/builds/ci-internal.yml | 4 ++-- .vsts-pipelines/builds/ci-public.yml | 6 +++--- korebuild.json | 4 ++-- run.ps1 | 6 +++--- run.sh | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 4eea96ab69..d45bd5a1f8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,7 +2,7 @@ init: - git config --global core.autocrlf true branches: only: - - dev + - master - /^release\/.*$/ - /^(.*\/)?ci-.*$/ build_script: diff --git a/.travis.yml b/.travis.yml index 64bdbb4441..ab3980055c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ addons: - libunwind8 branches: only: - - dev + - master - /^release\/.*$/ - /^(.*\/)?ci-.*$/ before_install: diff --git a/.vsts-pipelines/builds/ci-internal.yml b/.vsts-pipelines/builds/ci-internal.yml index d7ceb76378..c2c5336fd0 100644 --- a/.vsts-pipelines/builds/ci-internal.yml +++ b/.vsts-pipelines/builds/ci-internal.yml @@ -1,5 +1,5 @@ trigger: -- dev +- master - release/* resources: @@ -7,7 +7,7 @@ resources: - repository: buildtools type: git name: aspnet-BuildTools - ref: refs/heads/dev + ref: refs/heads/master phases: - template: .vsts-pipelines/templates/project-ci.yml@buildtools diff --git a/.vsts-pipelines/builds/ci-public.yml b/.vsts-pipelines/builds/ci-public.yml index b7f25723f8..507c89b025 100644 --- a/.vsts-pipelines/builds/ci-public.yml +++ b/.vsts-pipelines/builds/ci-public.yml @@ -1,5 +1,5 @@ trigger: -- dev +- master - release/* # See https://github.com/aspnet/BuildTools @@ -9,7 +9,7 @@ resources: type: github endpoint: DotNet-Bot GitHub Connection name: aspnet/BuildTools - ref: refs/heads/dev - + ref: refs/heads/master + phases: - template: .vsts-pipelines/templates/project-ci.yml@buildtools diff --git a/korebuild.json b/korebuild.json index bd5d51a51b..8a276a7f35 100644 --- a/korebuild.json +++ b/korebuild.json @@ -1,4 +1,4 @@ { - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev" + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", + "channel": "master" } diff --git a/run.ps1 b/run.ps1 index 3b27382468..34604c7175 100644 --- a/run.ps1 +++ b/run.ps1 @@ -52,8 +52,8 @@ in the file are overridden by command line parameters. Example config file: ```json { - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev", + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", + "channel": "master", "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" } ``` @@ -192,7 +192,7 @@ if (!$DotNetHome) { else { Join-Path $PSScriptRoot '.dotnet'} } -if (!$Channel) { $Channel = 'dev' } +if (!$Channel) { $Channel = 'master' } if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } # Execute diff --git a/run.sh b/run.sh index 02aac15874..61f7a53385 100755 --- a/run.sh +++ b/run.sh @@ -248,7 +248,7 @@ if [ -f "$config_file" ]; then [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" fi -[ -z "$channel" ] && channel='dev' +[ -z "$channel" ] && channel='master' [ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' get_korebuild From d5041b9a333ea355266d0e142aa3cccc87aaaa0b Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Tue, 3 Jul 2018 16:13:09 +0000 Subject: [PATCH 03/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 127314b44e..d5056b5fb9 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,17 +3,17 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.2.0-preview1-17090 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 - 2.2.0-preview1-34530 + 3.0.0-alpha1-10000 + 3.0.0-alpha1-10016 + 3.0.0-alpha1-10016 + 3.0.0-alpha1-10016 + 3.0.0-alpha1-10016 + 3.0.0-alpha1-10016 + 3.0.0-alpha1-10016 + 3.0.0-alpha1-10016 + 3.0.0-alpha1-10016 + 3.0.0-alpha1-10016 + 3.0.0-alpha1-10016 2.0.0 2.1.0 2.2.0-preview1-26618-02 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index a8109db529..f0b76184fd 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.2.0-preview1-17090 -commithash:b19e903e946579cd9482089bce7d917e8bacd765 +version:3.0.0-alpha1-10000 +commithash:b7b88d08d55abc8b71de9abf16e26fc713e332cd From 405fdc3c1e9a935f294562fef3ec91edfd6c3c60 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 8 Jul 2018 19:54:40 +0000 Subject: [PATCH 04/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 24 ++++++++++++------------ korebuild-lock.txt | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index d5056b5fb9..518d17f26d 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,17 +3,17 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 3.0.0-alpha1-10000 - 3.0.0-alpha1-10016 - 3.0.0-alpha1-10016 - 3.0.0-alpha1-10016 - 3.0.0-alpha1-10016 - 3.0.0-alpha1-10016 - 3.0.0-alpha1-10016 - 3.0.0-alpha1-10016 - 3.0.0-alpha1-10016 - 3.0.0-alpha1-10016 - 3.0.0-alpha1-10016 + 3.0.0-alpha1-10005 + 3.0.0-alpha1-10044 + 3.0.0-alpha1-10044 + 3.0.0-alpha1-10044 + 3.0.0-alpha1-10044 + 3.0.0-alpha1-10044 + 3.0.0-alpha1-10044 + 3.0.0-alpha1-10044 + 3.0.0-alpha1-10044 + 3.0.0-alpha1-10044 + 3.0.0-alpha1-10044 2.0.0 2.1.0 2.2.0-preview1-26618-02 @@ -24,7 +24,7 @@ 4.6.0-preview1-26617-01 0.8.0 2.3.1 - 2.4.0-beta.1.build3945 + 2.4.0-rc.1.build4038 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index f0b76184fd..f357ac9f7d 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-10000 -commithash:b7b88d08d55abc8b71de9abf16e26fc713e332cd +version:3.0.0-alpha1-10005 +commithash:05570853de976a526462ca140a55b1ac59d9a351 From 468496b4ad76f14966a6726cdc4fc613dea8a645 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 15 Jul 2018 19:58:02 +0000 Subject: [PATCH 05/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 32 ++++++++++++++++---------------- korebuild-lock.txt | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 518d17f26d..673328908f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,26 +3,26 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 3.0.0-alpha1-10005 - 3.0.0-alpha1-10044 - 3.0.0-alpha1-10044 - 3.0.0-alpha1-10044 - 3.0.0-alpha1-10044 - 3.0.0-alpha1-10044 - 3.0.0-alpha1-10044 - 3.0.0-alpha1-10044 - 3.0.0-alpha1-10044 - 3.0.0-alpha1-10044 - 3.0.0-alpha1-10044 - 2.0.0 - 2.1.0 + 3.0.0-alpha1-10009 + 3.0.0-alpha1-10081 + 3.0.0-alpha1-10081 + 3.0.0-alpha1-10081 + 3.0.0-alpha1-10081 + 3.0.0-alpha1-10081 + 3.0.0-alpha1-10081 + 3.0.0-alpha1-10081 + 3.0.0-alpha1-10081 + 3.0.0-alpha1-10081 + 3.0.0-alpha1-10081 + 2.0.9 + 2.1.2 2.2.0-preview1-26618-02 15.6.1 4.7.49 2.0.3 - 4.6.0-preview1-26617-01 - 4.6.0-preview1-26617-01 - 0.8.0 + 4.6.0-preview1-26708-04 + 4.6.0-preview1-26708-04 + 0.9.0 2.3.1 2.4.0-rc.1.build4038 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index f357ac9f7d..4db537685b 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-10005 -commithash:05570853de976a526462ca140a55b1ac59d9a351 +version:3.0.0-alpha1-10009 +commithash:86be4707e47d2f1930a982f2b59eacfc4196da33 From 5965614632e68f79fac73e1f89416f977522cf80 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 22 Jul 2018 12:59:03 -0700 Subject: [PATCH 06/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 673328908f..d067a0c47c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,17 +3,17 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 3.0.0-alpha1-10009 - 3.0.0-alpha1-10081 - 3.0.0-alpha1-10081 - 3.0.0-alpha1-10081 - 3.0.0-alpha1-10081 - 3.0.0-alpha1-10081 - 3.0.0-alpha1-10081 - 3.0.0-alpha1-10081 - 3.0.0-alpha1-10081 - 3.0.0-alpha1-10081 - 3.0.0-alpha1-10081 + 3.0.0-alpha1-10011 + 3.0.0-alpha1-10123 + 3.0.0-alpha1-10123 + 3.0.0-alpha1-10123 + 3.0.0-alpha1-10123 + 3.0.0-alpha1-10123 + 3.0.0-alpha1-10123 + 3.0.0-alpha1-10123 + 3.0.0-alpha1-10123 + 3.0.0-alpha1-10123 + 3.0.0-alpha1-10123 2.0.9 2.1.2 2.2.0-preview1-26618-02 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 4db537685b..9a67ee9c28 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-10009 -commithash:86be4707e47d2f1930a982f2b59eacfc4196da33 +version:3.0.0-alpha1-10011 +commithash:717c2eb1f91dafd2580c1a9b8e5064d12dd8c054 From 8b602e9cb22a9a6e708569050cbfc29469188fe8 Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Tue, 24 Jul 2018 10:51:03 -0700 Subject: [PATCH 07/41] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64ff041d5c..eac4268e4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ Contributing ====== -Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/dev/CONTRIBUTING.md) in the Home repo. +Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/master/CONTRIBUTING.md) in the Home repo. From 4a1cf4c63dc5911629d83c10c10cb5ef3b1e5ed1 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 29 Jul 2018 19:56:44 +0000 Subject: [PATCH 08/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 31 ++++++++++++++++--------------- korebuild-lock.txt | 4 ++-- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index d067a0c47c..d34b673aa9 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,28 +3,29 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 3.0.0-alpha1-10011 - 3.0.0-alpha1-10123 - 3.0.0-alpha1-10123 - 3.0.0-alpha1-10123 - 3.0.0-alpha1-10123 - 3.0.0-alpha1-10123 - 3.0.0-alpha1-10123 - 3.0.0-alpha1-10123 - 3.0.0-alpha1-10123 - 3.0.0-alpha1-10123 - 3.0.0-alpha1-10123 + 3.0.0-alpha1-10015 + 3.0.0-alpha1-10173 + 3.0.0-alpha1-10173 + 3.0.0-alpha1-10173 + 3.0.0-alpha1-10173 + 3.0.0-alpha1-10173 + 3.0.0-alpha1-10173 + 3.0.0-alpha1-10173 + 3.0.0-alpha1-10173 + 3.0.0-alpha1-10173 + 3.0.0-alpha1-10173 2.0.9 2.1.2 2.2.0-preview1-26618-02 15.6.1 4.7.49 2.0.3 - 4.6.0-preview1-26708-04 - 4.6.0-preview1-26708-04 - 0.9.0 + 4.6.0-preview1-26727-04 + 4.6.0-preview1-26727-04 + 0.10.0 2.3.1 - 2.4.0-rc.1.build4038 + 2.4.0 + diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 9a67ee9c28..1e75dc3a23 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-10011 -commithash:717c2eb1f91dafd2580c1a9b8e5064d12dd8c054 +version:3.0.0-alpha1-10015 +commithash:3f36e5c2f061712f76f2766c435d2555681d5c55 From 23783e4dba7c3ad8b7bf4ca60eb02f1c151e50a2 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Mon, 30 Jul 2018 13:59:06 -0700 Subject: [PATCH 09/41] Fix korebuild-lock.txt --- korebuild-lock.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 5f8b0b813f..1e75dc3a23 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,7 +1,2 @@ -<<<<<<< HEAD version:3.0.0-alpha1-10015 commithash:3f36e5c2f061712f76f2766c435d2555681d5c55 -======= -version:2.2.0-preview1-17102 -commithash:e7e2b5a97ca92cfc6acc4def534cb0901a6d1eb9 ->>>>>>> merge/release/2.2-to-master From 2bae05fd83abc935d212e6efea1809fb0694e307 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 5 Aug 2018 19:53:21 +0000 Subject: [PATCH 10/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index d34b673aa9..134e99cce8 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,17 +3,17 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 3.0.0-alpha1-10015 - 3.0.0-alpha1-10173 - 3.0.0-alpha1-10173 - 3.0.0-alpha1-10173 - 3.0.0-alpha1-10173 - 3.0.0-alpha1-10173 - 3.0.0-alpha1-10173 - 3.0.0-alpha1-10173 - 3.0.0-alpha1-10173 - 3.0.0-alpha1-10173 - 3.0.0-alpha1-10173 + 3.0.0-alpha1-20180731.2 + 3.0.0-alpha1-10221 + 3.0.0-alpha1-10221 + 3.0.0-alpha1-10221 + 3.0.0-alpha1-10221 + 3.0.0-alpha1-10221 + 3.0.0-alpha1-10221 + 3.0.0-alpha1-10221 + 3.0.0-alpha1-10221 + 3.0.0-alpha1-10221 + 3.0.0-alpha1-10221 2.0.9 2.1.2 2.2.0-preview1-26618-02 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 1e75dc3a23..8f9f6066ef 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-10015 -commithash:3f36e5c2f061712f76f2766c435d2555681d5c55 +version:3.0.0-alpha1-20180731.2 +commithash:1179f1083695ac9213c8a70a4d1d6c45a52caf41 From bfa183747f6fb528087554c3d6ec58ef05f1c10a Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 12 Aug 2018 20:00:29 +0000 Subject: [PATCH 11/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 26 +++++++++++++------------- korebuild-lock.txt | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 134e99cce8..c6b6dfaef1 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,25 +3,25 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 3.0.0-alpha1-20180731.2 - 3.0.0-alpha1-10221 - 3.0.0-alpha1-10221 - 3.0.0-alpha1-10221 - 3.0.0-alpha1-10221 - 3.0.0-alpha1-10221 - 3.0.0-alpha1-10221 - 3.0.0-alpha1-10221 - 3.0.0-alpha1-10221 - 3.0.0-alpha1-10221 - 3.0.0-alpha1-10221 + 3.0.0-alpha1-20180810.1 + 3.0.0-alpha1-10275 + 3.0.0-alpha1-10275 + 3.0.0-alpha1-10275 + 3.0.0-alpha1-10275 + 3.0.0-alpha1-10275 + 3.0.0-alpha1-10275 + 3.0.0-alpha1-10275 + 3.0.0-alpha1-10275 + 3.0.0-alpha1-10275 + 3.0.0-alpha1-10275 2.0.9 2.1.2 2.2.0-preview1-26618-02 15.6.1 4.7.49 2.0.3 - 4.6.0-preview1-26727-04 - 4.6.0-preview1-26727-04 + 4.6.0-preview1-26807-04 + 4.6.0-preview1-26807-04 0.10.0 2.3.1 2.4.0 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 8f9f6066ef..e417a01b52 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-20180731.2 -commithash:1179f1083695ac9213c8a70a4d1d6c45a52caf41 +version:3.0.0-alpha1-20180810.1 +commithash:45c32b4f020e14a9295be31866051a18d293309d From 829bafe7b72c9d7fd4a10e789a5971fcc092d528 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 19 Aug 2018 19:15:01 +0000 Subject: [PATCH 12/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 26 +++++++++++++------------- korebuild-lock.txt | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index c6b6dfaef1..8ec168a2e9 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,25 +3,25 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 3.0.0-alpha1-20180810.1 - 3.0.0-alpha1-10275 - 3.0.0-alpha1-10275 - 3.0.0-alpha1-10275 - 3.0.0-alpha1-10275 - 3.0.0-alpha1-10275 - 3.0.0-alpha1-10275 - 3.0.0-alpha1-10275 - 3.0.0-alpha1-10275 - 3.0.0-alpha1-10275 - 3.0.0-alpha1-10275 + 3.0.0-alpha1-20180817.3 + 3.0.0-alpha1-10321 + 3.0.0-alpha1-10321 + 3.0.0-alpha1-10321 + 3.0.0-alpha1-10321 + 3.0.0-alpha1-10321 + 3.0.0-alpha1-10321 + 3.0.0-alpha1-10321 + 3.0.0-alpha1-10321 + 3.0.0-alpha1-10321 + 3.0.0-alpha1-10321 2.0.9 2.1.2 2.2.0-preview1-26618-02 15.6.1 4.7.49 2.0.3 - 4.6.0-preview1-26807-04 - 4.6.0-preview1-26807-04 + 4.6.0-preview1-26816-01 + 4.6.0-preview1-26816-01 0.10.0 2.3.1 2.4.0 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index e417a01b52..e5330b3d33 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-20180810.1 -commithash:45c32b4f020e14a9295be31866051a18d293309d +version:3.0.0-alpha1-20180817.3 +commithash:134cdbee9bee29dd3ccb654c67663b27b9ffa6c8 From 55ba639657370e40841c56f44b06b911ca3b57df Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 26 Aug 2018 19:14:26 +0000 Subject: [PATCH 13/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 26 +++++++++++++------------- korebuild-lock.txt | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 8ec168a2e9..db34bfd06f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,25 +3,25 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 3.0.0-alpha1-20180817.3 - 3.0.0-alpha1-10321 - 3.0.0-alpha1-10321 - 3.0.0-alpha1-10321 - 3.0.0-alpha1-10321 - 3.0.0-alpha1-10321 - 3.0.0-alpha1-10321 - 3.0.0-alpha1-10321 - 3.0.0-alpha1-10321 - 3.0.0-alpha1-10321 - 3.0.0-alpha1-10321 + 3.0.0-alpha1-20180821.3 + 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 2.0.9 2.1.2 2.2.0-preview1-26618-02 15.6.1 4.7.49 2.0.3 - 4.6.0-preview1-26816-01 - 4.6.0-preview1-26816-01 + 4.6.0-preview1-26817-04 + 4.6.0-preview1-26817-04 0.10.0 2.3.1 2.4.0 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index e5330b3d33..767a471795 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-20180817.3 -commithash:134cdbee9bee29dd3ccb654c67663b27b9ffa6c8 +version:3.0.0-alpha1-20180821.3 +commithash:0939a90812deb1c604eb9a4768869687495fc1dd From 05a581a1324fc8a86869b1e28c6197b32e5e7e90 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 29 Aug 2018 17:07:16 +1200 Subject: [PATCH 14/41] Copy core endpoint routing types to HttpAbstractions (#1030) --- build/dependencies.props | 1 + ...rosoft.AspNetCore.Http.Abstractions.csproj | 1 + .../Properties/Resources.Designer.cs | 28 + .../Resources.resx | 6 + .../Routing/Endpoint.cs | 49 + .../Routing/EndpointMetadataCollection.cs | 201 ++ .../Routing/IEndpointFeature.cs | 18 + .../Routing/IRouteValuesFeature.cs | 20 + .../Routing/RouteValueDictionary.cs | 598 ++++++ .../EndpointMetadataCollectionTests.cs | 49 + .../RouteValueDictionaryTests.cs | 1621 +++++++++++++++++ 11 files changed, 2592 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Routing/Endpoint.cs create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointMetadataCollection.cs create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Routing/IEndpointFeature.cs create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Routing/IRouteValuesFeature.cs create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs create mode 100644 test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointMetadataCollectionTests.cs create mode 100644 test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs diff --git a/build/dependencies.props b/build/dependencies.props index db34bfd06f..772f34b57d 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -13,6 +13,7 @@ 3.0.0-alpha1-10352 3.0.0-alpha1-10352 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 3.0.0-alpha1-10352 2.0.9 2.1.2 diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Microsoft.AspNetCore.Http.Abstractions/Microsoft.AspNetCore.Http.Abstractions.csproj index d4a9eff0e6..eb17ae4e2f 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -20,6 +20,7 @@ Microsoft.AspNetCore.Http.HttpResponse + diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs index 6af7d138be..02985d630b 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs @@ -192,6 +192,34 @@ namespace Microsoft.AspNetCore.Http.Abstractions internal static string FormatArgumentCannotBeNullOrEmpty() => GetString("ArgumentCannotBeNullOrEmpty"); + /// + /// An element with the key '{0}' already exists in the {1}. + /// + internal static string RouteValueDictionary_DuplicateKey + { + get => GetString("RouteValueDictionary_DuplicateKey"); + } + + /// + /// An element with the key '{0}' already exists in the {1}. + /// + internal static string FormatRouteValueDictionary_DuplicateKey(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicateKey"), p0, p1); + + /// + /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + /// + internal static string RouteValueDictionary_DuplicatePropertyName + { + get => GetString("RouteValueDictionary_DuplicatePropertyName"); + } + + /// + /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + /// + internal static string FormatRouteValueDictionary_DuplicatePropertyName(object p0, object p1, object p2, object p3) + => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicatePropertyName"), p0, p1, p2, p3); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx b/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx index dfdfeaf7d1..4ce2d8bd7a 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx @@ -156,4 +156,10 @@ Argument cannot be null or empty. + + An element with the key '{0}' already exists in the {1}. + + + The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/Endpoint.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/Endpoint.cs new file mode 100644 index 0000000000..16202b45f5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/Endpoint.cs @@ -0,0 +1,49 @@ +// 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. + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Respresents a logical endpoint in an application. + /// + public class Endpoint + { + /// + /// Creates a new instance of . + /// + /// The delegate used to process requests for the endpoint. + /// + /// The endpoint . May be null. + /// + /// + /// The informational display name of the endpoint. May be null. + /// + public Endpoint( + RequestDelegate requestDelegate, + EndpointMetadataCollection metadata, + string displayName) + { + // All are allowed to be null + RequestDelegate = requestDelegate; + Metadata = metadata ?? EndpointMetadataCollection.Empty; + DisplayName = displayName; + } + + /// + /// Gets the informational display name of this endpoint. + /// + public string DisplayName { get; } + + /// + /// Gets the collection of metadata associated with this endpoint. + /// + public EndpointMetadataCollection Metadata { get; } + + /// + /// Gets the delegate used to process requests for the endpoint. + /// + public RequestDelegate RequestDelegate { get; } + + public override string ToString() => DisplayName ?? base.ToString(); + } +} diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointMetadataCollection.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointMetadataCollection.cs new file mode 100644 index 0000000000..a792fff295 --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointMetadataCollection.cs @@ -0,0 +1,201 @@ +// 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; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// A collection of arbitrary metadata associated with an endpoint. + /// + /// + /// instances contain a list of metadata items + /// of arbitrary types. The metadata items are stored as an ordered collection with + /// items arranged in ascending order of precedence. + /// + public sealed class EndpointMetadataCollection : IReadOnlyList + { + /// + /// An empty . + /// + public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty()); + + private readonly object[] _items; + private readonly ConcurrentDictionary _cache; + + /// + /// Creates a new . + /// + /// The metadata items. + public EndpointMetadataCollection(IEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + _items = items.ToArray(); + _cache = new ConcurrentDictionary(); + } + + /// + /// Creates a new . + /// + /// The metadata items. + public EndpointMetadataCollection(params object[] items) + : this((IEnumerable)items) + { + } + + /// + /// Gets the item at . + /// + /// The index of the item to retrieve. + /// The item at . + public object this[int index] => _items[index]; + + /// + /// Gets the count of metadata items. + /// + public int Count => _items.Length; + + /// + /// Gets the most significant metadata item of type . + /// + /// The type of metadata to retrieve. + /// + /// The most significant metadata of type or null. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T GetMetadata() where T : class + { + if (_cache.TryGetValue(typeof(T), out var result)) + { + var length = result.Length; + return length > 0 ? (T)result[length - 1] : default; + } + + return GetMetadataSlow(); + } + + private T GetMetadataSlow() where T : class + { + var array = GetOrderedMetadataSlow(); + var length = array.Length; + return length > 0 ? array[length - 1] : default; + } + + /// + /// Gets the metadata items of type in ascending + /// order of precedence. + /// + /// The type of metadata. + /// A sequence of metadata items of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerable GetOrderedMetadata() where T : class + { + if (_cache.TryGetValue(typeof(T), out var result)) + { + return (T[])result; + } + + return GetOrderedMetadataSlow(); + } + + private T[] GetOrderedMetadataSlow() where T : class + { + var items = new List(); + for (var i = 0; i < _items.Length; i++) + { + if (_items[i] is T item) + { + items.Add(item); + } + } + + var array = items.ToArray(); + _cache.TryAdd(typeof(T), array); + return array; + } + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + public Enumerator GetEnumerator() => new Enumerator(this); + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Enumerates the elements of an . + /// + public struct Enumerator : IEnumerator + { + // Intentionally not readonly to prevent defensive struct copies + private object[] _items; + private int _index; + + internal Enumerator(EndpointMetadataCollection collection) + { + _items = collection._items; + _index = 0; + Current = null; + } + + /// + /// Gets the element at the current position of the enumerator + /// + public object Current { get; private set; } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + } + + /// + /// Advances the enumerator to the next element of the . + /// + /// + /// true if the enumerator was successfully advanced to the next element; + /// false if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + if (_index < _items.Length) + { + Current = _items[_index++]; + return true; + } + + Current = null; + return false; + } + + /// + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// + public void Reset() + { + _index = 0; + Current = null; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IEndpointFeature.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IEndpointFeature.cs new file mode 100644 index 0000000000..ff0762bb20 --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IEndpointFeature.cs @@ -0,0 +1,18 @@ +// 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. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// A feature interface for endpoint routing. Use + /// to access an instance associated with the current request. + /// + public interface IEndpointFeature + { + /// + /// Gets or sets the selected for the current + /// request. + /// + Endpoint Endpoint { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IRouteValuesFeature.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IRouteValuesFeature.cs new file mode 100644 index 0000000000..d1c91577fe --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IRouteValuesFeature.cs @@ -0,0 +1,20 @@ +// 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 Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// A feature interface for routing values. Use + /// to access the values associated with the current request. + /// + public interface IRouteValuesFeature + { + /// + /// Gets or sets the associated with the currrent + /// request. + /// + RouteValueDictionary RouteValues { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs new file mode 100644 index 0000000000..f4153881b3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs @@ -0,0 +1,598 @@ +// 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; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http.Abstractions; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Routing +{ + /// + /// An type for route values. + /// + public class RouteValueDictionary : IDictionary, IReadOnlyDictionary + { + // 4 is a good default capacity here because that leaves enough space for area/controller/action/id + private const int DefaultCapacity = 4; + + internal KeyValuePair[] _arrayStorage; + internal PropertyStorage _propertyStorage; + private int _count; + + /// + /// Creates a new from the provided array. + /// The new instance will take ownership of the array, and may mutate it. + /// + /// The items array. + /// A new . + public static RouteValueDictionary FromArray(KeyValuePair[] items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + // We need to compress the array by removing non-contiguous items. We + // typically have a very small number of items to process. We don't need + // to preserve order. + var start = 0; + var end = items.Length - 1; + + // We walk forwards from the beginning of the array and fill in 'null' slots. + // We walk backwards from the end of the array end move items in non-null' slots + // into whatever start is pointing to. O(n) + while (start <= end) + { + if (items[start].Key != null) + { + start++; + } + else if (items[end].Key != null) + { + // Swap this item into start and advance + items[start] = items[end]; + items[end] = default; + start++; + end--; + } + else + { + // Both null, we need to hold on 'start' since we + // still need to fill it with something. + end--; + } + } + + return new RouteValueDictionary() + { + _arrayStorage = items, + _count = start, + }; + } + + /// + /// Creates an empty . + /// + public RouteValueDictionary() + { + _arrayStorage = Array.Empty>(); + } + + /// + /// Creates a initialized with the specified . + /// + /// An object to initialize the dictionary. The value can be of type + /// or + /// or an object with public properties as key-value pairs. + /// + /// + /// If the value is a dictionary or other of , + /// then its entries are copied. Otherwise the object is interpreted as a set of key-value pairs where the + /// property names are keys, and property values are the values, and copied into the dictionary. + /// Only public instance non-index properties are considered. + /// + public RouteValueDictionary(object values) + : this() + { + if (values is RouteValueDictionary dictionary) + { + if (dictionary._propertyStorage != null) + { + // PropertyStorage is immutable so we can just copy it. + _propertyStorage = dictionary._propertyStorage; + _count = dictionary._count; + return; + } + + var other = dictionary._arrayStorage; + var storage = new KeyValuePair[other.Length]; + if (dictionary._count != 0) + { + Array.Copy(other, 0, storage, 0, dictionary._count); + } + + _arrayStorage = storage; + _count = dictionary._count; + return; + } + + if (values is IEnumerable> keyValueEnumerable) + { + foreach (var kvp in keyValueEnumerable) + { + Add(kvp.Key, kvp.Value); + } + + return; + } + + if (values is IEnumerable> stringValueEnumerable) + { + foreach (var kvp in stringValueEnumerable) + { + Add(kvp.Key, kvp.Value); + } + + return; + } + + if (values != null) + { + var storage = new PropertyStorage(values); + _propertyStorage = storage; + _count = storage.Properties.Length; + return; + } + } + + /// + public object this[string key] + { + get + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + + object value; + TryGetValue(key, out value); + return value; + } + + set + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + + // We're calling this here for the side-effect of converting from properties + // to array. We need to create the array even if we just set an existing value since + // property storage is immutable. + EnsureCapacity(_count); + + var index = FindInArray(key); + if (index < 0) + { + EnsureCapacity(_count + 1); + _arrayStorage[_count++] = new KeyValuePair(key, value); + } + else + { + _arrayStorage[index] = new KeyValuePair(key, value); + } + } + } + + /// + /// Gets the comparer for this dictionary. + /// + /// + /// This will always be a reference to + /// + public IEqualityComparer Comparer => StringComparer.OrdinalIgnoreCase; + + /// + public int Count => _count; + + /// + bool ICollection>.IsReadOnly => false; + + /// + public ICollection Keys + { + get + { + EnsureCapacity(_count); + + var array = _arrayStorage; + var keys = new string[_count]; + for (var i = 0; i < keys.Length; i++) + { + keys[i] = array[i].Key; + } + + return keys; + } + } + + IEnumerable IReadOnlyDictionary.Keys => Keys; + + /// + public ICollection Values + { + get + { + EnsureCapacity(_count); + + var array = _arrayStorage; + var values = new object[_count]; + for (var i = 0; i < values.Length; i++) + { + values[i] = array[i].Value; + } + + return values; + } + } + + IEnumerable IReadOnlyDictionary.Values => Values; + + /// + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + /// + public void Add(string key, object value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + EnsureCapacity(_count + 1); + + var index = FindInArray(key); + if (index >= 0) + { + var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary)); + throw new ArgumentException(message, nameof(key)); + } + + _arrayStorage[_count] = new KeyValuePair(key, value); + _count++; + } + + /// + public void Clear() + { + if (_count == 0) + { + return; + } + + if (_propertyStorage != null) + { + _arrayStorage = Array.Empty>(); + _propertyStorage = null; + _count = 0; + return; + } + + Array.Clear(_arrayStorage, 0, _count); + _count = 0; + } + + /// + bool ICollection>.Contains(KeyValuePair item) + { + return TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value); + } + + /// + public bool ContainsKey(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + return TryGetValue(key, out var _); + } + + /// + void ICollection>.CopyTo( + KeyValuePair[] array, + int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0 || arrayIndex > array.Length || array.Length - arrayIndex < this.Count) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + if (Count == 0) + { + return; + } + + EnsureCapacity(Count); + + var storage = _arrayStorage; + Array.Copy(storage, 0, array, arrayIndex, _count); + } + + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + bool ICollection>.Remove(KeyValuePair item) + { + if (Count == 0) + { + return false; + } + + EnsureCapacity(Count); + + var index = FindInArray(item.Key); + var array = _arrayStorage; + if (index >= 0 && EqualityComparer.Default.Equals(array[index].Value, item.Value)) + { + Array.Copy(array, index + 1, array, index, _count - index); + _count--; + array[_count] = default; + return true; + } + + return false; + } + + /// + public bool Remove(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (Count == 0) + { + return false; + } + + EnsureCapacity(Count); + + var index = FindInArray(key); + if (index >= 0) + { + _count--; + var array = _arrayStorage; + Array.Copy(array, index + 1, array, index, _count - index); + array[_count] = default; + + return true; + } + + return false; + } + + /// + public bool TryGetValue(string key, out object value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (_propertyStorage != null) + { + var storage = _propertyStorage; + for (var i = 0; i < storage.Properties.Length; i++) + { + if (string.Equals(storage.Properties[i].Name, key, StringComparison.OrdinalIgnoreCase)) + { + value = storage.Properties[i].GetValue(storage.Value); + return true; + } + } + + value = default; + return false; + } + + var array = _arrayStorage; + for (var i = 0; i < _count; i++) + { + if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) + { + value = array[i].Value; + return true; + } + } + + value = default; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int capacity) + { + if (_propertyStorage != null || _arrayStorage.Length < capacity) + { + EnsureCapacitySlow(capacity); + } + } + + private void EnsureCapacitySlow(int capacity) + { + if (_propertyStorage != null) + { + var storage = _propertyStorage; + capacity = Math.Max(storage.Properties.Length, capacity); + var array = new KeyValuePair[capacity]; + + for (var i = 0; i < storage.Properties.Length; i++) + { + var property = storage.Properties[i]; + array[i] = new KeyValuePair(property.Name, property.GetValue(storage.Value)); + } + + _arrayStorage = array; + _propertyStorage = null; + return; + } + + if (_arrayStorage.Length < capacity) + { + capacity = _arrayStorage.Length == 0 ? DefaultCapacity : _arrayStorage.Length * 2; + var array = new KeyValuePair[capacity]; + if (_count > 0) + { + Array.Copy(_arrayStorage, 0, array, 0, _count); + } + + _arrayStorage = array; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int FindInArray(string key) + { + var array = _arrayStorage; + for (var i = 0; i < _count; i++) + { + if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + + return -1; + } + + public struct Enumerator : IEnumerator> + { + private RouteValueDictionary _dictionary; + private int _index; + + public Enumerator(RouteValueDictionary dictionary) + { + if (dictionary == null) + { + throw new ArgumentNullException(); + } + + _dictionary = dictionary; + + Current = default; + _index = -1; + } + + public KeyValuePair Current { get; private set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + + public bool MoveNext() + { + if (++_index < _dictionary.Count) + { + if (_dictionary._propertyStorage != null) + { + var storage = _dictionary._propertyStorage; + var property = storage.Properties[_index]; + Current = new KeyValuePair(property.Name, property.GetValue(storage.Value)); + return true; + } + + Current = _dictionary._arrayStorage[_index]; + return true; + } + + Current = default; + return false; + } + + public void Reset() + { + Current = default; + _index = -1; + } + } + + internal class PropertyStorage + { + private static readonly ConcurrentDictionary _propertyCache = new ConcurrentDictionary(); + + public readonly object Value; + public readonly PropertyHelper[] Properties; + + public PropertyStorage(object value) + { + Debug.Assert(value != null); + Value = value; + + // Cache the properties so we can know if we've already validated them for duplicates. + var type = Value.GetType(); + if (!_propertyCache.TryGetValue(type, out Properties)) + { + Properties = PropertyHelper.GetVisibleProperties(type); + ValidatePropertyNames(type, Properties); + _propertyCache.TryAdd(type, Properties); + } + } + + private static void ValidatePropertyNames(Type type, PropertyHelper[] properties) + { + var names = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < properties.Length; i++) + { + var property = properties[i]; + + if (names.TryGetValue(property.Name, out var duplicate)) + { + var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName( + type.FullName, + property.Name, + duplicate.Name, + nameof(RouteValueDictionary)); + throw new InvalidOperationException(message); + } + + names.Add(property.Name, property); + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointMetadataCollectionTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointMetadataCollectionTests.cs new file mode 100644 index 0000000000..62432b7b2b --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointMetadataCollectionTests.cs @@ -0,0 +1,49 @@ +// 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.Text; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class EndpointMetadataCollectionTests + { + [Fact] + public void Constructor_Enumeration_ContainsValues() + { + // Arrange & Act + var metadata = new EndpointMetadataCollection(new List + { + 1, + 2, + 3, + }); + + // Assert + Assert.Equal(3, metadata.Count); + + Assert.Collection(metadata, + value => Assert.Equal(1, value), + value => Assert.Equal(2, value), + value => Assert.Equal(3, value)); + } + + [Fact] + public void Constructor_ParamsArray_ContainsValues() + { + // Arrange & Act + var metadata = new EndpointMetadataCollection(1, 2, 3); + + // Assert + Assert.Equal(3, metadata.Count); + + Assert.Collection(metadata, + value => Assert.Equal(1, value), + value => Assert.Equal(2, value), + value => Assert.Equal(3, value)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs new file mode 100644 index 0000000000..524966dc05 --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs @@ -0,0 +1,1621 @@ +// 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.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class RouteValueDictionaryTests + { + [Fact] + public void DefaultCtor_UsesEmptyStorage() + { + // Arrange + // Act + var dict = new RouteValueDictionary(); + + // Assert + Assert.Empty(dict); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._propertyStorage); + } + + [Fact] + public void CreateFromNull_UsesEmptyStorage() + { + // Arrange + // Act + var dict = new RouteValueDictionary(null); + + // Assert + Assert.Empty(dict); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._propertyStorage); + } + + [Fact] + public void CreateFromRouteValueDictionary_WithArrayStorage_CopiesStorage() + { + // Arrange + var other = new RouteValueDictionary() + { + { "1", 1 } + }; + + // Act + var dict = new RouteValueDictionary(other); + + // Assert + Assert.Equal(other, dict); + + var storage = Assert.IsType[]>(dict._arrayStorage); + var otherStorage = Assert.IsType[]>(other._arrayStorage); + Assert.NotSame(otherStorage, storage); + } + + [Fact] + public void CreateFromRouteValueDictionary_WithPropertyStorage_CopiesStorage() + { + // Arrange + var other = new RouteValueDictionary(new { key = "value" }); + + // Act + var dict = new RouteValueDictionary(other); + + // Assert + Assert.Equal(other, dict); + + var storage = dict._propertyStorage; + var otherStorage = other._propertyStorage; + Assert.Same(otherStorage, storage); + } + + public static IEnumerable IEnumerableKeyValuePairData + { + get + { + var routeValues = new[] + { + new KeyValuePair("Name", "James"), + new KeyValuePair("Age", 30), + new KeyValuePair("Address", new Address() { City = "Redmond", State = "WA" }) + }; + + yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; + + yield return new object[] { routeValues.ToList() }; + + yield return new object[] { routeValues }; + } + } + + public static IEnumerable IEnumerableStringValuePairData + { + get + { + var routeValues = new[] + { + new KeyValuePair("First Name", "James"), + new KeyValuePair("Last Name", "Henrik"), + new KeyValuePair("Middle Name", "Bob") + }; + + yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; + + yield return new object[] { routeValues.ToList() }; + + yield return new object[] { routeValues }; + } + } + + [Theory] + [MemberData(nameof(IEnumerableKeyValuePairData))] + public void CreateFromIEnumerableKeyValuePair_CopiesValues(object values) + { + // Arrange & Act + var dict = new RouteValueDictionary(values); + + // Assert + Assert.IsType[]>(dict._arrayStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("Address", kvp.Key); + var address = Assert.IsType
(kvp.Value); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + }, + kvp => { Assert.Equal("Age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("Name", kvp.Key); Assert.Equal("James", kvp.Value); }); + } + + [Theory] + [MemberData(nameof(IEnumerableStringValuePairData))] + public void CreateFromIEnumerableStringValuePair_CopiesValues(object values) + { + // Arrange & Act + var dict = new RouteValueDictionary(values); + + // Assert + Assert.IsType[]>(dict._arrayStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("First Name", kvp.Key); Assert.Equal("James", kvp.Value); }, + kvp => { Assert.Equal("Last Name", kvp.Key); Assert.Equal("Henrik", kvp.Value); }, + kvp => { Assert.Equal("Middle Name", kvp.Key); Assert.Equal("Bob", kvp.Value); }); + } + + [Fact] + public void CreateFromIEnumerableKeyValuePair_ThrowsExceptionForDuplicateKey() + { + // Arrange + var values = new List>() + { + new KeyValuePair("name", "Billy"), + new KeyValuePair("Name", "Joey"), + }; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new RouteValueDictionary(values), + "key", + $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); + } + + [Fact] + public void CreateFromIEnumerableStringValuePair_ThrowsExceptionForDuplicateKey() + { + // Arrange + var values = new List>() + { + new KeyValuePair("name", "Billy"), + new KeyValuePair("Name", "Joey"), + }; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new RouteValueDictionary(values), + "key", + $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromAnonymousType() + { + // Arrange + var obj = new { cool = "beans", awesome = 123 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("awesome", kvp.Key); Assert.Equal(123, kvp.Value); }, + kvp => { Assert.Equal("cool", kvp.Key); Assert.Equal("beans", kvp.Value); }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType() + { + // Arrange + var obj = new RegularType() { CoolnessFactor = 73 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("CoolnessFactor", kvp.Key); + Assert.Equal(73, kvp.Value); + }, + kvp => + { + Assert.Equal("IsAwesome", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.False(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_PublicOnly() + { + // Arrange + var obj = new Visibility() { IsPublic = true, ItsInternalDealWithIt = 5 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("IsPublic", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.True(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresStatic() + { + // Arrange + var obj = new StaticProperty(); + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresSetOnly() + { + // Arrange + var obj = new SetterOnly() { CoolSetOnly = false }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IncludesInherited() + { + // Arrange + var obj = new Derived() { TotallySweetProperty = true, DerivedProperty = false }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("DerivedProperty", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.False(value); + }, + kvp => + { + Assert.Equal("TotallySweetProperty", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.True(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_WithHiddenProperty() + { + // Arrange + var obj = new DerivedHiddenProperty() { DerivedProperty = 5 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("DerivedProperty", kvp.Key); Assert.Equal(5, kvp.Value); }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_WithIndexerProperty() + { + // Arrange + var obj = new IndexerProperty(); + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_MixedCaseThrows() + { + // Arrange + var obj = new { controller = "Home", Controller = "Home" }; + + var message = + $"The type '{obj.GetType().FullName}' defines properties 'controller' and 'Controller' which differ " + + $"only by casing. This is not supported by {nameof(RouteValueDictionary)} which uses " + + $"case-insensitive comparisons."; + + // Act & Assert + var exception = Assert.Throws(() => + { + var dictionary = new RouteValueDictionary(obj); + }); + + // Ignoring case to make sure we're not testing reflection's ordering. + Assert.Equal(message, exception.Message, ignoreCase: true); + } + + // Our comparer is hardcoded to be OrdinalIgnoreCase no matter what. + [Fact] + public void Comparer_IsOrdinalIgnoreCase() + { + // Arrange + // Act + var dict = new RouteValueDictionary(); + + // Assert + Assert.Same(StringComparer.OrdinalIgnoreCase, dict.Comparer); + } + + // Our comparer is hardcoded to be IsReadOnly==false no matter what. + [Fact] + public void IsReadOnly_False() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = ((ICollection>)dict).IsReadOnly; + + // Assert + Assert.False(result); + } + + [Fact] + public void IndexGet_EmptyStorage_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + } + + [Fact] + public void IndexGet_PropertyStorage_NoMatch_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void IndexGet_PropertyStorage_Match_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var value = dict["key"]; + + // Assert + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void IndexGet_PropertyStorage_MatchIgnoreCase_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var value = dict["kEy"]; + + // Assert + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void IndexGet_ArrayStorage_NoMatch_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexGet_ListStorage_Match_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var value = dict["key"]; + + // Assert + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexGet_ListStorage_MatchIgnoreCase_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var value = dict["kEy"]; + + // Assert + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_EmptyStorage_UpgradesToList() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_PropertyStorage_NoMatch_AddsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_PropertyStorage_Match_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_PropertyStorage_MatchIgnoreCase_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict["kEy"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("kEy", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_ListStorage_NoMatch_AddsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_ListStorage_Match_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_ListStorage_MatchIgnoreCase_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Count_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(0, count); + } + + [Fact] + public void Count_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(1, count); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void Count_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(1, count); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Keys_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var keys = dict.Keys; + + // Assert + Assert.Empty(keys); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Keys_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var keys = dict.Keys; + + // Assert + Assert.Equal(new[] { "key" }, keys); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Keys_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var keys = dict.Keys; + + // Assert + Assert.Equal(new[] { "key" }, keys); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Values_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var values = dict.Values; + + // Assert + Assert.Empty(values); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Values_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var values = dict.Values; + + // Assert + Assert.Equal(new object[] { "value" }, values); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Values_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var values = dict.Values; + + // Assert + Assert.Equal(new object[] { "value" }, values); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_DuplicateKey() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var message = $"An element with the key 'key' already exists in the {nameof(RouteValueDictionary)}"; + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => dict.Add("key", "value2"), "key", message); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_DuplicateKey_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var message = $"An element with the key 'kEy' already exists in the {nameof(RouteValueDictionary)}"; + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => dict.Add("kEy", "value2"), "key", message); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_KeyValuePair() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + ((ICollection>)dict).Add(new KeyValuePair("key", "value")); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Clear_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + } + + [Fact] + public void Clear_PropertyStorage_AlreadyEmpty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void Clear_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.Null(dict._propertyStorage); + } + + [Fact] + public void Clear_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Contains_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Contains_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("KEY", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Contains_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("other", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } + + // Value comparisons use the default equality comparer. + [Fact] + public void Contains_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "valUE"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ContainsKey_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.False(result); + } + + [Fact] + public void ContainsKey_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("other"); + + // Assert + Assert.False(result); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void ContainsKey_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void ContainsKey_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("kEy"); + + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void ContainsKey_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("other"); + + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ContainsKey_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ContainsKey_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("kEy"); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void CopyTo() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var array = new KeyValuePair[2]; + + // Act + ((ICollection>)dict).CopyTo(array, 1); + + // Assert + Assert.Equal( + new KeyValuePair[] + { + default(KeyValuePair), + new KeyValuePair("key", "value") + }, + array); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("KEY", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("other", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + // Value comparisons use the default equality comparer. + [Fact] + public void Remove_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "valUE"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.False(result); + } + + [Fact] + public void Remove_PropertyStorage_Empty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Empty(dict); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void Remove_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("kEy"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("kEy"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryGetValue_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + } + + [Fact] + public void TryGetValue_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("other", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void TryGetValue_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void TryGetValue_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("kEy", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void TryGetValue_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("other", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryGetValue_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryGetValue_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("kEy", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ListStorage_DynamicallyAdjustsCapacity() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act 1 + dict.Add("key", "value"); + + // Assert 1 + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(4, storage.Length); + + // Act 2 + dict.Add("key2", "value2"); + dict.Add("key3", "value3"); + dict.Add("key4", "value4"); + dict.Add("key5", "value5"); + + // Assert 2 + storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(8, storage.Length); + } + + [Fact] + public void ListStorage_RemoveAt_RearrangesInnerArray() + { + // Arrange + var dict = new RouteValueDictionary(); + dict.Add("key", "value"); + dict.Add("key2", "value2"); + dict.Add("key3", "value3"); + + // Assert 1 + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(3, dict.Count); + + // Act + dict.Remove("key2"); + + // Assert 2 + storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(2, dict.Count); + Assert.Equal("key", storage[0].Key); + Assert.Equal("value", storage[0].Value); + Assert.Equal("key3", storage[1].Key); + Assert.Equal("value3", storage[1].Value); + } + + [Fact] + public void FromArray_TakesOwnershipOfArray() + { + // Arrange + var array = new KeyValuePair[] + { + new KeyValuePair("a", 0), + new KeyValuePair("b", 1), + new KeyValuePair("c", 2), + }; + + var dictionary = RouteValueDictionary.FromArray(array); + + // Act - modifying the array should modify the dictionary + array[0] = new KeyValuePair("aa", 10); + + // Assert + Assert.Equal(3, dictionary.Count); + Assert.Equal(10, dictionary["aa"]); + } + + [Fact] + public void FromArray_EmptyArray() + { + // Arrange + var array = Array.Empty>(); + + // Act + var dictionary = RouteValueDictionary.FromArray(array); + + // Assert + Assert.Empty(dictionary); + } + + [Fact] + public void FromArray_RemovesGapsInArray() + { + // Arrange + var array = new KeyValuePair[] + { + new KeyValuePair(null, null), + new KeyValuePair("a", 0), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + new KeyValuePair("b", 1), + new KeyValuePair("c", 2), + new KeyValuePair("d", 3), + new KeyValuePair(null, null), + }; + + // Act - calling From should modify the array + var dictionary = RouteValueDictionary.FromArray(array); + + // Assert + Assert.Equal(4, dictionary.Count); + Assert.Equal( + new KeyValuePair[] + { + new KeyValuePair("d", 3), + new KeyValuePair("a", 0), + new KeyValuePair("c", 2), + new KeyValuePair("b", 1), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + }, + array); + } + + private class RegularType + { + public bool IsAwesome { get; set; } + + public int CoolnessFactor { get; set; } + } + + private class Visibility + { + private string PrivateYo { get; set; } + + internal int ItsInternalDealWithIt { get; set; } + + public bool IsPublic { get; set; } + } + + private class StaticProperty + { + public static bool IsStatic { get; set; } + } + + private class SetterOnly + { + private bool _coolSetOnly; + + public bool CoolSetOnly { set { _coolSetOnly = value; } } + } + + private class Base + { + public bool DerivedProperty { get; set; } + } + + private class Derived : Base + { + public bool TotallySweetProperty { get; set; } + } + + private class DerivedHiddenProperty : Base + { + public new int DerivedProperty { get; set; } + } + + private class IndexerProperty + { + public bool this[string key] + { + get { return false; } + set { } + } + } + + private class Address + { + public string City { get; set; } + + public string State { get; set; } + } + } +} From bbc8ab80bffbc1e73d5e3f20d478a0a12803a0b8 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 2 Sep 2018 19:14:28 +0000 Subject: [PATCH 15/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 772f34b57d..bc02d78ed6 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,25 +4,25 @@ 3.0.0-alpha1-20180821.3 - 3.0.0-alpha1-10352 - 3.0.0-alpha1-10352 - 3.0.0-alpha1-10352 - 3.0.0-alpha1-10352 - 3.0.0-alpha1-10352 - 3.0.0-alpha1-10352 - 3.0.0-alpha1-10352 - 3.0.0-alpha1-10352 - 3.0.0-alpha1-10352 - 3.0.0-alpha1-10352 - 3.0.0-alpha1-10352 + 3.0.0-alpha1-10393 + 3.0.0-alpha1-10393 + 3.0.0-alpha1-10393 + 3.0.0-alpha1-10393 + 3.0.0-alpha1-10393 + 3.0.0-alpha1-10393 + 3.0.0-alpha1-10393 + 3.0.0-alpha1-10393 + 3.0.0-alpha1-10393 + 3.0.0-alpha1-10393 + 3.0.0-alpha1-10393 2.0.9 2.1.2 2.2.0-preview1-26618-02 15.6.1 - 4.7.49 + 4.9.0 2.0.3 - 4.6.0-preview1-26817-04 - 4.6.0-preview1-26817-04 + 4.6.0-preview1-26829-04 + 4.6.0-preview1-26829-04 0.10.0 2.3.1 2.4.0 From fb560f599236e973cb96c35094cc8b573cab06de Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Wed, 5 Sep 2018 23:46:37 +0000 Subject: [PATCH 16/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index bc02d78ed6..0afb642b1a 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,25 +4,25 @@ 3.0.0-alpha1-20180821.3 - 3.0.0-alpha1-10393 - 3.0.0-alpha1-10393 - 3.0.0-alpha1-10393 - 3.0.0-alpha1-10393 - 3.0.0-alpha1-10393 - 3.0.0-alpha1-10393 - 3.0.0-alpha1-10393 - 3.0.0-alpha1-10393 - 3.0.0-alpha1-10393 - 3.0.0-alpha1-10393 - 3.0.0-alpha1-10393 + 3.0.0-alpha1-10400 + 3.0.0-alpha1-10400 + 3.0.0-alpha1-10400 + 3.0.0-alpha1-10400 + 3.0.0-alpha1-10400 + 3.0.0-alpha1-10400 + 3.0.0-alpha1-10400 + 3.0.0-alpha1-10400 + 3.0.0-alpha1-10400 + 3.0.0-alpha1-10400 + 3.0.0-alpha1-10400 2.0.9 - 2.1.2 - 2.2.0-preview1-26618-02 + 2.1.3 + 2.2.0-preview2-26905-02 15.6.1 4.9.0 2.0.3 - 4.6.0-preview1-26829-04 - 4.6.0-preview1-26829-04 + 4.6.0-preview1-26831-06 + 4.6.0-preview1-26831-06 0.10.0 2.3.1 2.4.0 From d81f04a5b0f481188c6e0e4a8bff44573285c1a7 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 6 Sep 2018 08:48:00 -0700 Subject: [PATCH 17/41] Update doc comment for HttpRequest.Protocol (#1013) --- src/Microsoft.AspNetCore.Http.Abstractions/HttpRequest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/HttpRequest.cs b/src/Microsoft.AspNetCore.Http.Abstractions/HttpRequest.cs index a4337b7766..3488bf3bb5 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/HttpRequest.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/HttpRequest.cs @@ -66,9 +66,9 @@ namespace Microsoft.AspNetCore.Http public abstract IQueryCollection Query { get; set; } /// - /// Gets or sets the RequestProtocol. + /// Gets or sets the request protocol (e.g. HTTP/1.1). /// - /// The RequestProtocol. + /// The request protocol. public abstract string Protocol { get; set; } /// From 02f55e181b19d8854672d05225e0dbbf991100db Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 9 Sep 2018 19:15:13 +0000 Subject: [PATCH 18/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 28 ++++++++++++++-------------- korebuild-lock.txt | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 0afb642b1a..a3e49fa1f1 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,26 +3,26 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 3.0.0-alpha1-20180821.3 - 3.0.0-alpha1-10400 - 3.0.0-alpha1-10400 - 3.0.0-alpha1-10400 - 3.0.0-alpha1-10400 - 3.0.0-alpha1-10400 - 3.0.0-alpha1-10400 - 3.0.0-alpha1-10400 - 3.0.0-alpha1-10400 - 3.0.0-alpha1-10400 - 3.0.0-alpha1-10400 - 3.0.0-alpha1-10400 + 3.0.0-alpha1-20180907.9 + 3.0.0-alpha1-10419 + 3.0.0-alpha1-10419 + 3.0.0-alpha1-10419 + 3.0.0-alpha1-10419 + 3.0.0-alpha1-10419 + 3.0.0-alpha1-10419 + 3.0.0-alpha1-10419 + 3.0.0-alpha1-10419 + 3.0.0-alpha1-10419 + 3.0.0-alpha1-10419 + 3.0.0-alpha1-10419 2.0.9 2.1.3 2.2.0-preview2-26905-02 15.6.1 4.9.0 2.0.3 - 4.6.0-preview1-26831-06 - 4.6.0-preview1-26831-06 + 4.6.0-preview1-26905-03 + 4.6.0-preview1-26905-03 0.10.0 2.3.1 2.4.0 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 767a471795..b614cab6e8 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-20180821.3 -commithash:0939a90812deb1c604eb9a4768869687495fc1dd +version:3.0.0-alpha1-20180907.9 +commithash:f997365a8832ff0a3cbd9a98df45734ac2723fa0 From 41c4a47680a2242bb389a58a18aba3b01491dab0 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 12 Sep 2018 01:40:38 -0700 Subject: [PATCH 19/41] Minor perf updates for RVD Porting changes from perf work in https://github.com/aspnet/Routing/pull/788 Includes porting/adding the RVD benchmarks, as well as a new TryAdd method. --- HttpAbstractions.sln | 19 +- ...crosoft.AspNetCore.Http.Performance.csproj | 21 ++ .../Properties/AssemblyInfo.cs | 1 + .../RouteValueDictionaryBenchmark.cs | 182 +++++++++++++++ build/dependencies.props | 2 + build/repo.props | 4 + .../Routing/RouteValueDictionary.cs | 160 +++++++++---- .../RouteValueDictionaryTests.cs | 215 +++++++++++++++++- 8 files changed, 560 insertions(+), 44 deletions(-) create mode 100644 benchmarks/Microsoft.AspNetCore.Http.Performance/Microsoft.AspNetCore.Http.Performance.csproj create mode 100644 benchmarks/Microsoft.AspNetCore.Http.Performance/Properties/AssemblyInfo.cs create mode 100644 benchmarks/Microsoft.AspNetCore.Http.Performance/RouteValueDictionaryBenchmark.cs diff --git a/HttpAbstractions.sln b/HttpAbstractions.sln index fc578eb8a3..bf9b67107e 100644 --- a/HttpAbstractions.sln +++ b/HttpAbstractions.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26730.10 MinimumVisualStudioVersion = 15.0.26730.03 @@ -70,6 +70,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authen EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Core.Test", "test\Microsoft.AspNetCore.Authentication.Core.Test\Microsoft.AspNetCore.Authentication.Core.Test.csproj", "{A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{5C05BDE0-6339-40BF-8215-97AFA72DCFE1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Performance", "benchmarks\Microsoft.AspNetCore.Http.Performance\Microsoft.AspNetCore.Http.Performance.csproj", "{4633ADE6-7A61-44EB-B7CE-0141C009DBAA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -272,6 +276,18 @@ Global {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Release|Mixed Platforms.Build.0 = Release|Any CPU {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Release|x86.ActiveCfg = Release|Any CPU {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7}.Release|x86.Build.0 = Release|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Debug|x86.ActiveCfg = Debug|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Debug|x86.Build.0 = Debug|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Release|Any CPU.Build.0 = Release|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Release|x86.ActiveCfg = Release|Any CPU + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -296,6 +312,7 @@ Global {3D8C9A87-5DFB-4EC0-9CB6-174AD3B33852} = {A5A15F1C-885A-452A-A731-B0173DDBD913} {73CA3145-91BD-4DA5-BC74-40008DE7EA98} = {A5A15F1C-885A-452A-A731-B0173DDBD913} {A85950C5-2794-47E2-8EAA-05A1DC7C6DA7} = {F31FF137-390C-49BF-A3BD-7C6ED3597C21} + {4633ADE6-7A61-44EB-B7CE-0141C009DBAA} = {5C05BDE0-6339-40BF-8215-97AFA72DCFE1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D9A9994D-F09F-4209-861B-4A9036485D1F} diff --git a/benchmarks/Microsoft.AspNetCore.Http.Performance/Microsoft.AspNetCore.Http.Performance.csproj b/benchmarks/Microsoft.AspNetCore.Http.Performance/Microsoft.AspNetCore.Http.Performance.csproj new file mode 100644 index 0000000000..2cb0d4f30d --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Http.Performance/Microsoft.AspNetCore.Http.Performance.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp2.2 + Exe + true + true + false + Microsoft.AspNetCore.Http + + + + + + + + + + + + diff --git a/benchmarks/Microsoft.AspNetCore.Http.Performance/Properties/AssemblyInfo.cs b/benchmarks/Microsoft.AspNetCore.Http.Performance/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..2efc4cb5fb --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Http.Performance/Properties/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark] \ No newline at end of file diff --git a/benchmarks/Microsoft.AspNetCore.Http.Performance/RouteValueDictionaryBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Http.Performance/RouteValueDictionaryBenchmark.cs new file mode 100644 index 0000000000..c256d31a55 --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Http.Performance/RouteValueDictionaryBenchmark.cs @@ -0,0 +1,182 @@ +// 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 BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteValueDictionaryBenchmark + { + private RouteValueDictionary _arrayValues; + private RouteValueDictionary _propertyValues; + + // We modify the route value dictionaries in many of these benchmarks. + [IterationSetup] + public void Setup() + { + _arrayValues = new RouteValueDictionary() + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" }, + }; + _propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + } + + [Benchmark] + public RouteValueDictionary AddSingleItem() + { + var dictionary = new RouteValueDictionary + { + { "action", "Index" } + }; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary AddThreeItems() + { + var dictionary = new RouteValueDictionary + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "15" } + }; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary ConditionalAdd_ContainsKeyAdd() + { + var dictionary = _arrayValues; + + if (!dictionary.ContainsKey("action")) + { + dictionary.Add("action", "Index"); + } + + if (!dictionary.ContainsKey("controller")) + { + dictionary.Add("controller", "Home"); + } + + if (!dictionary.ContainsKey("area")) + { + dictionary.Add("area", "Admin"); + } + + return dictionary; + } + + [Benchmark] + public RouteValueDictionary ConditionalAdd_TryAdd() + { + var dictionary = _arrayValues; + + dictionary.TryAdd("action", "Index"); + dictionary.TryAdd("controller", "Home"); + dictionary.TryAdd("area", "Admin"); + + return dictionary; + } + + [Benchmark] + public RouteValueDictionary ForEachThreeItems_Array() + { + var dictionary = _arrayValues; + foreach (var kvp in dictionary) + { + GC.KeepAlive(kvp.Value); + } + return dictionary; + } + + [Benchmark] + public RouteValueDictionary ForEachThreeItems_Properties() + { + var dictionary = _propertyValues; + foreach (var kvp in dictionary) + { + GC.KeepAlive(kvp.Value); + } + return dictionary; + } + + [Benchmark] + public RouteValueDictionary GetThreeItems_Array() + { + var dictionary = _arrayValues; + GC.KeepAlive(dictionary["action"]); + GC.KeepAlive(dictionary["controller"]); + GC.KeepAlive(dictionary["id"]); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary GetThreeItems_Properties() + { + var dictionary = _propertyValues; + GC.KeepAlive(dictionary["action"]); + GC.KeepAlive(dictionary["controller"]); + GC.KeepAlive(dictionary["id"]); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary SetSingleItem() + { + var dictionary = new RouteValueDictionary + { + ["action"] = "Index" + }; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary SetExistingItem() + { + var dictionary = _arrayValues; + dictionary["action"] = "About"; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary SetThreeItems() + { + var dictionary = new RouteValueDictionary + { + ["action"] = "Index", + ["controller"] = "Home", + ["id"] = "15" + }; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary TryGetValueThreeItems_Array() + { + var dictionary = _arrayValues; + dictionary.TryGetValue("action", out var action); + dictionary.TryGetValue("controller", out var controller); + dictionary.TryGetValue("id", out var id); + GC.KeepAlive(action); + GC.KeepAlive(controller); + GC.KeepAlive(id); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary TryGetValueThreeItems_Properties() + { + var dictionary = _propertyValues; + dictionary.TryGetValue("action", out var action); + dictionary.TryGetValue("controller", out var controller); + dictionary.TryGetValue("id", out var id); + GC.KeepAlive(action); + GC.KeepAlive(controller); + GC.KeepAlive(id); + return dictionary; + } + } +} diff --git a/build/dependencies.props b/build/dependencies.props index a3e49fa1f1..500c0656c9 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,7 +3,9 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + 0.10.13 3.0.0-alpha1-20180907.9 + 3.0.0-alpha1-10419 3.0.0-alpha1-10419 3.0.0-alpha1-10419 3.0.0-alpha1-10419 diff --git a/build/repo.props b/build/repo.props index 17a98ac7e7..71840e75d1 100644 --- a/build/repo.props +++ b/build/repo.props @@ -12,4 +12,8 @@ + + + true + diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs index f4153881b3..396f79b507 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs @@ -155,9 +155,9 @@ namespace Microsoft.AspNetCore.Routing { get { - if (string.IsNullOrEmpty(key)) + if (key == null) { - throw new ArgumentNullException(nameof(key)); + ThrowArgumentNullExceptionForKey(); } object value; @@ -167,9 +167,9 @@ namespace Microsoft.AspNetCore.Routing set { - if (string.IsNullOrEmpty(key)) + if (key == null) { - throw new ArgumentNullException(nameof(key)); + ThrowArgumentNullExceptionForKey(); } // We're calling this here for the side-effect of converting from properties @@ -177,7 +177,7 @@ namespace Microsoft.AspNetCore.Routing // property storage is immutable. EnsureCapacity(_count); - var index = FindInArray(key); + var index = FindIndex(key); if (index < 0) { EnsureCapacity(_count + 1); @@ -255,12 +255,12 @@ namespace Microsoft.AspNetCore.Routing { if (key == null) { - throw new ArgumentNullException(nameof(key)); + ThrowArgumentNullExceptionForKey(); } EnsureCapacity(_count + 1); - var index = FindInArray(key); + var index = FindIndex(key); if (index >= 0) { var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary)); @@ -302,7 +302,7 @@ namespace Microsoft.AspNetCore.Routing { if (key == null) { - throw new ArgumentNullException(nameof(key)); + ThrowArgumentNullExceptionForKey(); } return TryGetValue(key, out var _); @@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.Routing EnsureCapacity(Count); - var index = FindInArray(item.Key); + var index = FindIndex(item.Key); var array = _arrayStorage; if (index >= 0 && EqualityComparer.Default.Equals(array[index].Value, item.Value)) { @@ -380,7 +380,7 @@ namespace Microsoft.AspNetCore.Routing { if (key == null) { - throw new ArgumentNullException(nameof(key)); + ThrowArgumentNullExceptionForKey(); } if (Count == 0) @@ -390,7 +390,7 @@ namespace Microsoft.AspNetCore.Routing EnsureCapacity(Count); - var index = FindInArray(key); + var index = FindIndex(key); if (index >= 0) { _count--; @@ -404,14 +404,54 @@ namespace Microsoft.AspNetCore.Routing return false; } + /// + /// Attempts to the add the provided and to the dictionary. + /// + /// The key. + /// The value. + /// Returns true if the value was added. Returns false if the key was already present. + public bool TryAdd(string key, object value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + // Since this is an attempt to write to the dictionary, just make it an array if it isn't. If the code + // path we're on event tries to write to the dictionary, it will likely get 'upgraded' at some point, + // so we do it here to keep the code size and complexity down. + EnsureCapacity(Count); + + var index = FindIndex(key); + if (index >= 0) + { + return false; + } + + EnsureCapacity(Count + 1); + _arrayStorage[Count] = new KeyValuePair(key, value); + _count++; + return true; + } + /// public bool TryGetValue(string key, out object value) { if (key == null) { - throw new ArgumentNullException(nameof(key)); + ThrowArgumentNullExceptionForKey(); } + if (_propertyStorage == null) + { + return TryFindItem(key, out value); + } + + return TryGetValueSlow(key, out value); + } + + private bool TryGetValueSlow(string key, out object value) + { if (_propertyStorage != null) { var storage = _propertyStorage; @@ -423,25 +463,17 @@ namespace Microsoft.AspNetCore.Routing return true; } } - - value = default; - return false; - } - - var array = _arrayStorage; - for (var i = 0; i < _count; i++) - { - if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) - { - value = array[i].Value; - return true; - } } value = default; return false; } + private static void ThrowArgumentNullExceptionForKey() + { + throw new ArgumentNullException("key"); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EnsureCapacity(int capacity) { @@ -456,7 +488,10 @@ namespace Microsoft.AspNetCore.Routing if (_propertyStorage != null) { var storage = _propertyStorage; - capacity = Math.Max(storage.Properties.Length, capacity); + + // If we're converting from properties, it's likely due to an 'add' to make sure we have at least + // the default amount of space. + capacity = Math.Max(DefaultCapacity, Math.Max(storage.Properties.Length, capacity)); var array = new KeyValuePair[capacity]; for (var i = 0; i < storage.Properties.Length; i++) @@ -484,10 +519,14 @@ namespace Microsoft.AspNetCore.Routing } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int FindInArray(string key) + private int FindIndex(string key) { + // Generally the bounds checking here will be elided by the JIT because this will be called + // on the same code path as EnsureCapacity. var array = _arrayStorage; - for (var i = 0; i < _count; i++) + var count = _count; + + for (var i = 0; i < count; i++) { if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) { @@ -498,9 +537,32 @@ namespace Microsoft.AspNetCore.Routing return -1; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryFindItem(string key, out object value) + { + var array = _arrayStorage; + var count = _count; + + // Elide bounds check for indexing. + if ((uint)count <= (uint)array.Length) + { + for (var i = 0; i < count; i++) + { + if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) + { + value = array[i].Value; + return true; + } + } + } + + value = null; + return false; + } + public struct Enumerator : IEnumerator> { - private RouteValueDictionary _dictionary; + private readonly RouteValueDictionary _dictionary; private int _index; public Enumerator(RouteValueDictionary dictionary) @@ -513,7 +575,7 @@ namespace Microsoft.AspNetCore.Routing _dictionary = dictionary; Current = default; - _index = -1; + _index = 0; } public KeyValuePair Current { get; private set; } @@ -524,22 +586,36 @@ namespace Microsoft.AspNetCore.Routing { } + // Similar to the design of List.Enumerator - Split into fast path and slow path for inlining friendliness + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - if (++_index < _dictionary.Count) - { - if (_dictionary._propertyStorage != null) - { - var storage = _dictionary._propertyStorage; - var property = storage.Properties[_index]; - Current = new KeyValuePair(property.Name, property.GetValue(storage.Value)); - return true; - } + var dictionary = _dictionary; - Current = _dictionary._arrayStorage[_index]; + // The uncommon case is that the propertyStorage is in use + if (dictionary._propertyStorage == null && ((uint)_index < (uint)dictionary._count)) + { + Current = dictionary._arrayStorage[_index]; + _index++; return true; } + return MoveNextRare(); + } + + private bool MoveNextRare() + { + var dictionary = _dictionary; + if (dictionary._propertyStorage != null && ((uint)_index < (uint)dictionary._count)) + { + var storage = dictionary._propertyStorage; + var property = storage.Properties[_index]; + Current = new KeyValuePair(property.Name, property.GetValue(storage.Value)); + _index++; + return true; + } + + _index = dictionary._count; Current = default; return false; } @@ -547,7 +623,7 @@ namespace Microsoft.AspNetCore.Routing public void Reset() { Current = default; - _index = -1; + _index = 0; } } @@ -595,4 +671,4 @@ namespace Microsoft.AspNetCore.Routing } } } -} +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs index 524966dc05..65a97c70ba 100644 --- a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs @@ -380,6 +380,19 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.False(result); } + [Fact] + public void IndexGet_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var value = dict[""]; + + // Assert + Assert.Null(value); + } + [Fact] public void IndexGet_EmptyStorage_ReturnsNull() { @@ -486,6 +499,19 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.IsType[]>(dict._arrayStorage); } + [Fact] + public void IndexSet_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict[""] = "foo"; + + // Assert + Assert.Equal("foo", dict[""]); + } + [Fact] public void IndexSet_EmptyStorage_UpgradesToList() { @@ -747,6 +773,19 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.IsType[]>(dict._arrayStorage); } + [Fact] + public void Add_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict.Add("", "foo"); + + // Assert + Assert.Equal("foo", dict[""]); + } + [Fact] public void Add_PropertyStorage() { @@ -762,6 +801,14 @@ namespace Microsoft.AspNetCore.Routing.Tests kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); Assert.IsType[]>(dict._arrayStorage); + + // The upgrade from property -> array should make space for at least 4 entries + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("age", 30), kvp), + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); } [Fact] @@ -994,6 +1041,19 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.False(result); } + [Fact] + public void ContainsKey_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.ContainsKey(""); + + // Assert + Assert.False(result); + } + [Fact] public void ContainsKey_PropertyStorage_False() { @@ -1206,6 +1266,19 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.False(result); } + [Fact] + public void Remove_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.Remove(""); + + // Assert + Assert.False(result); + } + [Fact] public void Remove_PropertyStorage_Empty() { @@ -1320,6 +1393,132 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.IsType[]>(dict._arrayStorage); } + [Fact] + public void TryAdd_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.TryAdd("", "foo"); + + // Assert + Assert.True(result); + } + + // We always 'upgrade' if you are trying to write to the dictionary. + [Fact] + public void TryAdd_ConvertsPropertyStorage_ToArrayStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var result = dict.TryAdd("key", "value"); + + // Assert + Assert.False(result); + Assert.Null(dict._propertyStorage); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryAdd_EmptyStorage_CanAdd() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.TryAdd("key", "value"); + + // Assert + Assert.True(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryAdd_ArrayStorage_CanAdd() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key0", "value0" }, + }; + + // Act + var result = dict.TryAdd("key1", "value1"); + + // Assert + Assert.True(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), + kvp => Assert.Equal(new KeyValuePair("key1", "value1"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryAdd_ArrayStorage_CanAddWithResize() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key0", "value0" }, + { "key1", "value1" }, + { "key2", "value2" }, + { "key3", "value3" }, + }; + + // Act + var result = dict.TryAdd("key4", "value4"); + + // Assert + Assert.True(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), + kvp => Assert.Equal(new KeyValuePair("key1", "value1"), kvp), + kvp => Assert.Equal(new KeyValuePair("key2", "value2"), kvp), + kvp => Assert.Equal(new KeyValuePair("key3", "value3"), kvp), + kvp => Assert.Equal(new KeyValuePair("key4", "value4"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryAdd_ArrayStorage_DoesNotAddWhenKeyIsPresent() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key0", "value0" }, + }; + + // Act + var result = dict.TryAdd("key0", "value1"); + + // Assert + Assert.False(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + [Fact] public void TryGetValue_EmptyStorage() { @@ -1335,6 +1534,20 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.Null(value); } + [Fact] + public void TryGetValue_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.TryGetValue("", out var value); + + // Assert + Assert.False(result); + Assert.Null(value); + } + [Fact] public void TryGetValue_PropertyStorage_False() { @@ -1618,4 +1831,4 @@ namespace Microsoft.AspNetCore.Routing.Tests public string State { get; set; } } } -} +} \ No newline at end of file From fbb0623f03e333b1e3f1d387418fe74372d98afe Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 16 Sep 2018 19:14:12 +0000 Subject: [PATCH 20/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 30 +++++++++++++++--------------- korebuild-lock.txt | 4 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 500c0656c9..a00bfab89c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,27 +4,27 @@ 0.10.13 - 3.0.0-alpha1-20180907.9 - 3.0.0-alpha1-10419 - 3.0.0-alpha1-10419 - 3.0.0-alpha1-10419 - 3.0.0-alpha1-10419 - 3.0.0-alpha1-10419 - 3.0.0-alpha1-10419 - 3.0.0-alpha1-10419 - 3.0.0-alpha1-10419 - 3.0.0-alpha1-10419 - 3.0.0-alpha1-10419 - 3.0.0-alpha1-10419 - 3.0.0-alpha1-10419 + 3.0.0-alpha1-20180911.2 + 3.0.0-alpha1-10454 + 3.0.0-alpha1-10454 + 3.0.0-alpha1-10454 + 3.0.0-alpha1-10454 + 3.0.0-alpha1-10454 + 3.0.0-alpha1-10454 + 3.0.0-alpha1-10454 + 3.0.0-alpha1-10454 + 3.0.0-alpha1-10454 + 3.0.0-alpha1-10454 + 3.0.0-alpha1-10454 + 3.0.0-alpha1-10454 2.0.9 2.1.3 2.2.0-preview2-26905-02 15.6.1 4.9.0 2.0.3 - 4.6.0-preview1-26905-03 - 4.6.0-preview1-26905-03 + 4.6.0-preview1-26907-04 + 4.6.0-preview1-26907-04 0.10.0 2.3.1 2.4.0 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index b614cab6e8..a817c10d28 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-20180907.9 -commithash:f997365a8832ff0a3cbd9a98df45734ac2723fa0 +version:3.0.0-alpha1-20180911.2 +commithash:2a2b7dbea1b247930c41da497f4ea0b2bb756818 From 2422138ab417dbb569723700d34eb513d4ac1c4b Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 23 Sep 2018 12:13:37 -0700 Subject: [PATCH 21/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 26 +++++++++++++------------- korebuild-lock.txt | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 56f33d7e90..de91692a3f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,19 +4,19 @@ 0.10.13 - 3.0.0-alpha1-20180911.2 - 3.0.0-alpha1-10454 - 3.0.0-alpha1-10454 - 3.0.0-alpha1-10454 - 3.0.0-alpha1-10454 - 3.0.0-alpha1-10454 - 3.0.0-alpha1-10454 - 3.0.0-alpha1-10454 - 3.0.0-alpha1-10454 - 3.0.0-alpha1-10454 - 3.0.0-alpha1-10454 - 3.0.0-alpha1-10454 - 3.0.0-alpha1-10454 + 3.0.0-alpha1-20180919.1 + 3.0.0-alpha1-10495 + 3.0.0-alpha1-10495 + 3.0.0-alpha1-10495 + 3.0.0-alpha1-10495 + 3.0.0-alpha1-10495 + 3.0.0-alpha1-10495 + 3.0.0-alpha1-10495 + 3.0.0-alpha1-10495 + 3.0.0-alpha1-10495 + 3.0.0-alpha1-10495 + 3.0.0-alpha1-10495 + 3.0.0-alpha1-10495 2.0.9 2.1.3 2.2.0-preview2-26905-02 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index a817c10d28..d66a13bdc7 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-20180911.2 -commithash:2a2b7dbea1b247930c41da497f4ea0b2bb756818 +version:3.0.0-alpha1-20180919.1 +commithash:3066ae0a230870ea07e3f132605b5e5493f8bbd4 From cbed7393a21483cf25cda77dea8b7cda2b51751f Mon Sep 17 00:00:00 2001 From: Roma Marusyk Date: Thu, 27 Sep 2018 19:53:39 +0300 Subject: [PATCH 22/41] Add XML documentation for IFormFileCollection. #3528 (#1040) --- .../IFormFileCollection.cs | 23 +++++++++++++++++++ .../Internal/FormFileCollection.cs | 6 +++++ 2 files changed, 29 insertions(+) diff --git a/src/Microsoft.AspNetCore.Http.Features/IFormFileCollection.cs b/src/Microsoft.AspNetCore.Http.Features/IFormFileCollection.cs index e66c96e05d..b7ec5f0af8 100644 --- a/src/Microsoft.AspNetCore.Http.Features/IFormFileCollection.cs +++ b/src/Microsoft.AspNetCore.Http.Features/IFormFileCollection.cs @@ -10,10 +10,33 @@ namespace Microsoft.AspNetCore.Http /// public interface IFormFileCollection : IReadOnlyList { + /// + /// Gets the file with the specified name. + /// + /// The name of the file to get. + /// + /// The requested file, or null if it is not present. + /// IFormFile this[string name] { get; } + /// + /// Gets the file with the specified name. + /// + /// The name of the file to get. + /// + /// The requested file, or null if it is not present. + /// IFormFile GetFile(string name); + /// + /// Gets an containing the files of the + /// with the specified name. + /// + /// The name of the files to get. + /// + /// An containing the files of the object + /// that implements . + /// IReadOnlyList GetFiles(string name); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Http/Internal/FormFileCollection.cs b/src/Microsoft.AspNetCore.Http/Internal/FormFileCollection.cs index 806e756a8e..bb13e121b2 100644 --- a/src/Microsoft.AspNetCore.Http/Internal/FormFileCollection.cs +++ b/src/Microsoft.AspNetCore.Http/Internal/FormFileCollection.cs @@ -6,10 +6,15 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Http.Internal { + /// + /// Default implementation of . + /// public class FormFileCollection : List, IFormFileCollection { + /// public IFormFile this[string name] => GetFile(name); + /// public IFormFile GetFile(string name) { foreach (var file in this) @@ -23,6 +28,7 @@ namespace Microsoft.AspNetCore.Http.Internal return null; } + /// public IReadOnlyList GetFiles(string name) { var files = new List(); From 65338ca9f18e4ed306aa3a58f792e50206e65ded Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Thu, 27 Sep 2018 15:32:25 -0700 Subject: [PATCH 23/41] Update LICENSE.txt --- LICENSE.txt | 207 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 197 insertions(+), 10 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 7b2956ecee..b3b180cd51 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,14 +1,201 @@ -Copyright (c) .NET Foundation and Contributors + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -All rights reserved. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at + 1. Definitions. - http://www.apache.org/licenses/LICENSE-2.0 + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) .NET Foundation and Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From c7eaf77953964b3c30e98a55f45c01300de7a7fc Mon Sep 17 00:00:00 2001 From: Roma Marusyk Date: Fri, 28 Sep 2018 03:13:28 +0300 Subject: [PATCH 24/41] Fix XML documentation for IFormFileCollection (#1041) --- .../IFormFileCollection.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNetCore.Http.Features/IFormFileCollection.cs b/src/Microsoft.AspNetCore.Http.Features/IFormFileCollection.cs index b7ec5f0af8..ab862c917b 100644 --- a/src/Microsoft.AspNetCore.Http.Features/IFormFileCollection.cs +++ b/src/Microsoft.AspNetCore.Http.Features/IFormFileCollection.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Http public interface IFormFileCollection : IReadOnlyList { /// - /// Gets the file with the specified name. + /// Gets the first file with the specified name. /// /// The name of the file to get. /// @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Http IFormFile this[string name] { get; } /// - /// Gets the file with the specified name. + /// Gets the first file with the specified name. /// /// The name of the file to get. /// @@ -39,4 +39,4 @@ namespace Microsoft.AspNetCore.Http /// IReadOnlyList GetFiles(string name); } -} \ No newline at end of file +} From e8d5a85c9949e26914abc8b96594f741725c0442 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 30 Sep 2018 12:14:17 -0700 Subject: [PATCH 25/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index de91692a3f..e027d8648a 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -5,18 +5,18 @@ 0.10.13 3.0.0-alpha1-20180919.1 - 3.0.0-alpha1-10495 - 3.0.0-alpha1-10495 - 3.0.0-alpha1-10495 - 3.0.0-alpha1-10495 - 3.0.0-alpha1-10495 - 3.0.0-alpha1-10495 - 3.0.0-alpha1-10495 - 3.0.0-alpha1-10495 - 3.0.0-alpha1-10495 - 3.0.0-alpha1-10495 - 3.0.0-alpha1-10495 - 3.0.0-alpha1-10495 + 3.0.0-alpha1-10549 + 3.0.0-alpha1-10549 + 3.0.0-alpha1-10549 + 3.0.0-alpha1-10549 + 3.0.0-alpha1-10549 + 3.0.0-alpha1-10549 + 3.0.0-alpha1-10549 + 3.0.0-alpha1-10549 + 3.0.0-alpha1-10549 + 3.0.0-alpha1-10549 + 3.0.0-alpha1-10549 + 3.0.0-alpha1-10549 2.0.9 2.1.3 2.2.0-preview2-26905-02 From 5f75c07bbff7824275c1486c3cc03f0df3eef3af Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 2 Oct 2018 16:03:58 +1300 Subject: [PATCH 26/41] Add RoutesValues to HttpRequest (#1042) --- .../HttpRequest.cs | 7 +++ .../Features/RouteValuesFeature.cs | 34 ++++++++++++ .../Internal/DefaultHttpRequest.cs | 12 +++++ .../Internal/DefaultHttpRequestTests.cs | 52 +++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Http/Features/RouteValuesFeature.cs diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/HttpRequest.cs b/src/Microsoft.AspNetCore.Http.Abstractions/HttpRequest.cs index 3488bf3bb5..4c4d0d1af1 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/HttpRequest.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/HttpRequest.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Http { @@ -117,5 +118,11 @@ namespace Microsoft.AspNetCore.Http /// /// public abstract Task ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()); + + /// + /// Gets the collection of route values for this request. + /// + /// The collection of route values for this request. + public virtual RouteValueDictionary RouteValues { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Http/Features/RouteValuesFeature.cs b/src/Microsoft.AspNetCore.Http/Features/RouteValuesFeature.cs new file mode 100644 index 0000000000..e4a459e991 --- /dev/null +++ b/src/Microsoft.AspNetCore.Http/Features/RouteValuesFeature.cs @@ -0,0 +1,34 @@ +// 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 Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// A feature for routing values. Use + /// to access the values associated with the current request. + /// + public class RouteValuesFeature : IRouteValuesFeature + { + private RouteValueDictionary _routeValues; + + /// + /// Gets or sets the associated with the currrent + /// request. + /// + public RouteValueDictionary RouteValues + { + get + { + if (_routeValues == null) + { + _routeValues = new RouteValueDictionary(); + } + + return _routeValues; + } + set => _routeValues = value; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpRequest.cs b/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpRequest.cs index f216475db6..e2512f60dc 100644 --- a/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpRequest.cs +++ b/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpRequest.cs @@ -6,6 +6,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Http.Internal @@ -17,6 +18,7 @@ namespace Microsoft.AspNetCore.Http.Internal private readonly static Func _newQueryFeature = f => new QueryFeature(f); private readonly static Func _newFormFeature = r => new FormFeature(r); private readonly static Func _newRequestCookiesFeature = f => new RequestCookiesFeature(f); + private readonly static Func _newRouteValuesFeature = f => new RouteValuesFeature(); private HttpContext _context; private FeatureReferences _features; @@ -52,6 +54,9 @@ namespace Microsoft.AspNetCore.Http.Internal private IRequestCookiesFeature RequestCookiesFeature => _features.Fetch(ref _features.Cache.Cookies, _newRequestCookiesFeature); + private IRouteValuesFeature RouteValuesFeature => + _features.Fetch(ref _features.Cache.RouteValues, _newRouteValuesFeature); + public override PathString PathBase { get { return new PathString(HttpRequestFeature.PathBase); } @@ -151,12 +156,19 @@ namespace Microsoft.AspNetCore.Http.Internal return FormFeature.ReadFormAsync(cancellationToken); } + public override RouteValueDictionary RouteValues + { + get { return RouteValuesFeature.RouteValues; } + set { RouteValuesFeature.RouteValues = value; } + } + struct FeatureInterfaces { public IHttpRequestFeature Request; public IQueryFeature Query; public IFormFeature Form; public IRequestCookiesFeature Cookies; + public IRouteValuesFeature RouteValues; } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Http.Tests/Internal/DefaultHttpRequestTests.cs b/test/Microsoft.AspNetCore.Http.Tests/Internal/DefaultHttpRequestTests.cs index dbe1d54dd0..09e47a962e 100644 --- a/test/Microsoft.AspNetCore.Http.Tests/Internal/DefaultHttpRequestTests.cs +++ b/test/Microsoft.AspNetCore.Http.Tests/Internal/DefaultHttpRequestTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Primitives; using Xunit; @@ -194,6 +195,57 @@ namespace Microsoft.AspNetCore.Http.Internal Assert.Equal(new[] { "name2=value2" }, cookieHeaders); } + [Fact] + public void RouteValues_GetAndSet() + { + var context = new DefaultHttpContext(); + var request = context.Request; + + var routeValuesFeature = context.Features.Get(); + // No feature set for initial DefaultHttpRequest + Assert.Null(routeValuesFeature); + + // Route values returns empty collection by default + Assert.Empty(request.RouteValues); + + // Get and set value on request route values + request.RouteValues["new"] = "setvalue"; + Assert.Equal("setvalue", request.RouteValues["new"]); + + routeValuesFeature = context.Features.Get(); + // Accessing DefaultHttpRequest.RouteValues creates feature + Assert.NotNull(routeValuesFeature); + + request.RouteValues = new RouteValueDictionary(new { key = "value" }); + // Can set DefaultHttpRequest.RouteValues + Assert.NotNull(request.RouteValues); + Assert.Equal("value", request.RouteValues["key"]); + + // DefaultHttpRequest.RouteValues uses feature + Assert.Equal(routeValuesFeature.RouteValues, request.RouteValues); + + // Setting route values to null sets empty collection on request + routeValuesFeature.RouteValues = null; + Assert.Empty(request.RouteValues); + + var customRouteValuesFeature = new CustomRouteValuesFeature + { + RouteValues = new RouteValueDictionary(new { key = "customvalue" }) + }; + context.Features.Set(customRouteValuesFeature); + // Can override DefaultHttpRequest.RouteValues with custom feature + Assert.Equal(customRouteValuesFeature.RouteValues, request.RouteValues); + + // Can clear feature + context.Features.Set(null); + Assert.Empty(request.RouteValues); + } + + private class CustomRouteValuesFeature : IRouteValuesFeature + { + public RouteValueDictionary RouteValues { get; set; } + } + private static HttpRequest CreateRequest(IHeaderDictionary headers) { var context = new DefaultHttpContext(); From fc6ac2361e056306d5236996d22fc30719a2315b Mon Sep 17 00:00:00 2001 From: Jo Ham Date: Fri, 5 Oct 2018 10:52:15 -0400 Subject: [PATCH 27/41] Fix Typo (#1044) --- .../AuthenticationToken.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationToken.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationToken.cs index 555da9e098..e188e98823 100644 --- a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationToken.cs +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationToken.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Authentication { /// - /// Name/Value representing an token. + /// Name/Value representing a token. /// public class AuthenticationToken { @@ -19,4 +19,4 @@ namespace Microsoft.AspNetCore.Authentication /// public string Value { get; set; } } -} \ No newline at end of file +} From 449356bfc4d4f5eab9bc6ffbc1f756db949e2f2e Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 7 Oct 2018 12:14:43 -0700 Subject: [PATCH 28/41] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 28 ++++++++++++++-------------- korebuild-lock.txt | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 1845c3a2d6..2aa50e3e13 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,22 +4,22 @@ 0.10.13 - 3.0.0-alpha1-20180919.1 - 3.0.0-alpha1-10549 - 3.0.0-alpha1-10549 - 3.0.0-alpha1-10549 - 3.0.0-alpha1-10549 - 3.0.0-alpha1-10549 - 3.0.0-alpha1-10549 - 3.0.0-alpha1-10549 - 3.0.0-alpha1-10549 - 3.0.0-alpha1-10549 - 3.0.0-alpha1-10549 - 3.0.0-alpha1-10549 - 3.0.0-alpha1-10549 + 3.0.0-alpha1-20181004.7 + 3.0.0-alpha1-10584 + 3.0.0-alpha1-10584 + 3.0.0-alpha1-10584 + 3.0.0-alpha1-10584 + 3.0.0-alpha1-10584 + 3.0.0-alpha1-10584 + 3.0.0-alpha1-10584 + 3.0.0-alpha1-10584 + 3.0.0-alpha1-10584 + 3.0.0-alpha1-10584 + 3.0.0-alpha1-10584 + 3.0.0-alpha1-10584 2.0.9 2.1.3 - 2.2.0-preview3-26927-02 + 2.2.0-preview2-26905-02 15.6.1 4.9.0 2.0.3 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index d66a13bdc7..591cfd52ff 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:3.0.0-alpha1-20180919.1 -commithash:3066ae0a230870ea07e3f132605b5e5493f8bbd4 +version:3.0.0-alpha1-20181004.7 +commithash:27fabdaf2b1d4753c3d2749581694ca65d78f7f2 From 39c25357c621bf11c700ed268e5ef27e0eaf2160 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 17 Oct 2018 12:48:23 -0400 Subject: [PATCH 29/41] Skip extensions in SetCookie parser #1049 (#1050) --- .../SetCookieHeaderValue.cs | 14 ++++++++++---- .../SetCookieHeaderValueTest.cs | 12 ++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs index f3477648de..74b5c6c48c 100644 --- a/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs @@ -452,9 +452,15 @@ namespace Microsoft.Net.Http.Headers result.HttpOnly = true; } // extension-av = - else - { - // TODO: skip it? Store it in a list? + else + { + // TODO: skiping it for now to avoid parsing failure? Store it in a list? + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) + { + return 0; + } + ReadToSemicolonOrEnd(input, ref offset); } } @@ -520,4 +526,4 @@ namespace Microsoft.Net.Http.Headers ^ HttpOnly.GetHashCode(); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs index 9a920f40d0..058f8d4bd9 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs +++ b/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs @@ -365,6 +365,18 @@ namespace Microsoft.Net.Http.Headers Assert.Equal(cookies, results); } + [Fact] + public void SetCookieHeaderValue_TryParse_SkipExtensionValues() + { + string cookieHeaderValue = "cookiename=value; extensionname=value;"; + + SetCookieHeaderValue setCookieHeaderValue; + + SetCookieHeaderValue.TryParse(cookieHeaderValue, out setCookieHeaderValue); + + Assert.Equal("value", setCookieHeaderValue.Value); + } + [Theory] [MemberData(nameof(ListOfSetCookieHeaderDataSet))] public void SetCookieHeaderValue_ParseStrictList_AcceptsValidValues(IList cookies, string[] input) From d17d9155c7d2677a2eba4075fc472229266ecf19 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 17 Oct 2018 14:09:12 -0700 Subject: [PATCH 30/41] Port aspnet/Routing/pull/858 --- .../Routing/RouteValueDictionary.cs | 45 +++- .../RouteValueDictionaryTests.cs | 228 ++++++++++++++++++ 2 files changed, 272 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs index 396f79b507..9fe777358b 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs @@ -388,7 +388,9 @@ namespace Microsoft.AspNetCore.Routing return false; } - EnsureCapacity(Count); + // Ensure property storage is converted to array storage as we'll be + // applying the lookup and removal on the array + EnsureCapacity(_count); var index = FindIndex(key); if (index >= 0) @@ -404,6 +406,47 @@ namespace Microsoft.AspNetCore.Routing return false; } + /// + /// Attempts to remove and return the value that has the specified key from the . + /// + /// The key of the element to remove and return. + /// When this method returns, contains the object removed from the , or null if key does not exist. + /// + /// true if the object was removed successfully; otherwise, false. + /// + public bool Remove(string key, out object value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (_count == 0) + { + value = default; + return false; + } + + // Ensure property storage is converted to array storage as we'll be + // applying the lookup and removal on the array + EnsureCapacity(_count); + + var index = FindIndex(key); + if (index >= 0) + { + _count--; + var array = _arrayStorage; + value = array[index].Value; + Array.Copy(array, index + 1, array, index, _count - index); + array[_count] = default; + + return true; + } + + value = default; + return false; + } + /// /// Attempts to the add the provided and to the dictionary. /// diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs index 65a97c70ba..c4aa286fba 100644 --- a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs @@ -1393,6 +1393,234 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.IsType[]>(dict._arrayStorage); } + [Fact] + public void Remove_KeyAndOutValue_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + } + + [Fact] + public void Remove_KeyAndOutValue_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.Remove("", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + } + + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_Empty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); + + // Act + var result = dict.Remove("other", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + Assert.Empty(dict); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("other", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_True() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary(new { key = value }); + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_True_CaseInsensitive() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary(new { key = value }); + + // Act + var result = dict.Remove("kEy", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("other", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_True() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() + { + { "key", value } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_True_CaseInsensitive() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() + { + { "key", value } + }; + + // Act + var result = dict.Remove("kEy", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_First() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() + { + { "key", value }, + { "other", 5 }, + { "dotnet", "rocks" } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_Middle() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() + { + { "other", 5 }, + { "key", value }, + { "dotnet", "rocks" } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_Last() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() + { + { "other", 5 }, + { "dotnet", "rocks" }, + { "key", value } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } + [Fact] public void TryAdd_EmptyStringIsAllowed() { From ec176b2e40699c24cc1c0e3bde8a828e93fe67c7 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Thu, 25 Oct 2018 18:43:21 -0700 Subject: [PATCH 31/41] Merge release/2.2 (#1055) * Add safe copy for enumeration (#1052) --- .../AuthenticationSchemeProvider.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationSchemeProvider.cs b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationSchemeProvider.cs index c14608511b..a7b913b1b2 100644 --- a/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationSchemeProvider.cs +++ b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationSchemeProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; @@ -49,6 +50,9 @@ namespace Microsoft.AspNetCore.Authentication private readonly IDictionary _schemes; private readonly List _requestHandlers; + // Used as a safe return value for enumeration apis + private IEnumerable _schemesCopy = Array.Empty(); + private IEnumerable _requestHandlersCopy = Array.Empty(); private Task GetDefaultSchemeAsync() => _options.DefaultScheme != null @@ -123,7 +127,7 @@ namespace Microsoft.AspNetCore.Authentication /// /// The schemes in priority order for request handling public virtual Task> GetRequestHandlerSchemesAsync() - => Task.FromResult>(_requestHandlers); + => Task.FromResult(_requestHandlersCopy); /// /// Registers a scheme for use by . @@ -144,8 +148,10 @@ namespace Microsoft.AspNetCore.Authentication if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType)) { _requestHandlers.Add(scheme); + _requestHandlersCopy = _requestHandlers.ToArray(); } _schemes[scheme.Name] = scheme; + _schemesCopy = _schemes.Values.ToArray(); } } @@ -164,13 +170,17 @@ namespace Microsoft.AspNetCore.Authentication if (_schemes.ContainsKey(name)) { var scheme = _schemes[name]; - _requestHandlers.Remove(scheme); + if (_requestHandlers.Remove(scheme)) + { + _requestHandlersCopy = _requestHandlers.ToArray(); + } _schemes.Remove(name); + _schemesCopy = _schemes.Values.ToArray(); } } } public virtual Task> GetAllSchemesAsync() - => Task.FromResult>(_schemes.Values); + => Task.FromResult(_schemesCopy); } } \ No newline at end of file From 187e89f6f0ee67728a50f987d63234c1900130fd Mon Sep 17 00:00:00 2001 From: Filip W Date: Tue, 30 Oct 2018 20:32:26 +0100 Subject: [PATCH 32/41] Do not throw a null reference from request.GetDisplayUrl() (#1057) --- .../UriHelper.cs | 13 +++++++------ .../UriHelperTests.cs | 10 ++++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.AspNetCore.Http.Extensions/UriHelper.cs b/src/Microsoft.AspNetCore.Http.Extensions/UriHelper.cs index 633e591186..a6b5846a7d 100644 --- a/src/Microsoft.AspNetCore.Http.Extensions/UriHelper.cs +++ b/src/Microsoft.AspNetCore.Http.Extensions/UriHelper.cs @@ -195,17 +195,18 @@ namespace Microsoft.AspNetCore.Http.Extensions /// public static string GetDisplayUrl(this HttpRequest request) { - var host = request.Host.Value; - var pathBase = request.PathBase.Value; - var path = request.Path.Value; - var queryString = request.QueryString.Value; + var scheme = request.Scheme ?? string.Empty; + var host = request.Host.Value ?? string.Empty; + var pathBase = request.PathBase.Value ?? string.Empty; + var path = request.Path.Value ?? string.Empty; + var queryString = request.QueryString.Value ?? string.Empty; // PERF: Calculate string length to allocate correct buffer size for StringBuilder. - var length = request.Scheme.Length + SchemeDelimiter.Length + host.Length + var length = scheme.Length + SchemeDelimiter.Length + host.Length + pathBase.Length + path.Length + queryString.Length; return new StringBuilder(length) - .Append(request.Scheme) + .Append(scheme) .Append(SchemeDelimiter) .Append(host) .Append(pathBase) diff --git a/test/Microsoft.AspNetCore.Http.Extensions.Tests/UriHelperTests.cs b/test/Microsoft.AspNetCore.Http.Extensions.Tests/UriHelperTests.cs index 11b045af4f..ba604d576e 100644 --- a/test/Microsoft.AspNetCore.Http.Extensions.Tests/UriHelperTests.cs +++ b/test/Microsoft.AspNetCore.Http.Extensions.Tests/UriHelperTests.cs @@ -55,17 +55,19 @@ namespace Microsoft.AspNetCore.Http.Extensions Assert.Equal("http://my.xn--host-cpd:80/un%3Fescaped/base/un%3Fescaped?name=val%23ue", request.GetEncodedUrl()); } - [Fact] - public void GetDisplayUrlFromRequest() + [Theory] + [InlineData("/un?escaped/base")] + [InlineData(null)] + public void GetDisplayUrlFromRequest(string pathBase) { var request = new DefaultHttpContext().Request; request.Scheme = "http"; request.Host = new HostString("my.HoΨst:80"); - request.PathBase = new PathString("/un?escaped/base"); + request.PathBase = new PathString(pathBase); request.Path = new PathString("/un?escaped"); request.QueryString = new QueryString("?name=val%23ue"); - Assert.Equal("http://my.hoψst:80/un?escaped/base/un?escaped?name=val%23ue", request.GetDisplayUrl()); + Assert.Equal("http://my.hoψst:80" + pathBase + "/un?escaped?name=val%23ue", request.GetDisplayUrl()); } [Theory] From d77b370fb196b95226410c2528a6af77e56a23a1 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 7 Nov 2018 12:24:15 +1300 Subject: [PATCH 33/41] Add Endpoint extension methods to HttpContext (#1060) --- .../Routing/EndpointHttpContextExtensions.cs | 70 ++++++++ .../EndpointHttpContextExtensionsTests.cs | 155 ++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointHttpContextExtensions.cs create mode 100644 test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointHttpContextExtensionsTests.cs diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointHttpContextExtensions.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointHttpContextExtensions.cs new file mode 100644 index 0000000000..bf4a5ed046 --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointHttpContextExtensions.cs @@ -0,0 +1,70 @@ +// 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 Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Extension methods to expose Endpoint on HttpContext. + /// + public static class EndpointHttpContextExtensions + { + /// + /// Extension method for getting the for the current request. + /// + /// The context. + /// The . + public static Endpoint GetEndpoint(this HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return context.Features.Get()?.Endpoint; + } + + /// + /// Extension method for setting the for the current request. + /// + /// The context. + /// The . + public static void SetEndpoint(this HttpContext context, Endpoint endpoint) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var feature = context.Features.Get(); + + if (endpoint != null) + { + if (feature == null) + { + feature = new EndpointFeature(); + context.Features.Set(feature); + } + + feature.Endpoint = endpoint; + } + else + { + if (feature == null) + { + // No endpoint to set and no feature on context. Do nothing + return; + } + + feature.Endpoint = null; + } + } + + private class EndpointFeature : IEndpointFeature + { + public Endpoint Endpoint { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointHttpContextExtensionsTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointHttpContextExtensionsTests.cs new file mode 100644 index 0000000000..c34f06f380 --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointHttpContextExtensionsTests.cs @@ -0,0 +1,155 @@ +// 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.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Abstractions.Tests +{ + public class EndpointHttpContextExtensionsTests + { + [Fact] + public void GetEndpoint_ContextWithoutFeature_ReturnsNull() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act + var endpoint = context.GetEndpoint(); + + // Assert + Assert.Null(endpoint); + } + + [Fact] + public void GetEndpoint_ContextWithFeatureAndNullEndpoint_ReturnsNull() + { + // Arrange + var context = new DefaultHttpContext(); + context.Features.Set(new EndpointFeature + { + Endpoint = null + }); + + // Act + var endpoint = context.GetEndpoint(); + + // Assert + Assert.Null(endpoint); + } + + [Fact] + public void GetEndpoint_ContextWithFeatureAndEndpoint_ReturnsNull() + { + // Arrange + var context = new DefaultHttpContext(); + var initial = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + context.Features.Set(new EndpointFeature + { + Endpoint = initial + }); + + // Act + var endpoint = context.GetEndpoint(); + + // Assert + Assert.Equal(initial, endpoint); + } + + [Fact] + public void SetEndpoint_NullOnContextWithoutFeature_NoFeatureSet() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act + context.SetEndpoint(null); + + // Assert + Assert.Null(context.Features.Get()); + } + + [Fact] + public void SetEndpoint_EndpointOnContextWithoutFeature_FeatureWithEndpointSet() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act + var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + context.SetEndpoint(endpoint); + + // Assert + var feature = context.Features.Get(); + Assert.NotNull(feature); + Assert.Equal(endpoint, feature.Endpoint); + } + + [Fact] + public void SetEndpoint_EndpointOnContextWithFeature_EndpointSetOnExistingFeature() + { + // Arrange + var context = new DefaultHttpContext(); + var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + var initialFeature = new EndpointFeature + { + Endpoint = initialEndpoint + }; + context.Features.Set(initialFeature); + + // Act + var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + context.SetEndpoint(endpoint); + + // Assert + var feature = context.Features.Get(); + Assert.Equal(initialFeature, feature); + Assert.Equal(endpoint, feature.Endpoint); + } + + [Fact] + public void SetEndpoint_NullOnContextWithFeature_NullSetOnExistingFeature() + { + // Arrange + var context = new DefaultHttpContext(); + var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + var initialFeature = new EndpointFeature + { + Endpoint = initialEndpoint + }; + context.Features.Set(initialFeature); + + // Act + context.SetEndpoint(null); + + // Assert + var feature = context.Features.Get(); + Assert.Equal(initialFeature, feature); + Assert.Null(feature.Endpoint); + } + + [Fact] + public void SetAndGetEndpoint_Roundtrip_EndpointIsRoundtrip() + { + // Arrange + var context = new DefaultHttpContext(); + var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + + // Act + context.SetEndpoint(initialEndpoint); + var endpoint = context.GetEndpoint(); + + // Assert + Assert.Equal(initialEndpoint, endpoint); + } + + private class EndpointFeature : IEndpointFeature + { + public Endpoint Endpoint { get; set; } + } + } +} From 066c5ce337f1d43e0ccc62edeb4258c368f35dbc Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 7 Nov 2018 12:57:31 +1300 Subject: [PATCH 34/41] Implicitly execute matched endpoint at the end of middleware pipeline (#1059) --- .../Internal/ApplicationBuilder.cs | 7 +++ .../Internal/ApplicationBuilderTests.cs | 55 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs b/src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs index d0b6b6f6bf..291ccc3893 100644 --- a/src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs +++ b/src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs @@ -81,6 +81,13 @@ namespace Microsoft.AspNetCore.Builder.Internal { RequestDelegate app = context => { + // Implicitly execute matched endpoint at the end of the pipeline instead of returning 404 + var endpointRequestDelegate = context.GetEndpoint()?.RequestDelegate; + if (endpointRequestDelegate != null) + { + return endpointRequestDelegate(context); + } + context.Response.StatusCode = 404; return Task.CompletedTask; }; diff --git a/test/Microsoft.AspNetCore.Http.Tests/Internal/ApplicationBuilderTests.cs b/test/Microsoft.AspNetCore.Http.Tests/Internal/ApplicationBuilderTests.cs index e1336c82ba..cee2042aae 100644 --- a/test/Microsoft.AspNetCore.Http.Tests/Internal/ApplicationBuilderTests.cs +++ b/test/Microsoft.AspNetCore.Http.Tests/Internal/ApplicationBuilderTests.cs @@ -1,7 +1,9 @@ // 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 Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Xunit; namespace Microsoft.AspNetCore.Builder.Internal @@ -20,6 +22,59 @@ namespace Microsoft.AspNetCore.Builder.Internal Assert.Equal(404, httpContext.Response.StatusCode); } + [Fact] + public void BuildImplicitlyCallsMatchedEndpointAsLastStep() + { + var builder = new ApplicationBuilder(null); + var app = builder.Build(); + + var endpointCalled = false; + var endpoint = new Endpoint( + context => + { + endpointCalled = true; + return Task.CompletedTask; + }, + EndpointMetadataCollection.Empty, + "Test endpoint"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(endpoint); + + app.Invoke(httpContext); + + Assert.True(endpointCalled); + } + + [Fact] + public void BuildDoesNotCallMatchedEndpointWhenTerminated() + { + var builder = new ApplicationBuilder(null); + builder.Use((context, next) => + { + // Do not call next + return Task.CompletedTask; + }); + var app = builder.Build(); + + var endpointCalled = false; + var endpoint = new Endpoint( + context => + { + endpointCalled = true; + return Task.CompletedTask; + }, + EndpointMetadataCollection.Empty, + "Test endpoint"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(endpoint); + + app.Invoke(httpContext); + + Assert.False(endpointCalled); + } + [Fact] public void PropertiesDictionaryIsDistinctAfterNew() { From 1d3521113ad69a9903d1590c77d67da04c5f5dfd Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 8 Nov 2018 15:52:01 +1300 Subject: [PATCH 35/41] Change namespace to Microsoft.AspNetCore.Http.Endpoints (#1061) --- .../Routing/EndpointHttpContextExtensions.cs | 2 +- src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs | 1 + .../EndpointHttpContextExtensionsTests.cs | 1 + .../Internal/ApplicationBuilderTests.cs | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointHttpContextExtensions.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointHttpContextExtensions.cs index bf4a5ed046..642af5f4d5 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointHttpContextExtensions.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointHttpContextExtensions.cs @@ -4,7 +4,7 @@ using System; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http.Endpoints { /// /// Extension methods to expose Endpoint on HttpContext. diff --git a/src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs b/src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs index 291ccc3893..8c7e5d1a50 100644 --- a/src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs +++ b/src/Microsoft.AspNetCore.Http/Internal/ApplicationBuilder.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Internal; using Microsoft.Extensions.Internal; diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointHttpContextExtensionsTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointHttpContextExtensionsTests.cs index c34f06f380..b65af91f4f 100644 --- a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointHttpContextExtensionsTests.cs +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointHttpContextExtensionsTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Endpoints; using Microsoft.AspNetCore.Http.Features; using Xunit; diff --git a/test/Microsoft.AspNetCore.Http.Tests/Internal/ApplicationBuilderTests.cs b/test/Microsoft.AspNetCore.Http.Tests/Internal/ApplicationBuilderTests.cs index cee2042aae..2c57489f91 100644 --- a/test/Microsoft.AspNetCore.Http.Tests/Internal/ApplicationBuilderTests.cs +++ b/test/Microsoft.AspNetCore.Http.Tests/Internal/ApplicationBuilderTests.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Endpoints; using Xunit; namespace Microsoft.AspNetCore.Builder.Internal From 5da68a2bb750f1a5cff74295b9d7098cfc7b6266 Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Wed, 14 Nov 2018 21:15:08 +0100 Subject: [PATCH 36/41] Improve performance and reduce allocations in RouteValueDictionary. (#1062) --- .../RouteValueDictionaryBenchmark.cs | 147 +++++++++++++++++- .../Routing/RouteValueDictionary.cs | 87 ++++++++--- .../RouteValueDictionaryTests.cs | 137 ++++++++++++++-- 3 files changed, 339 insertions(+), 32 deletions(-) diff --git a/benchmarks/Microsoft.AspNetCore.Http.Performance/RouteValueDictionaryBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Http.Performance/RouteValueDictionaryBenchmark.cs index c256d31a55..2dfc36afa4 100644 --- a/benchmarks/Microsoft.AspNetCore.Http.Performance/RouteValueDictionaryBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Http.Performance/RouteValueDictionaryBenchmark.cs @@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Routing { private RouteValueDictionary _arrayValues; private RouteValueDictionary _propertyValues; + private RouteValueDictionary _arrayValuesEmpty; // We modify the route value dictionaries in many of these benchmarks. [IterationSetup] @@ -21,9 +22,22 @@ namespace Microsoft.AspNetCore.Routing { "controller", "Home" }, { "id", "17" }, }; + _arrayValuesEmpty = new RouteValueDictionary(); _propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); } + [Benchmark] + public void Ctor_Values_RouteValueDictionary_EmptyArray() + { + new RouteValueDictionary(_arrayValuesEmpty); + } + + [Benchmark] + public void Ctor_Values_RouteValueDictionary_Array() + { + new RouteValueDictionary(_arrayValues); + } + [Benchmark] public RouteValueDictionary AddSingleItem() { @@ -47,7 +61,136 @@ namespace Microsoft.AspNetCore.Routing } [Benchmark] - public RouteValueDictionary ConditionalAdd_ContainsKeyAdd() + public void ContainsKey_Array_Found() + { + _arrayValues.ContainsKey("id"); + } + + [Benchmark] + public void ContainsKey_Array_NotFound() + { + _arrayValues.ContainsKey("name"); + } + + [Benchmark] + public void ContainsKey_Properties_Found() + { + _propertyValues.ContainsKey("id"); + } + + [Benchmark] + public void ContainsKey_Properties_NotFound() + { + _propertyValues.ContainsKey("name"); + } + + [Benchmark] + public void TryAdd_Properties_AtCapacity_KeyExists() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17", area = "root" }); + propertyValues.TryAdd("id", "15"); + } + + [Benchmark] + public void TryAdd_Properties_AtCapacity_KeyDoesNotExist() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17", area = "root" }); + _propertyValues.TryAdd("name", "Service"); + } + + [Benchmark] + public void TryAdd_Properties_NotAtCapacity_KeyExists() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + propertyValues.TryAdd("id", "15"); + } + + [Benchmark] + public void TryAdd_Properties_NotAtCapacity_KeyDoesNotExist() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + _propertyValues.TryAdd("name", "Service"); + } + + [Benchmark] + public void TryAdd_Array_AtCapacity_KeyExists() + { + var arrayValues = new RouteValueDictionary + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" }, + { "area", "root" } + }; + arrayValues.TryAdd("id", "15"); + } + + [Benchmark] + public void TryAdd_Array_AtCapacity_KeyDoesNotExist() + { + var arrayValues = new RouteValueDictionary + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" }, + { "area", "root" } + }; + arrayValues.TryAdd("name", "Service"); + } + + [Benchmark] + public void TryAdd_Array_NotAtCapacity_KeyExists() + { + var arrayValues = new RouteValueDictionary + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" } + }; + arrayValues.TryAdd("id", "15"); + } + + [Benchmark] + public void TryAdd_Array_NotAtCapacity_KeyDoesNotExist() + { + var arrayValues = new RouteValueDictionary + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" }, + }; + arrayValues.TryAdd("name", "Service"); + } + + [Benchmark] + public void ConditionalAdd_Array() + { + var arrayValues = new RouteValueDictionary() + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" }, + }; + + if (!arrayValues.ContainsKey("name")) + { + arrayValues.Add("name", "Service"); + } + } + + [Benchmark] + public void ConditionalAdd_Properties() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + + if (!propertyValues.ContainsKey("name")) + { + propertyValues.Add("name", "Service"); + } + } + + [Benchmark] + public RouteValueDictionary ConditionalAdd_ContainsKey_Array() { var dictionary = _arrayValues; @@ -68,7 +211,7 @@ namespace Microsoft.AspNetCore.Routing return dictionary; } - + [Benchmark] public RouteValueDictionary ConditionalAdd_TryAdd() { diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs index 9fe777358b..13c433b387 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs @@ -97,7 +97,6 @@ namespace Microsoft.AspNetCore.Routing /// Only public instance non-index properties are considered. /// public RouteValueDictionary(object values) - : this() { if (values is RouteValueDictionary dictionary) { @@ -109,20 +108,27 @@ namespace Microsoft.AspNetCore.Routing return; } - var other = dictionary._arrayStorage; - var storage = new KeyValuePair[other.Length]; - if (dictionary._count != 0) + var count = dictionary._count; + if (count > 0) { - Array.Copy(other, 0, storage, 0, dictionary._count); + var other = dictionary._arrayStorage; + var storage = new KeyValuePair[count]; + Array.Copy(other, 0, storage, 0, count); + _arrayStorage = storage; + _count = count; + } + else + { + _arrayStorage = Array.Empty>(); } - _arrayStorage = storage; - _count = dictionary._count; return; } if (values is IEnumerable> keyValueEnumerable) { + _arrayStorage = Array.Empty>(); + foreach (var kvp in keyValueEnumerable) { Add(kvp.Key, kvp.Value); @@ -133,6 +139,8 @@ namespace Microsoft.AspNetCore.Routing if (values is IEnumerable> stringValueEnumerable) { + _arrayStorage = Array.Empty>(); + foreach (var kvp in stringValueEnumerable) { Add(kvp.Key, kvp.Value); @@ -146,7 +154,10 @@ namespace Microsoft.AspNetCore.Routing var storage = new PropertyStorage(values); _propertyStorage = storage; _count = storage.Properties.Length; - return; + } + else + { + _arrayStorage = Array.Empty>(); } } @@ -260,8 +271,7 @@ namespace Microsoft.AspNetCore.Routing EnsureCapacity(_count + 1); - var index = FindIndex(key); - if (index >= 0) + if (ContainsKeyArray(key)) { var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary)); throw new ArgumentException(message, nameof(key)); @@ -305,7 +315,18 @@ namespace Microsoft.AspNetCore.Routing ThrowArgumentNullExceptionForKey(); } - return TryGetValue(key, out var _); + return ContainsKeyCore(key); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ContainsKeyCore(string key) + { + if (_propertyStorage == null) + { + return ContainsKeyArray(key); + } + + return ContainsKeyProperties(key); } /// @@ -460,13 +481,7 @@ namespace Microsoft.AspNetCore.Routing ThrowArgumentNullExceptionForKey(); } - // Since this is an attempt to write to the dictionary, just make it an array if it isn't. If the code - // path we're on event tries to write to the dictionary, it will likely get 'upgraded' at some point, - // so we do it here to keep the code size and complexity down. - EnsureCapacity(Count); - - var index = FindIndex(key); - if (index >= 0) + if (ContainsKeyCore(key)) { return false; } @@ -603,6 +618,42 @@ namespace Microsoft.AspNetCore.Routing return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ContainsKeyArray(string key) + { + var array = _arrayStorage; + var count = _count; + + // Elide bounds check for indexing. + if ((uint)count <= (uint)array.Length) + { + for (var i = 0; i < count; i++) + { + if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ContainsKeyProperties(string key) + { + var properties = _propertyStorage.Properties; + for (var i = 0; i < properties.Length; i++) + { + if (string.Equals(properties[i].Name, key, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + public struct Enumerator : IEnumerator> { private readonly RouteValueDictionary _dictionary; diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs index c4aa286fba..ab5925e219 100644 --- a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs @@ -51,6 +51,8 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal(other, dict); + Assert.Single(dict._arrayStorage); + Assert.Null(dict._propertyStorage); var storage = Assert.IsType[]>(dict._arrayStorage); var otherStorage = Assert.IsType[]>(other._arrayStorage); @@ -68,6 +70,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal(other, dict); + Assert.Null(dict._arrayStorage); var storage = dict._propertyStorage; var otherStorage = other._propertyStorage; @@ -259,6 +262,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); Assert.Empty(dict); } @@ -273,6 +277,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); Assert.Empty(dict); } @@ -287,6 +292,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); Assert.Collection( dict.OrderBy(kvp => kvp.Key), kvp => @@ -314,6 +320,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); Assert.Collection( dict.OrderBy(kvp => kvp.Key), kvp => { Assert.Equal("DerivedProperty", kvp.Key); Assert.Equal(5, kvp.Value); }); @@ -330,6 +337,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); Assert.Empty(dict); } @@ -918,6 +926,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Empty(dict); Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); } [Fact] @@ -932,6 +941,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Empty(dict); Assert.Null(dict._propertyStorage); + Assert.Empty(dict._arrayStorage); } [Fact] @@ -949,10 +959,11 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Empty(dict); Assert.IsType[]>(dict._arrayStorage); + Assert.Null(dict._propertyStorage); } [Fact] - public void Contains_KeyValuePair_True() + public void Contains_ListStorage_KeyValuePair_True() { // Arrange var dict = new RouteValueDictionary() @@ -971,7 +982,7 @@ namespace Microsoft.AspNetCore.Routing.Tests } [Fact] - public void Contains_KeyValuePair_True_CaseInsensitive() + public void Contains_ListStory_KeyValuePair_True_CaseInsensitive() { // Arrange var dict = new RouteValueDictionary() @@ -990,7 +1001,7 @@ namespace Microsoft.AspNetCore.Routing.Tests } [Fact] - public void Contains_KeyValuePair_False() + public void Contains_ListStorage_KeyValuePair_False() { // Arrange var dict = new RouteValueDictionary() @@ -1010,7 +1021,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Value comparisons use the default equality comparer. [Fact] - public void Contains_KeyValuePair_False_ValueComparisonIsDefault() + public void Contains_ListStorage_KeyValuePair_False_ValueComparisonIsDefault() { // Arrange var dict = new RouteValueDictionary() @@ -1028,6 +1039,87 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.IsType[]>(dict._arrayStorage); } + [Fact] + public void Contains_PropertyStorage_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + var input = new KeyValuePair("key", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } + + [Fact] + public void Contains_PropertyStory_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + var input = new KeyValuePair("KEY", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } + + [Fact] + public void Contains_PropertyStorage_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + var input = new KeyValuePair("other", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } + + // Value comparisons use the default equality comparer. + [Fact] + public void Contains_PropertyStorage_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + var input = new KeyValuePair("key", "valUE"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } + [Fact] public void ContainsKey_EmptyStorage() { @@ -1066,6 +1158,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); } [Fact] @@ -1080,6 +1173,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); } [Fact] @@ -1094,6 +1188,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.NotNull(dict._propertyStorage); + Assert.Null(dict._arrayStorage); } [Fact] @@ -1393,6 +1488,7 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.IsType[]>(dict._arrayStorage); } + [Fact] public void Remove_KeyAndOutValue_EmptyStorage() { @@ -1634,9 +1730,28 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.True(result); } - // We always 'upgrade' if you are trying to write to the dictionary. [Fact] - public void TryAdd_ConvertsPropertyStorage_ToArrayStorage() + public void TryAdd_PropertyStorage_KeyDoesNotExist_ConvertsPropertyStorageToArrayStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var result = dict.TryAdd("otherKey", "value"); + + // Assert + Assert.True(result); + Assert.Null(dict._propertyStorage); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), + kvp => Assert.Equal(new KeyValuePair("otherKey", "value"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryAdd_PropertyStory_KeyExist_DoesNotConvertPropertyStorageToArrayStorage() { // Arrange var dict = new RouteValueDictionary(new { key = "value", }); @@ -1646,13 +1761,11 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); - Assert.Null(dict._propertyStorage); + Assert.Null(dict._arrayStorage); + Assert.NotNull(dict._propertyStorage); Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); } [Fact] From 31a836c9f35987c736161bf6e3f763517da8d504 Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Wed, 14 Nov 2018 21:16:44 +0100 Subject: [PATCH 37/41] Use Nullable.GetValueOrDefault() instead of Nullable.Value when a it is known to have a value. (#1063) --- .../CookieBuilder.cs | 2 +- .../Internal/ParsingHelpers.cs | 4 +-- .../HeaderDictionaryTypeExtensions.cs | 2 +- .../SendFileResponseExtensions.cs | 2 +- .../StreamCopyOperation.cs | 6 ++-- .../Features/FormFeature.cs | 2 +- .../HeaderDictionary.cs | 2 +- .../DictionaryStringValuesWrapper.cs | 2 +- .../MultipartReaderStream.cs | 4 +-- .../StreamHelperExtensions.cs | 4 +-- .../CacheControlHeaderValue.cs | 16 +++++----- .../ContentDispositionHeaderValue.cs | 6 ++-- .../ContentRangeHeaderValue.cs | 6 ++-- .../RangeConditionHeaderValue.cs | 6 ++-- .../RangeItemHeaderValue.cs | 29 +++++++++---------- .../SetCookieHeaderValue.cs | 8 ++--- .../StringWithQualityHeaderValue.cs | 6 ++-- 17 files changed, 53 insertions(+), 54 deletions(-) diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/CookieBuilder.cs b/src/Microsoft.AspNetCore.Http.Abstractions/CookieBuilder.cs index ce89e5b054..5c0db2a46f 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/CookieBuilder.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/CookieBuilder.cs @@ -107,7 +107,7 @@ namespace Microsoft.AspNetCore.Http Domain = Domain, IsEssential = IsEssential, Secure = SecurePolicy == CookieSecurePolicy.Always || (SecurePolicy == CookieSecurePolicy.SameAsRequest && context.Request.IsHttps), - Expires = Expiration.HasValue ? expiresFrom.Add(Expiration.Value) : default(DateTimeOffset?) + Expires = Expiration.HasValue ? expiresFrom.Add(Expiration.GetValueOrDefault()) : default(DateTimeOffset?) }; } } diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Internal/ParsingHelpers.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Internal/ParsingHelpers.cs index 185fc40ac7..971447fa44 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Internal/ParsingHelpers.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Internal/ParsingHelpers.cs @@ -103,13 +103,13 @@ namespace Microsoft.AspNetCore.Http.Internal { throw new ArgumentNullException(nameof(key)); } - if (!values.HasValue || StringValues.IsNullOrEmpty(values.Value)) + if (!values.HasValue || StringValues.IsNullOrEmpty(values.GetValueOrDefault())) { headers.Remove(key); } else { - headers[key] = values.Value; + headers[key] = values.GetValueOrDefault(); } } diff --git a/src/Microsoft.AspNetCore.Http.Extensions/HeaderDictionaryTypeExtensions.cs b/src/Microsoft.AspNetCore.Http.Extensions/HeaderDictionaryTypeExtensions.cs index 1723ee6fd5..70f1c4da71 100644 --- a/src/Microsoft.AspNetCore.Http.Extensions/HeaderDictionaryTypeExtensions.cs +++ b/src/Microsoft.AspNetCore.Http.Extensions/HeaderDictionaryTypeExtensions.cs @@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Http if (value.HasValue) { - headers[name] = HeaderUtilities.FormatDate(value.Value); + headers[name] = HeaderUtilities.FormatDate(value.GetValueOrDefault()); } else { diff --git a/src/Microsoft.AspNetCore.Http.Extensions/SendFileResponseExtensions.cs b/src/Microsoft.AspNetCore.Http.Extensions/SendFileResponseExtensions.cs index 74c0422ef4..90282bd2df 100644 --- a/src/Microsoft.AspNetCore.Http.Extensions/SendFileResponseExtensions.cs +++ b/src/Microsoft.AspNetCore.Http.Extensions/SendFileResponseExtensions.cs @@ -172,7 +172,7 @@ namespace Microsoft.AspNetCore.Http throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); } if (count.HasValue && - (count.Value < 0 || count.Value > fileLength - offset)) + (count.GetValueOrDefault() < 0 || count.GetValueOrDefault() > fileLength - offset)) { throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); } diff --git a/src/Microsoft.AspNetCore.Http.Extensions/StreamCopyOperation.cs b/src/Microsoft.AspNetCore.Http.Extensions/StreamCopyOperation.cs index 12067fef65..e0a301f4eb 100644 --- a/src/Microsoft.AspNetCore.Http.Extensions/StreamCopyOperation.cs +++ b/src/Microsoft.AspNetCore.Http.Extensions/StreamCopyOperation.cs @@ -42,13 +42,13 @@ namespace Microsoft.AspNetCore.Http.Extensions { Debug.Assert(source != null); Debug.Assert(destination != null); - Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.Value >= 0); + Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.GetValueOrDefault() >= 0); Debug.Assert(buffer != null); while (true) { // The natural end of the range. - if (bytesRemaining.HasValue && bytesRemaining.Value <= 0) + if (bytesRemaining.HasValue && bytesRemaining.GetValueOrDefault() <= 0) { return; } @@ -58,7 +58,7 @@ namespace Microsoft.AspNetCore.Http.Extensions int readLength = buffer.Length; if (bytesRemaining.HasValue) { - readLength = (int)Math.Min(bytesRemaining.Value, (long)readLength); + readLength = (int)Math.Min(bytesRemaining.GetValueOrDefault(), (long)readLength); } int read = await source.ReadAsync(buffer, 0, readLength, cancel); diff --git a/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs b/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs index 865e183f76..d93234d08b 100644 --- a/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs +++ b/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs @@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.Http.Features if (section.BaseStreamOffset.HasValue) { // Relative reference to buffered request body - file = new FormFile(_request.Body, section.BaseStreamOffset.Value, section.Body.Length, name, fileName); + file = new FormFile(_request.Body, section.BaseStreamOffset.GetValueOrDefault(), section.Body.Length, name, fileName); } else { diff --git a/src/Microsoft.AspNetCore.Http/HeaderDictionary.cs b/src/Microsoft.AspNetCore.Http/HeaderDictionary.cs index bc0b7a26ce..2113204823 100644 --- a/src/Microsoft.AspNetCore.Http/HeaderDictionary.cs +++ b/src/Microsoft.AspNetCore.Http/HeaderDictionary.cs @@ -121,7 +121,7 @@ namespace Microsoft.AspNetCore.Http ThrowIfReadOnly(); if (value.HasValue) { - this[HeaderNames.ContentLength] = HeaderUtilities.FormatNonNegativeInt64(value.Value); + this[HeaderNames.ContentLength] = HeaderUtilities.FormatNonNegativeInt64(value.GetValueOrDefault()); } else { diff --git a/src/Microsoft.AspNetCore.Owin/DictionaryStringValuesWrapper.cs b/src/Microsoft.AspNetCore.Owin/DictionaryStringValuesWrapper.cs index b31c7e9790..c590a86789 100644 --- a/src/Microsoft.AspNetCore.Owin/DictionaryStringValuesWrapper.cs +++ b/src/Microsoft.AspNetCore.Owin/DictionaryStringValuesWrapper.cs @@ -68,7 +68,7 @@ namespace Microsoft.AspNetCore.Owin { if (value.HasValue) { - Inner[HeaderNames.ContentLength] = (StringValues)HeaderUtilities.FormatNonNegativeInt64(value.Value); + Inner[HeaderNames.ContentLength] = (StringValues)HeaderUtilities.FormatNonNegativeInt64(value.GetValueOrDefault()); } else { diff --git a/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs b/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs index 7952bd34b2..e1c4f642c8 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/MultipartReaderStream.cs @@ -151,9 +151,9 @@ namespace Microsoft.AspNetCore.WebUtilities if (_observedLength < _position) { _observedLength = _position; - if (LengthLimit.HasValue && _observedLength > LengthLimit.Value) + if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault()) { - throw new InvalidDataException($"Multipart body length limit {LengthLimit.Value} exceeded."); + throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded."); } } return read; diff --git a/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs b/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs index e2c16a9cf2..f4ec558aaa 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/StreamHelperExtensions.cs @@ -34,9 +34,9 @@ namespace Microsoft.AspNetCore.WebUtilities { // Not all streams support cancellation directly. cancellationToken.ThrowIfCancellationRequested(); - if (limit.HasValue && limit.Value - total < read) + if (limit.HasValue && limit.GetValueOrDefault() - total < read) { - throw new InvalidDataException($"The stream exceeded the data limit {limit.Value}."); + throw new InvalidDataException($"The stream exceeded the data limit {limit.GetValueOrDefault()}."); } total += read; read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken); diff --git a/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs b/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs index 81e18faf47..655d9447ce 100644 --- a/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs @@ -197,14 +197,14 @@ namespace Microsoft.Net.Http.Headers { AppendValueWithSeparatorIfRequired(sb, MaxAgeString); sb.Append('='); - sb.Append(((int)_maxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(((int)_maxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); } if (_sharedMaxAge.HasValue) { AppendValueWithSeparatorIfRequired(sb, SharedMaxAgeString); sb.Append('='); - sb.Append(((int)_sharedMaxAge.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(((int)_sharedMaxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); } if (_maxStale) @@ -213,7 +213,7 @@ namespace Microsoft.Net.Http.Headers if (_maxStaleLimit.HasValue) { sb.Append('='); - sb.Append(((int)_maxStaleLimit.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(((int)_maxStaleLimit.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); } } @@ -221,7 +221,7 @@ namespace Microsoft.Net.Http.Headers { AppendValueWithSeparatorIfRequired(sb, MinFreshString); sb.Append('='); - sb.Append(((int)_minFresh.Value.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(((int)_minFresh.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); } if (_private) @@ -291,10 +291,10 @@ namespace Microsoft.Net.Http.Headers // XOR the hashcode of timespan values with different numbers to make sure two instances with the same // timespan set on different fields result in different hashcodes. - result = result ^ (_maxAge.HasValue ? _maxAge.Value.GetHashCode() ^ 1 : 0) ^ - (_sharedMaxAge.HasValue ? _sharedMaxAge.Value.GetHashCode() ^ 2 : 0) ^ - (_maxStaleLimit.HasValue ? _maxStaleLimit.Value.GetHashCode() ^ 4 : 0) ^ - (_minFresh.HasValue ? _minFresh.Value.GetHashCode() ^ 8 : 0); + result = result ^ (_maxAge.HasValue ? _maxAge.GetValueOrDefault().GetHashCode() ^ 1 : 0) ^ + (_sharedMaxAge.HasValue ? _sharedMaxAge.GetValueOrDefault().GetHashCode() ^ 2 : 0) ^ + (_maxStaleLimit.HasValue ? _maxStaleLimit.GetValueOrDefault().GetHashCode() ^ 4 : 0) ^ + (_minFresh.HasValue ? _minFresh.GetValueOrDefault().GetHashCode() ^ 8 : 0); if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0)) { diff --git a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs index 392a441733..5084f8ea06 100644 --- a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs @@ -137,11 +137,11 @@ namespace Microsoft.Net.Http.Headers } else if (sizeParameter != null) { - sizeParameter.Value = value.Value.ToString(CultureInfo.InvariantCulture); + sizeParameter.Value = value.GetValueOrDefault().ToString(CultureInfo.InvariantCulture); } else { - string sizeString = value.Value.ToString(CultureInfo.InvariantCulture); + string sizeString = value.GetValueOrDefault().ToString(CultureInfo.InvariantCulture); _parameters.Add(new NameValueHeaderValue(SizeString, sizeString)); } } @@ -324,7 +324,7 @@ namespace Microsoft.Net.Http.Headers else { // Must always be quoted - var dateString = HeaderUtilities.FormatDate(date.Value, quoted: true); + var dateString = HeaderUtilities.FormatDate(date.GetValueOrDefault(), quoted: true); if (dateParameter != null) { dateParameter.Value = dateString; diff --git a/src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs b/src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs index 99583cdf47..1149c33865 100644 --- a/src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs @@ -151,9 +151,9 @@ namespace Microsoft.Net.Http.Headers if (HasRange) { - sb.Append(_from.Value.ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(_from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); sb.Append('-'); - sb.Append(_to.Value.ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(_to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); } else { @@ -163,7 +163,7 @@ namespace Microsoft.Net.Http.Headers sb.Append('/'); if (HasLength) { - sb.Append(_length.Value.ToString(NumberFormatInfo.InvariantInfo)); + sb.Append(_length.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); } else { diff --git a/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs index f1ebee276c..b1d6513e2e 100644 --- a/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs @@ -54,7 +54,7 @@ namespace Microsoft.Net.Http.Headers { if (_entityTag == null) { - return HeaderUtilities.FormatDate(_lastModified.Value); + return HeaderUtilities.FormatDate(_lastModified.GetValueOrDefault()); } return _entityTag.ToString(); } @@ -70,7 +70,7 @@ namespace Microsoft.Net.Http.Headers if (_entityTag == null) { - return (other._lastModified != null) && (_lastModified.Value == other._lastModified.Value); + return (other._lastModified != null) && (_lastModified.GetValueOrDefault() == other._lastModified.GetValueOrDefault()); } return _entityTag.Equals(other._entityTag); @@ -80,7 +80,7 @@ namespace Microsoft.Net.Http.Headers { if (_entityTag == null) { - return _lastModified.Value.GetHashCode(); + return _lastModified.GetValueOrDefault().GetHashCode(); } return _entityTag.GetHashCode(); diff --git a/src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs b/src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs index 3b177f6e9a..010f2da1e0 100644 --- a/src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs @@ -20,15 +20,15 @@ namespace Microsoft.Net.Http.Headers { throw new ArgumentException("Invalid header range."); } - if (from.HasValue && (from.Value < 0)) + if (from.HasValue && (from.GetValueOrDefault() < 0)) { throw new ArgumentOutOfRangeException(nameof(from)); } - if (to.HasValue && (to.Value < 0)) + if (to.HasValue && (to.GetValueOrDefault() < 0)) { throw new ArgumentOutOfRangeException(nameof(to)); } - if (from.HasValue && to.HasValue && (from.Value > to.Value)) + if (from.HasValue && to.HasValue && (from.GetValueOrDefault() > to.GetValueOrDefault())) { throw new ArgumentOutOfRangeException(nameof(from)); } @@ -51,38 +51,37 @@ namespace Microsoft.Net.Http.Headers { if (!_from.HasValue) { - return "-" + _to.Value.ToString(NumberFormatInfo.InvariantInfo); + return "-" + _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo); } else if (!_to.HasValue) { - return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-"; + return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-"; } - return _from.Value.ToString(NumberFormatInfo.InvariantInfo) + "-" + - _to.Value.ToString(NumberFormatInfo.InvariantInfo); + return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-" + + _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo); } public override bool Equals(object obj) { - var other = obj as RangeItemHeaderValue; - - if (other == null) + if (obj is RangeItemHeaderValue other) { - return false; + return ((_from == other._from) && (_to == other._to)); } - return ((_from == other._from) && (_to == other._to)); + + return false; } public override int GetHashCode() { if (!_from.HasValue) { - return _to.GetHashCode(); + return _to.GetValueOrDefault().GetHashCode(); } else if (!_to.HasValue) { - return _from.GetHashCode(); + return _from.GetValueOrDefault().GetHashCode(); } - return _from.GetHashCode() ^ _to.GetHashCode(); + return _from.GetValueOrDefault().GetHashCode() ^ _to.GetValueOrDefault().GetHashCode(); } // Returns the length of a range list. E.g. "1-2, 3-4, 5-6" adds 3 ranges to 'rangeCollection'. Note that empty diff --git a/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs index 74b5c6c48c..3070a6e1f9 100644 --- a/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs @@ -105,13 +105,13 @@ namespace Microsoft.Net.Http.Headers if (Expires.HasValue) { - expires = HeaderUtilities.FormatDate(Expires.Value); + expires = HeaderUtilities.FormatDate(Expires.GetValueOrDefault()); length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + expires.Length; } if (MaxAge.HasValue) { - maxAge = HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds); + maxAge = HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.GetValueOrDefault().TotalSeconds); length += SeparatorToken.Length + MaxAgeToken.Length + EqualsToken.Length + maxAge.Length; } @@ -212,12 +212,12 @@ namespace Microsoft.Net.Http.Headers if (Expires.HasValue) { - AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.Value)); + AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.GetValueOrDefault())); } if (MaxAge.HasValue) { - AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.Value.TotalSeconds)); + AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.GetValueOrDefault().TotalSeconds)); } if (Domain != null) diff --git a/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs index deba2d2697..153643b361 100644 --- a/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs @@ -58,7 +58,7 @@ namespace Microsoft.Net.Http.Headers { if (_quality.HasValue) { - return _value + "; q=" + _quality.Value.ToString("0.0##", NumberFormatInfo.InvariantInfo); + return _value + "; q=" + _quality.GetValueOrDefault().ToString("0.0##", NumberFormatInfo.InvariantInfo); } return _value.ToString(); @@ -83,7 +83,7 @@ namespace Microsoft.Net.Http.Headers // Note that we don't consider double.Epsilon here. We really consider two values equal if they're // actually equal. This makes sure that we also get the same hashcode for two values considered equal // by Equals(). - return other._quality.HasValue && (_quality.Value == other._quality.Value); + return other._quality.HasValue && (_quality.GetValueOrDefault() == other._quality.Value); } // If we don't have a quality value, then 'other' must also have no quality assigned in order to be @@ -97,7 +97,7 @@ namespace Microsoft.Net.Http.Headers if (_quality.HasValue) { - result = result ^ _quality.Value.GetHashCode(); + result = result ^ _quality.GetValueOrDefault().GetHashCode(); } return result; From ea1ee2b68c99fa696ae7efcc6cd04ad9ded10768 Mon Sep 17 00:00:00 2001 From: Gert Driesen Date: Wed, 14 Nov 2018 23:32:50 +0100 Subject: [PATCH 38/41] Do not check if key is present before removing item. (#1064) Use Nullable.GetValueOrDefault() instead of Nullable.Value when it is known to have a value. --- .../AuthenticationProperties.cs | 10 +- .../AuthenticationPropertiesTests.cs | 94 +++++++++++++++++++ 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs index 271329209a..ebb3995472 100644 --- a/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs +++ b/src/Microsoft.AspNetCore.Authentication.Abstractions/AuthenticationProperties.cs @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Authentication { Items[key] = value; } - else if (Items.ContainsKey(key)) + else { Items.Remove(key); } @@ -169,9 +169,9 @@ namespace Microsoft.AspNetCore.Authentication { if (value.HasValue) { - Items[key] = value.Value.ToString(); + Items[key] = value.GetValueOrDefault().ToString(); } - else if (Items.ContainsKey(key)) + else { Items.Remove(key); } @@ -201,9 +201,9 @@ namespace Microsoft.AspNetCore.Authentication { if (value.HasValue) { - Items[key] = value.Value.ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture); + Items[key] = value.GetValueOrDefault().ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture); } - else if (Items.ContainsKey(key)) + else { Items.Remove(key); } diff --git a/test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationPropertiesTests.cs b/test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationPropertiesTests.cs index 639c9b558e..84db381ce4 100644 --- a/test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationPropertiesTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationPropertiesTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Xunit; @@ -73,6 +74,10 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test props.SetString("foo", null); Assert.Null(props.GetString("foo")); Assert.Equal(1, props.Items.Count); + + props.SetString("doesntexist", null); + Assert.False(props.Items.ContainsKey("doesntexist")); + Assert.Equal(1, props.Items.Count); } [Fact] @@ -203,5 +208,94 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test props.Items.Clear(); Assert.Null(props.AllowRefresh); } + + [Fact] + public void SetDateTimeOffset() + { + var props = new MyAuthenticationProperties(); + + props.SetDateTimeOffset("foo", new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc))); + Assert.Equal("Mon, 19 Mar 2018 12:34:56 GMT", props.Items["foo"]); + + props.SetDateTimeOffset("foo", null); + Assert.False(props.Items.ContainsKey("foo")); + + props.SetDateTimeOffset("doesnotexist", null); + Assert.False(props.Items.ContainsKey("doesnotexist")); + } + + [Fact] + public void GetDateTimeOffset() + { + var props = new MyAuthenticationProperties(); + var dateTimeOffset = new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)); + + props.Items["foo"] = dateTimeOffset.ToString("r", CultureInfo.InvariantCulture); + Assert.Equal(dateTimeOffset, props.GetDateTimeOffset("foo")); + + props.Items.Remove("foo"); + Assert.Null(props.GetDateTimeOffset("foo")); + + props.Items["foo"] = "BAR"; + Assert.Null(props.GetDateTimeOffset("foo")); + Assert.Equal("BAR", props.Items["foo"]); + } + + [Fact] + public void SetBool() + { + var props = new MyAuthenticationProperties(); + + props.SetBool("foo", true); + Assert.Equal(true.ToString(), props.Items["foo"]); + + props.SetBool("foo", false); + Assert.Equal(false.ToString(), props.Items["foo"]); + + props.SetBool("foo", null); + Assert.False(props.Items.ContainsKey("foo")); + } + + [Fact] + public void GetBool() + { + var props = new MyAuthenticationProperties(); + + props.Items["foo"] = true.ToString(); + Assert.True(props.GetBool("foo")); + + props.Items["foo"] = false.ToString(); + Assert.False(props.GetBool("foo")); + + props.Items["foo"] = null; + Assert.Null(props.GetBool("foo")); + + props.Items["foo"] = "BAR"; + Assert.Null(props.GetBool("foo")); + Assert.Equal("BAR", props.Items["foo"]); + } + + public class MyAuthenticationProperties : AuthenticationProperties + { + public new DateTimeOffset? GetDateTimeOffset(string key) + { + return base.GetDateTimeOffset(key); + } + + public new void SetDateTimeOffset(string key, DateTimeOffset? value) + { + base.SetDateTimeOffset(key, value); + } + + public new void SetBool(string key, bool? value) + { + base.SetBool(key, value); + } + + public new bool? GetBool(string key) + { + return base.GetBool(key); + } + } } } From 49d785c9343d64b93fadfb7eea36c358825cccb0 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Fri, 16 Nov 2018 06:34:44 +0000 Subject: [PATCH 39/41] Use object indirection in HttpContextAccessor (#1066) --- .../HttpContextAccessor.cs | 26 ++++++++++++++----- .../HttpContextFactory.cs | 4 --- .../HttpContextAccessorTests.cs | 11 +------- .../HttpContextFactoryTests.cs | 2 -- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.AspNetCore.Http/HttpContextAccessor.cs b/src/Microsoft.AspNetCore.Http/HttpContextAccessor.cs index 897c27f734..286151029c 100644 --- a/src/Microsoft.AspNetCore.Http/HttpContextAccessor.cs +++ b/src/Microsoft.AspNetCore.Http/HttpContextAccessor.cs @@ -7,21 +7,35 @@ namespace Microsoft.AspNetCore.Http { public class HttpContextAccessor : IHttpContextAccessor { - private static AsyncLocal<(string traceIdentifier, HttpContext context)> _httpContextCurrent = new AsyncLocal<(string traceIdentifier, HttpContext context)>(); + private static AsyncLocal _httpContextCurrent = new AsyncLocal(); public HttpContext HttpContext { get { - var value = _httpContextCurrent.Value; - // Only return the context if the stored request id matches the stored trace identifier - // context.TraceIdentifier is cleared by HttpContextFactory.Dispose. - return value.traceIdentifier == value.context?.TraceIdentifier ? value.context : null; + return _httpContextCurrent.Value?.Context; } set { - _httpContextCurrent.Value = (value?.TraceIdentifier, value); + var holder = _httpContextCurrent.Value; + if (holder != null) + { + // Clear current HttpContext trapped in the AsyncLocals, as its done. + holder.Context = null; + } + + if (value != null) + { + // Use an object indirection to hold the HttpContext in the AsyncLocal, + // so it can be cleared in all ExecutionContexts when its cleared. + _httpContextCurrent.Value = new HttpContextHolder { Context = value }; + } } } + + private class HttpContextHolder + { + public HttpContext Context; + } } } diff --git a/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs b/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs index f293ef4782..8236a388a5 100644 --- a/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs +++ b/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs @@ -53,10 +53,6 @@ namespace Microsoft.AspNetCore.Http { _httpContextAccessor.HttpContext = null; } - - // Null out the TraceIdentifier here as a sign that this request is done, - // the HttpContextAccessor implementation relies on this to detect that the request is over - httpContext.TraceIdentifier = null; } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Http.Tests/HttpContextAccessorTests.cs b/test/Microsoft.AspNetCore.Http.Tests/HttpContextAccessorTests.cs index c1521b1bc3..c224a66a7d 100644 --- a/test/Microsoft.AspNetCore.Http.Tests/HttpContextAccessorTests.cs +++ b/test/Microsoft.AspNetCore.Http.Tests/HttpContextAccessorTests.cs @@ -44,7 +44,6 @@ namespace Microsoft.AspNetCore.Http var accessor = new HttpContextAccessor(); var context = new DefaultHttpContext(); - context.TraceIdentifier = "1"; accessor.HttpContext = context; var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -76,7 +75,6 @@ namespace Microsoft.AspNetCore.Http // Null out the accessor accessor.HttpContext = null; - context.TraceIdentifier = null; waitForNullTcs.SetResult(null); @@ -86,12 +84,11 @@ namespace Microsoft.AspNetCore.Http } [Fact] - public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfDifferentTraceIdentifier() + public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfChanged() { var accessor = new HttpContextAccessor(); var context = new DefaultHttpContext(); - context.TraceIdentifier = "1"; accessor.HttpContext = context; var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -121,12 +118,8 @@ namespace Microsoft.AspNetCore.Http await checkAsyncFlowTcs.Task; - // Reset the trace identifier on the first request - context.TraceIdentifier = null; - // Set a new http context var context2 = new DefaultHttpContext(); - context2.TraceIdentifier = "2"; accessor.HttpContext = context2; waitForNullTcs.SetResult(null); @@ -142,7 +135,6 @@ namespace Microsoft.AspNetCore.Http var accessor = new HttpContextAccessor(); var context = new DefaultHttpContext(); - context.TraceIdentifier = "1"; accessor.HttpContext = context; var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -172,7 +164,6 @@ namespace Microsoft.AspNetCore.Http var accessor = new HttpContextAccessor(); var context = new DefaultHttpContext(); - context.TraceIdentifier = "1"; accessor.HttpContext = context; var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/test/Microsoft.AspNetCore.Http.Tests/HttpContextFactoryTests.cs b/test/Microsoft.AspNetCore.Http.Tests/HttpContextFactoryTests.cs index 80e421273a..56b996f5be 100644 --- a/test/Microsoft.AspNetCore.Http.Tests/HttpContextFactoryTests.cs +++ b/test/Microsoft.AspNetCore.Http.Tests/HttpContextFactoryTests.cs @@ -34,7 +34,6 @@ namespace Microsoft.AspNetCore.Http // Act var context = contextFactory.Create(new FeatureCollection()); - var traceIdentifier = context.TraceIdentifier; // Assert Assert.Same(context, accessor.HttpContext); @@ -42,7 +41,6 @@ namespace Microsoft.AspNetCore.Http contextFactory.Dispose(context); Assert.Null(accessor.HttpContext); - Assert.NotEqual(traceIdentifier, context.TraceIdentifier); } [Fact] From 962ec07bdbb090ac2e9cd2a0e68e24c6c276a972 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Fri, 16 Nov 2018 19:18:47 -0800 Subject: [PATCH 40/41] Adds PipeWriterAdapter (#1065) --- .gitignore | 1 + .../StreamPipeWriterBenchmark.cs | 89 ++++ build/dependencies.props | 1 + .../Microsoft.AspNetCore.Http.csproj | 3 +- .../StreamPipeWriter.cs | 320 +++++++++++++++ .../FlushResultCancellationTests.cs | 68 ++++ .../Microsoft.AspNetCore.Http.Tests.csproj | 3 +- .../PipeTest.cs | 43 ++ .../PipeWriterTests.cs | 221 ++++++++++ .../StreamPipeWriterTests.cs | 380 ++++++++++++++++++ .../TestMemoryPool.cs | 139 +++++++ 11 files changed, 1266 insertions(+), 2 deletions(-) create mode 100644 benchmarks/Microsoft.AspNetCore.Http.Performance/StreamPipeWriterBenchmark.cs create mode 100644 src/Microsoft.AspNetCore.Http/StreamPipeWriter.cs create mode 100644 test/Microsoft.AspNetCore.Http.Tests/FlushResultCancellationTests.cs create mode 100644 test/Microsoft.AspNetCore.Http.Tests/PipeTest.cs create mode 100644 test/Microsoft.AspNetCore.Http.Tests/PipeWriterTests.cs create mode 100644 test/Microsoft.AspNetCore.Http.Tests/StreamPipeWriterTests.cs create mode 100644 test/Microsoft.AspNetCore.Http.Tests/TestMemoryPool.cs diff --git a/.gitignore b/.gitignore index d5717b3f3f..3d7e16e84a 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ project.lock.json /.vs/ .vscode/ global.json +BenchmarkDotNet.Artifacts/ diff --git a/benchmarks/Microsoft.AspNetCore.Http.Performance/StreamPipeWriterBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Http.Performance/StreamPipeWriterBenchmark.cs new file mode 100644 index 0000000000..705cb0d8af --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Http.Performance/StreamPipeWriterBenchmark.cs @@ -0,0 +1,89 @@ +// 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.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Http +{ + public class StreamPipeWriterBenchmark + { + private Stream _memoryStream; + private StreamPipeWriter _pipeWriter; + private static byte[] _helloWorldBytes = Encoding.ASCII.GetBytes("Hello World"); + private static byte[] _largeWrite = Encoding.ASCII.GetBytes(new string('a', 50000)); + + [IterationSetup] + public void Setup() + { + _memoryStream = new NoopStream(); + _pipeWriter = new StreamPipeWriter(_memoryStream); + } + + [Benchmark] + public async Task WriteHelloWorld() + { + await _pipeWriter.WriteAsync(_helloWorldBytes); + } + + [Benchmark] + public async Task WriteHelloWorldLargeWrite() + { + await _pipeWriter.WriteAsync(_largeWrite); + } + + public class NoopStream : Stream + { + public override bool CanRead => false; + + public override bool CanSeek => throw new System.NotImplementedException(); + + public override bool CanWrite => true; + + public override long Length => throw new System.NotImplementedException(); + + public override long Position { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new System.NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new System.NotImplementedException(); + } + + public override void SetLength(long value) + { + } + + public override void Write(byte[] buffer, int offset, int count) + { + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default(CancellationToken)) + { + return default(ValueTask); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } + } +} diff --git a/build/dependencies.props b/build/dependencies.props index 2aa50e3e13..c3991cb407 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -24,6 +24,7 @@ 4.9.0 2.0.3 4.6.0-preview1-26907-04 + 4.6.0-preview1-26907-04 4.6.0-preview1-26907-04 0.10.0 2.3.1 diff --git a/src/Microsoft.AspNetCore.Http/Microsoft.AspNetCore.Http.csproj b/src/Microsoft.AspNetCore.Http/Microsoft.AspNetCore.Http.csproj index 162315a7a6..94080281b3 100644 --- a/src/Microsoft.AspNetCore.Http/Microsoft.AspNetCore.Http.csproj +++ b/src/Microsoft.AspNetCore.Http/Microsoft.AspNetCore.Http.csproj @@ -2,7 +2,7 @@ ASP.NET Core default HTTP feature implementations. - netstandard2.0 + netstandard2.0;netcoreapp2.2 $(NoWarn);CS1591 true true @@ -19,6 +19,7 @@ + diff --git a/src/Microsoft.AspNetCore.Http/StreamPipeWriter.cs b/src/Microsoft.AspNetCore.Http/StreamPipeWriter.cs new file mode 100644 index 0000000000..f232aa97cf --- /dev/null +++ b/src/Microsoft.AspNetCore.Http/StreamPipeWriter.cs @@ -0,0 +1,320 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Implements PipeWriter using a underlying stream. + /// + public class StreamPipeWriter : PipeWriter, IDisposable + { + private readonly int _minimumSegmentSize; + private readonly Stream _writingStream; + private int _bytesWritten; + + private List _completedSegments; + private Memory _currentSegment; + private IMemoryOwner _currentSegmentOwner; + private MemoryPool _pool; + private int _position; + + private CancellationTokenSource _internalTokenSource; + private bool _isCompleted; + private ExceptionDispatchInfo _exceptionInfo; + private object _lockObject = new object(); + + private CancellationTokenSource InternalTokenSource + { + get + { + lock (_lockObject) + { + if (_internalTokenSource == null) + { + _internalTokenSource = new CancellationTokenSource(); + } + return _internalTokenSource; + } + } + } + + /// + /// Creates a new StreamPipeWrapper + /// + /// The stream to write to + public StreamPipeWriter(Stream writingStream) : this(writingStream, 4096) + { + } + + public StreamPipeWriter(Stream writingStream, int minimumSegmentSize, MemoryPool pool = null) + { + _minimumSegmentSize = minimumSegmentSize; + _writingStream = writingStream; + _pool = pool ?? MemoryPool.Shared; + } + + /// + public override void Advance(int count) + { + if (_currentSegment.IsEmpty) // TODO confirm this + { + throw new InvalidOperationException("No writing operation. Make sure GetMemory() was called."); + } + + if (count >= 0) + { + if (_currentSegment.Length < _position + count) + { + throw new InvalidOperationException("Can't advance past buffer size."); + } + _bytesWritten += count; + _position += count; + } + } + + /// + public override Memory GetMemory(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + + return _currentSegment; + } + + /// + public override Span GetSpan(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + + return _currentSegment.Span.Slice(_position); + } + + /// + public override void CancelPendingFlush() + { + Cancel(); + } + + /// + public override void Complete(Exception exception = null) + { + if (_isCompleted) + { + return; + } + + _isCompleted = true; + if (exception != null) + { + _exceptionInfo = ExceptionDispatchInfo.Capture(exception); + } + + _internalTokenSource?.Dispose(); + + if (_completedSegments != null) + { + foreach (var segment in _completedSegments) + { + segment.Return(); + } + } + + _currentSegmentOwner?.Dispose(); + } + + /// + public override void OnReaderCompleted(Action callback, object state) + { + throw new NotSupportedException("OnReaderCompleted isn't supported in StreamPipeWrapper."); + } + + /// + public override ValueTask FlushAsync(CancellationToken cancellationToken = default) + { + if (_bytesWritten == 0) + { + return new ValueTask(new FlushResult(isCanceled: false, IsCompletedOrThrow())); + } + + return FlushAsyncInternal(cancellationToken); + } + + private void Cancel() + { + InternalTokenSource.Cancel(); + } + + private async ValueTask FlushAsyncInternal(CancellationToken cancellationToken = default) + { + // Write all completed segments and whatever remains in the current segment + // and flush the result. + CancellationTokenRegistration reg = new CancellationTokenRegistration(); + if (cancellationToken.CanBeCanceled) + { + reg = cancellationToken.Register(state => ((StreamPipeWriter)state).Cancel(), this); + } + using (reg) + { + var localToken = InternalTokenSource.Token; + try + { + if (_completedSegments != null && _completedSegments.Count > 0) + { + var count = _completedSegments.Count; + for (var i = 0; i < count; i++) + { + var segment = _completedSegments[0]; +#if NETCOREAPP2_2 + await _writingStream.WriteAsync(segment.Buffer.Slice(0, segment.Length), localToken); +#elif NETSTANDARD2_0 + MemoryMarshal.TryGetArray(segment.Buffer, out var arraySegment); + await _writingStream.WriteAsync(arraySegment.Array, 0, segment.Length, localToken); +#else +#error Target frameworks need to be updated. +#endif + _bytesWritten -= segment.Length; + segment.Return(); + _completedSegments.RemoveAt(0); + } + } + + if (!_currentSegment.IsEmpty) + { +#if NETCOREAPP2_2 + await _writingStream.WriteAsync(_currentSegment.Slice(0, _position), localToken); +#elif NETSTANDARD2_0 + MemoryMarshal.TryGetArray(_currentSegment, out var arraySegment); + await _writingStream.WriteAsync(arraySegment.Array, 0, _position, localToken); +#else +#error Target frameworks need to be updated. +#endif + _bytesWritten -= _position; + _position = 0; + } + + await _writingStream.FlushAsync(localToken); + + return new FlushResult(isCanceled: false, IsCompletedOrThrow()); + } + catch (OperationCanceledException) + { + // Remove the cancellation token such that the next time Flush is called + // A new CTS is created. + lock (_lockObject) + { + _internalTokenSource = null; + } + + if (cancellationToken.IsCancellationRequested) + { + throw; + } + + // Catch any cancellation and translate it into setting isCanceled = true + return new FlushResult(isCanceled: true, IsCompletedOrThrow()); + } + } + } + + private void EnsureCapacity(int sizeHint) + { + // This does the Right Thing. It only subtracts _position from the current segment length if it's non-null. + // If _currentSegment is null, it returns 0. + var remainingSize = _currentSegment.Length - _position; + + // If the sizeHint is 0, any capacity will do + // Otherwise, the buffer must have enough space for the entire size hint, or we need to add a segment. + if ((sizeHint == 0 && remainingSize > 0) || (sizeHint > 0 && remainingSize >= sizeHint)) + { + // We have capacity in the current segment + return; + } + + AddSegment(sizeHint); + } + + private void AddSegment(int sizeHint = 0) + { + if (_currentSegment.Length != 0) + { + // We're adding a segment to the list + if (_completedSegments == null) + { + _completedSegments = new List(); + } + + // Position might be less than the segment length if there wasn't enough space to satisfy the sizeHint when + // GetMemory was called. In that case we'll take the current segment and call it "completed", but need to + // ignore any empty space in it. + _completedSegments.Add(new CompletedBuffer(_currentSegmentOwner, _position)); + } + + // Get a new buffer using the minimum segment size, unless the size hint is larger than a single segment. + _currentSegmentOwner = _pool.Rent(Math.Max(_minimumSegmentSize, sizeHint)); + _currentSegment = _currentSegmentOwner.Memory; + _position = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsCompletedOrThrow() + { + if (!_isCompleted) + { + return false; + } + + if (_exceptionInfo != null) + { + ThrowLatchedException(); + } + + return true; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ThrowLatchedException() + { + _exceptionInfo.Throw(); + } + + public void Dispose() + { + Complete(); + } + + /// + /// Holds a byte[] from the pool and a size value. Basically a Memory but guaranteed to be backed by an ArrayPool byte[], so that we know we can return it. + /// + private readonly struct CompletedBuffer + { + public Memory Buffer { get; } + public int Length { get; } + + public ReadOnlySpan Span => Buffer.Span; + + private readonly IMemoryOwner _memoryOwner; + + public CompletedBuffer(IMemoryOwner buffer, int length) + { + Buffer = buffer.Memory; + Length = length; + _memoryOwner = buffer; + } + + public void Return() + { + _memoryOwner.Dispose(); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Http.Tests/FlushResultCancellationTests.cs b/test/Microsoft.AspNetCore.Http.Tests/FlushResultCancellationTests.cs new file mode 100644 index 0000000000..f4ab7cb96f --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Tests/FlushResultCancellationTests.cs @@ -0,0 +1,68 @@ +// 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.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class FlushResultCancellationTests : PipeTest + { + [Fact] + public void FlushAsyncCancellationDeadlock() + { + var cts = new CancellationTokenSource(); + var cts2 = new CancellationTokenSource(); + + PipeWriter buffer = Writer.WriteEmpty(MaximumSizeHigh); + + var e = new ManualResetEventSlim(); + + ValueTaskAwaiter awaiter = buffer.FlushAsync(cts.Token).GetAwaiter(); + awaiter.OnCompleted( + () => { + // We are on cancellation thread and need to wait until another FlushAsync call + // takes pipe state lock + e.Wait(); + + // Make sure we had enough time to reach _cancellationTokenRegistration.Dispose + Thread.Sleep(100); + + // Try to take pipe state lock + buffer.FlushAsync(); + }); + + // Start a thread that would run cancellation callbacks + Task cancellationTask = Task.Run(() => cts.Cancel()); + // Start a thread that would call FlushAsync with different token + // and block on _cancellationTokenRegistration.Dispose + Task blockingTask = Task.Run( + () => { + e.Set(); + buffer.FlushAsync(cts2.Token); + }); + + bool completed = Task.WhenAll(cancellationTask, blockingTask).Wait(TimeSpan.FromSeconds(10)); + Assert.True(completed); + } + + [Fact] + public async Task FlushAsyncWithNewCancellationTokenNotAffectedByPrevious() + { + var cancellationTokenSource1 = new CancellationTokenSource(); + PipeWriter buffer = Writer.WriteEmpty(10); + await buffer.FlushAsync(cancellationTokenSource1.Token); + + cancellationTokenSource1.Cancel(); + + var cancellationTokenSource2 = new CancellationTokenSource(); + buffer = Writer.WriteEmpty(10); + + await buffer.FlushAsync(cancellationTokenSource2.Token); + } + } +} diff --git a/test/Microsoft.AspNetCore.Http.Tests/Microsoft.AspNetCore.Http.Tests.csproj b/test/Microsoft.AspNetCore.Http.Tests/Microsoft.AspNetCore.Http.Tests.csproj index aa428320cd..a8ee8f19fc 100644 --- a/test/Microsoft.AspNetCore.Http.Tests/Microsoft.AspNetCore.Http.Tests.csproj +++ b/test/Microsoft.AspNetCore.Http.Tests/Microsoft.AspNetCore.Http.Tests.csproj @@ -2,8 +2,9 @@ $(StandardTestTfms) + true - + diff --git a/test/Microsoft.AspNetCore.Http.Tests/PipeTest.cs b/test/Microsoft.AspNetCore.Http.Tests/PipeTest.cs new file mode 100644 index 0000000000..2e94e3a267 --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Tests/PipeTest.cs @@ -0,0 +1,43 @@ +// 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.IO; +using System.IO.Pipelines; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public abstract class PipeTest : IDisposable + { + protected const int MaximumSizeHigh = 65; + + public MemoryStream MemoryStream { get; set; } + + public PipeWriter Writer { get; set; } + + protected PipeTest() + { + MemoryStream = new MemoryStream(); + Writer = new StreamPipeWriter(MemoryStream, 4096, new TestMemoryPool()); + } + + public void Dispose() + { + Writer.Complete(); + } + + public byte[] Read() + { + Writer.FlushAsync().GetAwaiter().GetResult(); + return ReadWithoutFlush(); + } + + public byte[] ReadWithoutFlush() + { + MemoryStream.Position = 0; + var buffer = new byte[MemoryStream.Length]; + var result = MemoryStream.Read(buffer, 0, (int)MemoryStream.Length); + return buffer; + } + } +} diff --git a/test/Microsoft.AspNetCore.Http.Tests/PipeWriterTests.cs b/test/Microsoft.AspNetCore.Http.Tests/PipeWriterTests.cs new file mode 100644 index 0000000000..0cc6dc012f --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Tests/PipeWriterTests.cs @@ -0,0 +1,221 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class PipeWriterTests : PipeTest + { + + [Theory] + [InlineData(3, -1, 0)] + [InlineData(3, 0, -1)] + [InlineData(3, 0, 4)] + [InlineData(3, 4, 0)] + [InlineData(3, -1, -1)] + [InlineData(3, 4, 4)] + public void ThrowsForInvalidParameters(int arrayLength, int offset, int length) + { + var array = new byte[arrayLength]; + for (var i = 0; i < array.Length; i++) + { + array[i] = (byte)(i + 1); + } + + Writer.Write(new Span(array, 0, 0)); + Writer.Write(new Span(array, array.Length, 0)); + + try + { + Writer.Write(new Span(array, offset, length)); + Assert.True(false); + } + catch (Exception ex) + { + Assert.True(ex is ArgumentOutOfRangeException); + } + + Writer.Write(new Span(array, 0, array.Length)); + Assert.Equal(array, Read()); + } + + [Theory] + [InlineData(0, 3)] + [InlineData(1, 2)] + [InlineData(2, 1)] + [InlineData(1, 1)] + public void CanWriteWithOffsetAndLength(int offset, int length) + { + var array = new byte[] { 1, 2, 3 }; + + Writer.Write(new Span(array, offset, length)); + + Assert.Equal(array.Skip(offset).Take(length).ToArray(), Read()); + } + + [Fact] + public void CanWriteIntoHeadlessBuffer() + { + + Writer.Write(new byte[] { 1, 2, 3 }); + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Fact] + public void CanGetNewMemoryWhenSizeTooLarge() + { + var memory = Writer.GetMemory(0); + + var memoryLarge = Writer.GetMemory(10000); + + Assert.NotEqual(memory, memoryLarge); + } + + [Fact] + public void CanGetSameMemoryWhenNoAdvance() + { + var memory = Writer.GetMemory(0); + + var secondMemory = Writer.GetMemory(0); + + Assert.Equal(memory, secondMemory); + } + + [Fact] + public void CanGetNewSpanWhenNoAdvanceWhenSizeTooLarge() + { + var span = Writer.GetSpan(0); + + var secondSpan = Writer.GetSpan(8000); + + Assert.False(span.SequenceEqual(secondSpan)); + } + + [Fact] + public void CanGetSameSpanWhenNoAdvance() + { + var span = Writer.GetSpan(0); + + var secondSpan = Writer.GetSpan(0); + + Assert.True(span.SequenceEqual(secondSpan)); + } + + [Theory] + [InlineData(16, 32, 32)] + [InlineData(16, 16, 16)] + [InlineData(64, 32, 64)] + [InlineData(40, 32, 64)] // memory sizes are powers of 2. + public void CheckMinimumSegmentSizeWithGetMemory(int minimumSegmentSize, int getMemorySize, int expectedSize) + { + var writer = new StreamPipeWriter(new MemoryStream(), minimumSegmentSize); + var memory = writer.GetMemory(getMemorySize); + + Assert.Equal(expectedSize, memory.Length); + } + + [Fact] + public void CanWriteMultipleTimes() + { + + Writer.Write(new byte[] { 1 }); + Writer.Write(new byte[] { 2 }); + Writer.Write(new byte[] { 3 }); + + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Fact] + public void CanWriteOverTheBlockLength() + { + Memory memory = Writer.GetMemory(); + + IEnumerable source = Enumerable.Range(0, memory.Length).Select(i => (byte)i); + byte[] expectedBytes = source.Concat(source).Concat(source).ToArray(); + + Writer.Write(expectedBytes); + + Assert.Equal(expectedBytes, Read()); + } + + [Fact] + public void EnsureAllocatesSpan() + { + var span = Writer.GetSpan(10); + + Assert.True(span.Length >= 10); + // 0 byte Flush would not complete the reader so we complete. + Writer.Complete(); + Assert.Equal(new byte[] { }, Read()); + } + + [Fact] + public void SlicesSpanAndAdvancesAfterWrite() + { + int initialLength = Writer.GetSpan(3).Length; + + + Writer.Write(new byte[] { 1, 2, 3 }); + Span span = Writer.GetSpan(); + + Assert.Equal(initialLength - 3, span.Length); + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Theory] + [InlineData(5)] + [InlineData(50)] + [InlineData(500)] + [InlineData(5000)] + [InlineData(50000)] + public async Task WriteLargeDataBinary(int length) + { + var data = new byte[length]; + new Random(length).NextBytes(data); + PipeWriter output = Writer; + output.Write(data); + await output.FlushAsync(); + + var input = Read(); + Assert.Equal(data, input.ToArray()); + } + + [Fact] + public async Task CanWriteNothingToBuffer() + { + Writer.GetMemory(0); + Writer.Advance(0); // doing nothing, the hard way + await Writer.FlushAsync(); + } + + [Fact] + public void EmptyWriteDoesNotThrow() + { + Writer.Write(new byte[0]); + } + + [Fact] + public void ThrowsOnAdvanceOverMemorySize() + { + Memory buffer = Writer.GetMemory(1); + var exception = Assert.Throws(() => Writer.Advance(buffer.Length + 1)); + Assert.Equal("Can't advance past buffer size.", exception.Message); + } + + [Fact] + public void ThrowsOnAdvanceWithNoMemory() + { + PipeWriter buffer = Writer; + var exception = Assert.Throws(() => buffer.Advance(1)); + Assert.Equal("No writing operation. Make sure GetMemory() was called.", exception.Message); + } + } +} diff --git a/test/Microsoft.AspNetCore.Http.Tests/StreamPipeWriterTests.cs b/test/Microsoft.AspNetCore.Http.Tests/StreamPipeWriterTests.cs new file mode 100644 index 0000000000..76d3b34fae --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Tests/StreamPipeWriterTests.cs @@ -0,0 +1,380 @@ +// 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.Buffers; +using System.IO; +using System.IO.Pipelines; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class StreamPipeWriterTests : PipeTest + { + [Fact] + public async Task CanWriteAsyncMultipleTimesIntoSameBlock() + { + + await Writer.WriteAsync(new byte[] { 1 }); + await Writer.WriteAsync(new byte[] { 2 }); + await Writer.WriteAsync(new byte[] { 3 }); + + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Theory] + [InlineData(100, 1000)] + [InlineData(100, 8000)] + [InlineData(100, 10000)] + [InlineData(8000, 100)] + [InlineData(8000, 8000)] + public async Task CanAdvanceWithPartialConsumptionOfFirstSegment(int firstWriteLength, int secondWriteLength) + { + await Writer.WriteAsync(Encoding.ASCII.GetBytes("a")); + + var expectedLength = firstWriteLength + secondWriteLength + 1; + + var memory = Writer.GetMemory(firstWriteLength); + Writer.Advance(firstWriteLength); + + memory = Writer.GetMemory(secondWriteLength); + Writer.Advance(secondWriteLength); + + await Writer.FlushAsync(); + + Assert.Equal(expectedLength, Read().Length); + } + + [Fact] + public async Task ThrowsOnCompleteAndWrite() + { + Writer.Complete(new InvalidOperationException("Whoops")); + var exception = await Assert.ThrowsAsync(async () => await Writer.FlushAsync()); + + Assert.Equal("Whoops", exception.Message); + } + + [Fact] + public async Task WriteCanBeCancelledViaProvidedCancellationToken() + { + var pipeWriter = new StreamPipeWriter(new HangingStream()); + var cts = new CancellationTokenSource(1); + await Assert.ThrowsAsync(async () => await pipeWriter.WriteAsync(Encoding.ASCII.GetBytes("data"), cts.Token)); + } + + [Fact] + public async Task WriteCanBeCanceledViaCancelPendingFlushWhenFlushIsAsync() + { + var pipeWriter = new StreamPipeWriter(new HangingStream()); + FlushResult flushResult = new FlushResult(); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var task = Task.Run(async () => + { + try + { + var writingTask = pipeWriter.WriteAsync(Encoding.ASCII.GetBytes("data")); + tcs.SetResult(0); + flushResult = await writingTask; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw ex; + } + }); + + await tcs.Task; + + pipeWriter.CancelPendingFlush(); + + await task; + + Assert.True(flushResult.IsCanceled); + } + + [Fact] + public void FlushAsyncCompletedAfterPreCancellation() + { + PipeWriter writableBuffer = Writer.WriteEmpty(1); + + Writer.CancelPendingFlush(); + + ValueTask flushAsync = writableBuffer.FlushAsync(); + + Assert.True(flushAsync.IsCompleted); + + FlushResult flushResult = flushAsync.GetAwaiter().GetResult(); + + Assert.True(flushResult.IsCanceled); + + flushAsync = writableBuffer.FlushAsync(); + + Assert.True(flushAsync.IsCompleted); + } + + [Fact] + public void FlushAsyncReturnsCanceledIfCanceledBeforeFlush() + { + CheckCanceledFlush(); + } + + [Fact] + public void FlushAsyncReturnsCanceledIfCanceledBeforeFlushMultipleTimes() + { + for (var i = 0; i < 10; i++) + { + CheckCanceledFlush(); + } + } + + [Fact] + public async Task FlushAsyncReturnsCanceledInterleaved() + { + for (var i = 0; i < 5; i++) + { + CheckCanceledFlush(); + await CheckWriteIsNotCanceled(); + } + } + + [Fact] + public async Task CancelPendingFlushBetweenWritesAllDataIsPreserved() + { + MemoryStream = new SingleWriteStream(); + Writer = new StreamPipeWriter(MemoryStream); + FlushResult flushResult = new FlushResult(); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var task = Task.Run(async () => + { + try + { + await Writer.WriteAsync(Encoding.ASCII.GetBytes("data")); + + var writingTask = Writer.WriteAsync(Encoding.ASCII.GetBytes(" data")); + tcs.SetResult(0); + flushResult = await writingTask; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw ex; + } + }); + + await tcs.Task; + + Writer.CancelPendingFlush(); + + await task; + + Assert.True(flushResult.IsCanceled); + + await Writer.WriteAsync(Encoding.ASCII.GetBytes(" more data")); + Assert.Equal(Encoding.ASCII.GetBytes("data data more data"), Read()); + } + + [Fact] + public async Task CancelPendingFlushAfterAllWritesAllDataIsPreserved() + { + MemoryStream = new CannotFlushStream(); + Writer = new StreamPipeWriter(MemoryStream); + FlushResult flushResult = new FlushResult(); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var task = Task.Run(async () => + { + try + { + // Create two Segments + // First one will succeed to write, other one will hang. + var writingTask = Writer.WriteAsync(Encoding.ASCII.GetBytes("data")); + tcs.SetResult(0); + flushResult = await writingTask; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw ex; + } + }); + + await tcs.Task; + + Writer.CancelPendingFlush(); + + await task; + + Assert.True(flushResult.IsCanceled); + } + + [Fact] + public async Task CancelPendingFlushLostOfCancellationsNoDataLost() + { + var writeSize = 16; + var singleWriteStream = new SingleWriteStream(); + MemoryStream = singleWriteStream; + Writer = new StreamPipeWriter(MemoryStream, minimumSegmentSize: writeSize); + + for (var i = 0; i < 10; i++) + { + FlushResult flushResult = new FlushResult(); + var expectedData = Encoding.ASCII.GetBytes(new string('a', writeSize)); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + // TaskCreationOptions.RunAsync + + var task = Task.Run(async () => + { + try + { + // Create two Segments + // First one will succeed to write, other one will hang. + for (var j = 0; j < 2; j++) + { + Writer.Write(expectedData); + } + + var flushTask = Writer.FlushAsync(); + tcs.SetResult(0); + flushResult = await flushTask; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw ex; + } + }); + + await tcs.Task; + + Writer.CancelPendingFlush(); + + await task; + + Assert.True(flushResult.IsCanceled); + } + + // Only half of the data was written because every other flush failed. + Assert.Equal(16 * 10, ReadWithoutFlush().Length); + + // Start allowing all writes to make read succeed. + singleWriteStream.AllowAllWrites = true; + + Assert.Equal(16 * 10 * 2, Read().Length); + } + + private async Task CheckWriteIsNotCanceled() + { + var flushResult = await Writer.WriteAsync(Encoding.ASCII.GetBytes("data")); + Assert.False(flushResult.IsCanceled); + } + + private void CheckCanceledFlush() + { + PipeWriter writableBuffer = Writer.WriteEmpty(MaximumSizeHigh); + + Writer.CancelPendingFlush(); + + ValueTask flushAsync = writableBuffer.FlushAsync(); + + Assert.True(flushAsync.IsCompleted); + FlushResult flushResult = flushAsync.GetAwaiter().GetResult(); + Assert.True(flushResult.IsCanceled); + } + } + + internal class HangingStream : MemoryStream + { + + public HangingStream() + { + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await Task.Delay(30000, cancellationToken); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + await Task.Delay(30000, cancellationToken); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await Task.Delay(30000, cancellationToken); + return 0; + } + } + + internal class SingleWriteStream : MemoryStream + { + private bool _shouldNextWriteFail; + + public bool AllowAllWrites { get; set; } + + +#if NETCOREAPP2_2 + public override async ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + { + try + { + if (_shouldNextWriteFail && !AllowAllWrites) + { + await Task.Delay(30000, cancellationToken); + } + else + { + await base.WriteAsync(source, cancellationToken); + } + } + finally + { + _shouldNextWriteFail = !_shouldNextWriteFail; + } + } +#endif + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + try + { + if (_shouldNextWriteFail && !AllowAllWrites) + { + await Task.Delay(30000, cancellationToken); + } + await base.WriteAsync(buffer, offset, count, cancellationToken); + } + finally + { + _shouldNextWriteFail = !_shouldNextWriteFail; + } + } + } + + internal class CannotFlushStream : MemoryStream + { + public override async Task FlushAsync(CancellationToken cancellationToken) + { + await Task.Delay(30000, cancellationToken); + } + } + + internal static class TestWriterExtensions + { + public static PipeWriter WriteEmpty(this PipeWriter Writer, int count) + { + Writer.GetSpan(count).Slice(0, count).Fill(0); + Writer.Advance(count); + return Writer; + } + } +} diff --git a/test/Microsoft.AspNetCore.Http.Tests/TestMemoryPool.cs b/test/Microsoft.AspNetCore.Http.Tests/TestMemoryPool.cs new file mode 100644 index 0000000000..c5dd647dd1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Tests/TestMemoryPool.cs @@ -0,0 +1,139 @@ +// 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.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class TestMemoryPool : MemoryPool + { + private MemoryPool _pool = Shared; + + private bool _disposed; + + public override IMemoryOwner Rent(int minBufferSize = -1) + { + CheckDisposed(); + return new PooledMemory(_pool.Rent(minBufferSize), this); + } + + protected override void Dispose(bool disposing) + { + _disposed = true; + } + + public override int MaxBufferSize => 4096; + + internal void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(TestMemoryPool)); + } + } + + private class PooledMemory : MemoryManager + { + private IMemoryOwner _owner; + + private readonly TestMemoryPool _pool; + + private int _referenceCount; + + private bool _returned; + + private string _leaser; + + public PooledMemory(IMemoryOwner owner, TestMemoryPool pool) + { + _owner = owner; + _pool = pool; + _leaser = Environment.StackTrace; + _referenceCount = 1; + } + + ~PooledMemory() + { + Debug.Assert(_returned, "Block being garbage collected instead of returned to pool" + Environment.NewLine + _leaser); + } + + protected override void Dispose(bool disposing) + { + _pool.CheckDisposed(); + } + + public override MemoryHandle Pin(int elementIndex = 0) + { + _pool.CheckDisposed(); + Interlocked.Increment(ref _referenceCount); + + if (!MemoryMarshal.TryGetArray(_owner.Memory, out ArraySegment segment)) + { + throw new InvalidOperationException(); + } + + unsafe + { + try + { + if ((uint)elementIndex > (uint)segment.Count) + { + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + } + + GCHandle handle = GCHandle.Alloc(segment.Array, GCHandleType.Pinned); + + return new MemoryHandle(Unsafe.Add(((void*)handle.AddrOfPinnedObject()), elementIndex + segment.Offset), handle, this); + } + catch + { + Unpin(); + throw; + } + } + } + + public override void Unpin() + { + _pool.CheckDisposed(); + + int newRefCount = Interlocked.Decrement(ref _referenceCount); + + if (newRefCount < 0) + throw new InvalidOperationException(); + + if (newRefCount == 0) + { + _returned = true; + } + } + + protected override bool TryGetArray(out ArraySegment segment) + { + _pool.CheckDisposed(); + return MemoryMarshal.TryGetArray(_owner.Memory, out segment); + } + + public override Memory Memory + { + get + { + _pool.CheckDisposed(); + return _owner.Memory; + } + } + + public override Span GetSpan() + { + _pool.CheckDisposed(); + return _owner.Memory.Span; + } + } + } +} \ No newline at end of file From bc7092a32b1943c7f17439e419d3f66cd94ce9bd Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Mon, 19 Nov 2018 22:29:01 -0800 Subject: [PATCH 41/41] Update README.md --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a66e3a23e8..1df006fdc8 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,13 @@ -HttpAbstractions -================ +HttpAbstractions [Archived] +=========================== -| AppVeyor | Travis | -| ---- | ---- -| [![AppVeyor](https://ci.appveyor.com/api/projects/status/8civi9t457oc7rf8/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/HttpAbstractions/branch/dev) | [![Travis](https://travis-ci.org/aspnet/HttpAbstractions.svg?branch=dev)](https://travis-ci.org/aspnet/HttpAbstractions) | +**This GitHub project has been archived.** Ongoing development on this project can be found in https://github.com/aspnet/AspNetCore. Contains HTTP abstractions for ASP.NET Core such as `HttpContext`, `HttpRequest`, `HttpResponse` and `RequestDelegate`. It also contains `IApplicationBuilder` and extensions to create and compose your application's pipeline. -This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo. +This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [AspNetCore](https://github.com/aspnet/AspNetCore) repo.