diff --git a/NuGet.config b/NuGet.config index 50e58d647089..973492be36fe 100644 --- a/NuGet.config +++ b/NuGet.config @@ -5,6 +5,7 @@ + diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 663aca404a22..0413b558a486 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -40,6 +40,8 @@ and are generated based on the last package release. + + diff --git a/eng/SharedFramework.External.props b/eng/SharedFramework.External.props index 2f7d2bdc3786..175c7848407a 100644 --- a/eng/SharedFramework.External.props +++ b/eng/SharedFramework.External.props @@ -21,6 +21,8 @@ + + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 5bf2d93ee5c3..dff6d7b2fba1 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -9,300 +9,329 @@ --> - + https://github.com/dotnet/efcore - d3362f65007769fd1c8ef442743832d91c7a34cb + 62238db4577182dc5c41bcd4813424906e2c6799 - + https://github.com/dotnet/efcore - d3362f65007769fd1c8ef442743832d91c7a34cb + 62238db4577182dc5c41bcd4813424906e2c6799 - + https://github.com/dotnet/efcore - d3362f65007769fd1c8ef442743832d91c7a34cb + 62238db4577182dc5c41bcd4813424906e2c6799 - + https://github.com/dotnet/efcore - d3362f65007769fd1c8ef442743832d91c7a34cb + 62238db4577182dc5c41bcd4813424906e2c6799 - + https://github.com/dotnet/efcore - d3362f65007769fd1c8ef442743832d91c7a34cb + 62238db4577182dc5c41bcd4813424906e2c6799 - + https://github.com/dotnet/efcore - d3362f65007769fd1c8ef442743832d91c7a34cb + 62238db4577182dc5c41bcd4813424906e2c6799 - + https://github.com/dotnet/efcore - d3362f65007769fd1c8ef442743832d91c7a34cb + 62238db4577182dc5c41bcd4813424906e2c6799 - + https://github.com/dotnet/efcore - d3362f65007769fd1c8ef442743832d91c7a34cb + 62238db4577182dc5c41bcd4813424906e2c6799 - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + + https://github.com/dotnet/runtime + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a + + + https://github.com/dotnet/runtime + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a + + https://github.com/dotnet/source-build-externals - 21d564f9e16f7986d8af0692258afb91a52dda80 + 9439becb24301b70f92ce2534e2cb8a1ca9b7a99 - + + https://github.com/dotnet/runtime + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a + + + https://github.com/dotnet/runtime + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a + + + https://github.com/dotnet/runtime + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a + + + https://github.com/dotnet/runtime + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a + + + https://github.com/dotnet/runtime + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a + + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a https://github.com/dotnet/xdt 9a1c3e1b7f0c8763d4c96e593961a61a72679a7b - + https://github.com/dotnet/source-build-reference-packages - 43c337012443bb42d4baf97b4232dd04b443b5ef + db0cbe78748b71b00df05aff15cac2c8ce870cfd @@ -326,9 +355,9 @@ - + https://github.com/dotnet/runtime - 1e421670a6456d9c5b924b7ffea14cab8559a2e9 + bf7fb2ecbf69deb6a73bb8bbca1e56e7beec8d8a https://github.com/dotnet/arcade diff --git a/eng/Versions.props b/eng/Versions.props index 0bdde4945282..7043491348b7 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -63,78 +63,86 @@ --> - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 - 8.0.0-preview.5.23273.1 - 8.0.0-preview.5.23273.1 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 + 8.0.0-preview.6.23280.5 - 8.0.0-preview.6.23273.4 - 8.0.0-preview.6.23273.4 - 8.0.0-preview.6.23273.4 - 8.0.0-preview.6.23273.4 - 8.0.0-preview.6.23273.4 - 8.0.0-preview.6.23273.4 - 8.0.0-preview.6.23273.4 - 8.0.0-preview.6.23273.4 + 8.0.0-preview.6.23279.4 + 8.0.0-preview.6.23279.4 + 8.0.0-preview.6.23279.4 + 8.0.0-preview.6.23279.4 + 8.0.0-preview.6.23279.4 + 8.0.0-preview.6.23279.4 + 8.0.0-preview.6.23279.4 + 8.0.0-preview.6.23279.4 4.4.0-4.22520.2 4.4.0-4.22520.2 @@ -152,9 +160,9 @@ 8.0.0-beta.23218.3 - 8.0.0-alpha.1.23268.1 + 8.0.0-alpha.1.23274.1 - 8.0.0-alpha.1.23273.1 + 8.0.0-alpha.1.23274.2 7.0.0-preview.22423.2 diff --git a/eng/test-configuration.json b/eng/test-configuration.json index 16a5879abbb1..a471108ae6a0 100644 --- a/eng/test-configuration.json +++ b/eng/test-configuration.json @@ -17,6 +17,7 @@ {"testName": {"contains": "POST_ServerAbort_ClientReceivesAbort"}}, {"testName": {"contains": "POST_ServerAbortAfterWrite_ClientReceivesAbort"}}, {"testName": {"contains": "ServerReset_BeforeRequestBody_ClientBodyThrows"}}, + {"testName": {"contains": "InjectedStartup_DefaultApplicationNameIsEntryAssembly"}}, {"testAssembly": {"contains": "IIS"}}, {"testAssembly": {"contains": "Template"}}, {"failureMessage": {"contains":"(Site is started but no worker process found)"}}, diff --git a/eng/testing/linker/SupportFiles/Directory.Build.props b/eng/testing/linker/SupportFiles/Directory.Build.props index 38efb264b42d..d5fb01366512 100644 --- a/eng/testing/linker/SupportFiles/Directory.Build.props +++ b/eng/testing/linker/SupportFiles/Directory.Build.props @@ -5,7 +5,7 @@ true full - true + true true true diff --git a/eng/tools/GenerateFiles/Directory.Build.targets.in b/eng/tools/GenerateFiles/Directory.Build.targets.in index e202f98c2f93..9595d23b4930 100644 --- a/eng/tools/GenerateFiles/Directory.Build.targets.in +++ b/eng/tools/GenerateFiles/Directory.Build.targets.in @@ -139,7 +139,7 @@ BeforeTargets="ProcessFrameworkReferences"> diff --git a/global.json b/global.json index 6cd31d7e099b..e325b43e1a6c 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "8.0.100-preview.5.23257.1" + "version": "8.0.100-preview.6.23305.3" }, "tools": { - "dotnet": "8.0.100-preview.5.23257.1", + "dotnet": "8.0.100-preview.6.23305.3", "runtimes": { "dotnet/x86": [ "$(MicrosoftNETCoreBrowserDebugHostTransportVersion)" diff --git a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs index e0db0c46914d..05609873187d 100644 --- a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs +++ b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs @@ -1,8 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; @@ -32,6 +34,7 @@ public AuthorizeRouteViewTest() serviceCollection.AddSingleton(); serviceCollection.AddSingleton(_testAuthorizationService); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(); _renderer = new TestRenderer(services); @@ -467,4 +470,23 @@ public TestNavigationManager() Initialize("https://localhost:85/subdir/", "https://localhost:85/subdir/path?query=value#hash"); } } + + private class TestFormValueSupplier : IFormValueSupplier + { + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool CanConvertSingleValue(Type type) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue) + { + boundValue = null; + return false; + } + } } diff --git a/src/Components/Components/src/BindConverter.cs b/src/Components/Components/src/BindConverter.cs index 441d84f6e1b3..8320e835325d 100644 --- a/src/Components/Components/src/BindConverter.cs +++ b/src/Components/Components/src/BindConverter.cs @@ -1223,7 +1223,7 @@ private static bool ConvertToDateTimeCore(object? obj, CultureInfo? culture, out return ConvertToDateTimeCore(obj, culture, format: null, out value); } - private static bool ConvertToDateTimeCore(object? obj, CultureInfo? culture, string? format, out DateTime value) + private static bool ConvertToDateTimeCore(object? obj, CultureInfo? culture, [StringSyntax(StringSyntaxAttribute.TimeSpanFormat)] string? format, out DateTime value) { var text = (string?)obj; if (string.IsNullOrEmpty(text)) @@ -1252,7 +1252,7 @@ private static bool ConvertToNullableDateTimeCore(object? obj, CultureInfo? cult return ConvertToNullableDateTimeCore(obj, culture, format: null, out value); } - private static bool ConvertToNullableDateTimeCore(object? obj, CultureInfo? culture, string? format, out DateTime? value) + private static bool ConvertToNullableDateTimeCore(object? obj, CultureInfo? culture, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string? format, out DateTime? value) { var text = (string?)obj; if (string.IsNullOrEmpty(text)) @@ -1336,7 +1336,7 @@ private static bool ConvertToDateTimeOffsetCore(object? obj, CultureInfo? cultur return ConvertToDateTimeOffsetCore(obj, culture, format: null, out value); } - private static bool ConvertToDateTimeOffsetCore(object? obj, CultureInfo? culture, string? format, out DateTimeOffset value) + private static bool ConvertToDateTimeOffsetCore(object? obj, CultureInfo? culture, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string? format, out DateTimeOffset value) { var text = (string?)obj; if (string.IsNullOrEmpty(text)) @@ -1365,7 +1365,7 @@ private static bool ConvertToNullableDateTimeOffsetCore(object? obj, CultureInfo return ConvertToNullableDateTimeOffsetCore(obj, culture, format: null, out value); } - private static bool ConvertToNullableDateTimeOffsetCore(object? obj, CultureInfo? culture, string? format, out DateTimeOffset? value) + private static bool ConvertToNullableDateTimeOffsetCore(object? obj, CultureInfo? culture, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string? format, out DateTimeOffset? value) { var text = (string?)obj; if (string.IsNullOrEmpty(text)) diff --git a/src/Components/Components/src/Binding/CascadingModelBinder.cs b/src/Components/Components/src/Binding/CascadingModelBinder.cs index 4a6b842cc4f9..994e8430c3d6 100644 --- a/src/Components/Components/src/Binding/CascadingModelBinder.cs +++ b/src/Components/Components/src/Binding/CascadingModelBinder.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection.Metadata; +using Microsoft.AspNetCore.Components.Binding; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Routing; namespace Microsoft.AspNetCore.Components; @@ -9,11 +11,12 @@ namespace Microsoft.AspNetCore.Components; /// /// Defines the binding context for data bound from external sources. /// -public sealed class CascadingModelBinder : IComponent, IDisposable +public sealed class CascadingModelBinder : IComponent, ICascadingValueComponent, IDisposable { private RenderHandle _handle; private ModelBindingContext? _bindingContext; private bool _hasPendingQueuedRender; + private BindingInfo? _bindingInfo; /// /// The binding context name. @@ -35,7 +38,9 @@ public sealed class CascadingModelBinder : IComponent, IDisposable [CascadingParameter] ModelBindingContext? ParentContext { get; set; } - [Inject] private NavigationManager Navigation { get; set; } = null!; + [Inject] internal NavigationManager Navigation { get; set; } = null!; + + [Inject] internal IFormValueSupplier FormValueSupplier { get; set; } = null!; void IComponent.Attach(RenderHandle renderHandle) { @@ -87,7 +92,7 @@ private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) Render(); } - private void UpdateBindingInformation(string url) + internal void UpdateBindingInformation(string url) { // BindingContextId: action parameter used to define the handler // Name: form name and context used to bind @@ -103,13 +108,13 @@ private void UpdateBindingInformation(string url) // 3) Parent has a name "parent-name" // Name = "parent-name.my-handler"; // BindingContextId = <>((<>&)|?)handler=my-handler - var name = string.IsNullOrEmpty(ParentContext?.Name) ? Name : $"{ParentContext.Name}.{Name}"; + var name = ModelBindingContext.Combine(ParentContext, Name); var bindingId = string.IsNullOrEmpty(name) ? "" : GenerateBindingContextId(name); var bindingContext = _bindingContext != null && - string.Equals(_bindingContext.Name, Name, StringComparison.Ordinal) && + string.Equals(_bindingContext.Name, name, StringComparison.Ordinal) && string.Equals(_bindingContext.BindingContextId, bindingId, StringComparison.Ordinal) ? - _bindingContext : new ModelBindingContext(name, bindingId); + _bindingContext : new ModelBindingContext(name, bindingId, FormValueSupplier.CanConvertSingleValue); // It doesn't matter that we don't check IsFixed, since the CascadingValue we are setting up will throw if the app changes. if (IsFixed && _bindingContext != null && _bindingContext != bindingContext) @@ -136,4 +141,54 @@ void IDisposable.Dispose() { Navigation.LocationChanged -= HandleLocationChanged; } + + bool ICascadingValueComponent.CanSupplyValue(Type valueType, string? valueName) + { + var formName = string.IsNullOrEmpty(valueName) ? + (_bindingContext?.Name) : + ModelBindingContext.Combine(_bindingContext, valueName); + + if (_bindingInfo != null && + string.Equals(_bindingInfo.FormName, formName, StringComparison.Ordinal) && + _bindingInfo.ValueType.Equals(valueType)) + { + // We already bound the value, but some component might have been destroyed and + // re-created. If the type and name of the value that we bound are the same, + // we can provide the value that we bound. + return true; + } + + // Can't supply the value if this context is for a form with a different name. + if (FormValueSupplier.CanBind(formName!, valueType)) + { + var bindingSucceeded = FormValueSupplier.TryBind(formName!, valueType, out var boundValue); + _bindingInfo = new BindingInfo(formName, valueType, bindingSucceeded, boundValue); + if (!bindingSucceeded) + { + // Report errors + } + + return true; + } + + return false; + } + + void ICascadingValueComponent.Subscribe(ComponentState subscriber) + { + throw new InvalidOperationException("Form values are always fixed."); + } + + void ICascadingValueComponent.Unsubscribe(ComponentState subscriber) + { + throw new InvalidOperationException("Form values are always fixed."); + } + + object? ICascadingValueComponent.CurrentValue => _bindingInfo == null ? + throw new InvalidOperationException("Tried to access form value before it was bound.") : + _bindingInfo.BoundValue; + + bool ICascadingValueComponent.CurrentValueIsFixed => true; + + private record BindingInfo(string? FormName, Type ValueType, bool BindingResult, object? BoundValue); } diff --git a/src/Components/Components/src/Binding/IFormValueSupplier.cs b/src/Components/Components/src/Binding/IFormValueSupplier.cs new file mode 100644 index 000000000000..b8696a224339 --- /dev/null +++ b/src/Components/Components/src/Binding/IFormValueSupplier.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNetCore.Components.Binding; + +/// +/// Binds form data valuesto a model. +/// +public interface IFormValueSupplier +{ + /// + /// Determines whether the specified value type can be bound. + /// + /// The form name to bind data from. + /// The for the value to bind. + /// true if the value type can be bound; otherwise, false. + bool CanBind(string formName, Type valueType); + + /// + /// Determines whether a given can be converted from a single string value. + /// For example, strings, numbers, boolean values, enums, guids, etc. fall in this category. + /// + /// The to check. + /// true if the type can be converted from a single string value; otherwise, false. + bool CanConvertSingleValue(Type type); + + /// + /// Tries to bind the form with the specified name to a value of the specified type. + /// + /// The form name to bind data from. + /// The for the value to bind. + /// The bound value if succeeded. + /// true if the form was bound successfully; otherwise, false. + bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue); +} diff --git a/src/Components/Components/src/Binding/ModelBindingContext.cs b/src/Components/Components/src/Binding/ModelBindingContext.cs index 0659b325d8e1..75be17857de8 100644 --- a/src/Components/Components/src/Binding/ModelBindingContext.cs +++ b/src/Components/Components/src/Binding/ModelBindingContext.cs @@ -8,10 +8,13 @@ namespace Microsoft.AspNetCore.Components; /// public sealed class ModelBindingContext { - internal ModelBindingContext(string name, string bindingContextId) + private readonly Predicate _canBind; + + internal ModelBindingContext(string name, string bindingContextId, Predicate canBind) { ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(bindingContextId); + ArgumentNullException.ThrowIfNull(canBind); // We are initializing the root context, that can be a "named" root context, or the default context. // A named root context only provides a name, and that acts as the BindingId // A "default" root context does not provide a name, and instead it provides an explicit Binding ID. @@ -23,6 +26,7 @@ internal ModelBindingContext(string name, string bindingContextId) Name = name; BindingContextId = bindingContextId ?? name; + _canBind = canBind; } /// @@ -34,4 +38,12 @@ internal ModelBindingContext(string name, string bindingContextId) /// The computed identifier used to determine what parts of the app can bind data. /// public string BindingContextId { get; } + + internal static string Combine(ModelBindingContext? parentContext, string name) => + string.IsNullOrEmpty(parentContext?.Name) ? name : $"{parentContext.Name}.{name}"; + + internal bool CanConvert(Type type) + { + return _canBind(type); + } } diff --git a/src/Components/Components/src/CascadingParameterState.cs b/src/Components/Components/src/CascadingParameterState.cs index 52caad659d0b..7af4dc1b9cce 100644 --- a/src/Components/Components/src/CascadingParameterState.cs +++ b/src/Components/Components/src/CascadingParameterState.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Components.Reflection; using Microsoft.AspNetCore.Components.Rendering; @@ -45,11 +46,8 @@ public static IReadOnlyList FindCascadingParameters(Com var supplier = GetMatchingCascadingValueSupplier(info, componentState); if (supplier != null) { - if (resultStates == null) - { - // Although not all parameters might be matched, we know the maximum number - resultStates = new List(infos.Length - infoIndex); - } + // Although not all parameters might be matched, we know the maximum number + resultStates ??= new List(infos.Length - infoIndex); resultStates.Add(new CascadingParameterState(info.ConsumerValueName, supplier)); } @@ -98,16 +96,25 @@ private static ReflectedCascadingParameterInfo[] CreateReflectedCascadingParamet var attribute = prop.GetCustomAttribute(); if (attribute != null) { - if (result == null) - { - result = new List(); - } + result ??= new List(); result.Add(new ReflectedCascadingParameterInfo( prop.Name, prop.PropertyType, attribute.Name)); } + + var hostParameterAttribute = prop.GetCustomAttributes() + .OfType().SingleOrDefault(); + if (hostParameterAttribute != null) + { + result ??= new List(); + + result.Add(new ReflectedCascadingParameterInfo( + prop.Name, + prop.PropertyType, + hostParameterAttribute.Name)); + } } return result?.ToArray() ?? Array.Empty(); diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index 0496e1e028a5..e0150ec3dc62 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -327,7 +327,7 @@ Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg) Task IHandleAfterRender.OnAfterRenderAsync() { var firstRender = !_hasCalledOnAfterRender; - _hasCalledOnAfterRender |= true; + _hasCalledOnAfterRender = true; OnAfterRender(firstRender); diff --git a/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs b/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs new file mode 100644 index 000000000000..8f407e0cdd5e --- /dev/null +++ b/src/Components/Components/src/IHostEnvironmentCascadingParameter.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +// Marks a cascading parameter that can be offered via an attribute that is not +// directly defined in the Components assembly. For example [SupplyParameterFromForm]. +internal interface IHostEnvironmentCascadingParameter +{ + public string? Name { get; set; } +} diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 6048b3a28d79..625c0e05e595 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,5 +1,9 @@ #nullable enable abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! +Microsoft.AspNetCore.Components.Binding.IFormValueSupplier +Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.CanBind(string! formName, System.Type! valueType) -> bool +Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.CanConvertSingleValue(System.Type! type) -> bool +Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.TryBind(string! formName, System.Type! valueType, out object? boundValue) -> bool Microsoft.AspNetCore.Components.CascadingModelBinder Microsoft.AspNetCore.Components.CascadingModelBinder.CascadingModelBinder() -> void Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment! @@ -34,7 +38,6 @@ Microsoft.AspNetCore.Components.Rendering.ComponentState Microsoft.AspNetCore.Components.Rendering.ComponentState.Component.get -> Microsoft.AspNetCore.Components.IComponent! Microsoft.AspNetCore.Components.Rendering.ComponentState.ComponentId.get -> int Microsoft.AspNetCore.Components.Rendering.ComponentState.ComponentState(Microsoft.AspNetCore.Components.RenderTree.Renderer! renderer, int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> void -Microsoft.AspNetCore.Components.Rendering.ComponentState.Dispose() -> void Microsoft.AspNetCore.Components.Rendering.ComponentState.ParentComponentState.get -> Microsoft.AspNetCore.Components.Rendering.ComponentState? Microsoft.AspNetCore.Components.RenderTree.Renderer.GetComponentState(int componentId) -> Microsoft.AspNetCore.Components.Rendering.ComponentState! Microsoft.AspNetCore.Components.Sections.SectionContent @@ -61,6 +64,7 @@ override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool override Microsoft.AspNetCore.Components.EventCallback.GetHashCode() -> int override Microsoft.AspNetCore.Components.EventCallback.Equals(object? obj) -> bool +virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.CreateComponentState(int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> Microsoft.AspNetCore.Components.Rendering.ComponentState! virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task! diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index dd0df1e094cd..9569e8e6a84e 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using static Microsoft.AspNetCore.Internal.LinkerFlags; @@ -168,11 +169,14 @@ private static void ThrowForUnknownIncomingParameterName([DynamicallyAccessedMem var propertyInfo = targetType.GetProperty(parameterName, BindablePropertyFlags); if (propertyInfo != null) { - if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && !propertyInfo.IsDefined(typeof(CascadingParameterAttribute))) + if (!propertyInfo.IsDefined(typeof(ParameterAttribute)) && + !propertyInfo.IsDefined(typeof(CascadingParameterAttribute)) && + !propertyInfo.GetCustomAttributes().OfType().Any()) { throw new InvalidOperationException( $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " + - $"but it does not have [{nameof(ParameterAttribute)}] or [{nameof(CascadingParameterAttribute)}] applied."); + $"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or " + + $"[SupplyParameterFromFormAttribute] applied."); } else { @@ -257,9 +261,30 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType) foreach (var propertyInfo in GetCandidateBindableProperties(targetType)) { - var parameterAttribute = propertyInfo.GetCustomAttribute(); - var cascadingParameterAttribute = propertyInfo.GetCustomAttribute(); - var isParameter = parameterAttribute != null || cascadingParameterAttribute != null; + ParameterAttribute? parameterAttribute = null; + CascadingParameterAttribute? cascadingParameterAttribute = null; + IHostEnvironmentCascadingParameter? hostEnvironmentCascadingParameter = null; + + var attributes = propertyInfo.GetCustomAttributes(); + foreach (var attribute in attributes) + { + switch (attribute) + { + case ParameterAttribute parameter: + parameterAttribute = parameter; + break; + case CascadingParameterAttribute cascadingParameter: + cascadingParameterAttribute = cascadingParameter; + break; + case IHostEnvironmentCascadingParameter hostEnvironmentAttribute: + hostEnvironmentCascadingParameter = hostEnvironmentAttribute; + break; + default: + break; + } + } + + var isParameter = parameterAttribute != null || cascadingParameterAttribute != null || hostEnvironmentCascadingParameter != null; if (!isParameter) { continue; @@ -274,7 +299,7 @@ public WritersForType([DynamicallyAccessedMembers(Component)] Type targetType) var propertySetter = new PropertySetter(targetType, propertyInfo) { - Cascading = cascadingParameterAttribute != null, + Cascading = cascadingParameterAttribute != null || hostEnvironmentCascadingParameter != null, }; if (_underlyingWriters.ContainsKey(propertyName)) diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 001454f09b02..187ffb6d8f79 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -876,28 +876,20 @@ private void ProcessDisposalQueueInExistingBatch() var disposeComponentId = _batchBuilder.ComponentDisposalQueue.Dequeue(); var disposeComponentState = GetRequiredComponentState(disposeComponentId); Log.DisposingComponent(_logger, disposeComponentState); - if (!(disposeComponentState.Component is IAsyncDisposable)) - { - if (!disposeComponentState.TryDisposeInBatch(_batchBuilder, out var exception)) - { - exceptions ??= new List(); - exceptions.Add(exception); - } - } - else + + try { - var result = disposeComponentState.DisposeInBatchAsync(_batchBuilder); - if (result.IsCompleted) + var disposalTask = disposeComponentState.DisposeInBatchAsync(_batchBuilder); + if (disposalTask.IsCompletedSuccessfully) { - if (!result.IsCompletedSuccessfully) - { - exceptions ??= new List(); - exceptions.Add(result.Exception); - } + // If it's a IValueTaskSource backed ValueTask, + // inform it its result has been read so it can reset + disposalTask.GetAwaiter().GetResult(); } else { // We set owningComponentState to null because we don't want exceptions during disposal to be recoverable + var result = disposalTask.AsTask(); AddToPendingTasksWithErrorHandling(GetHandledAsynchronousDisposalErrorsTask(result), owningComponentState: null); async Task GetHandledAsynchronousDisposalErrorsTask(Task result) @@ -913,6 +905,11 @@ async Task GetHandledAsynchronousDisposalErrorsTask(Task result) } } } + catch (Exception exception) + { + exceptions ??= new List(); + exceptions.Add(exception); + } _componentStateById.Remove(disposeComponentId); _componentStateByComponent.Remove(disposeComponentState.Component); @@ -1080,39 +1077,25 @@ protected virtual void Dispose(bool disposing) { Log.DisposingComponent(_logger, componentState); - // Components shouldn't need to implement IAsyncDisposable and IDisposable simultaneously, - // but in case they do, we prefer the async overload since we understand the sync overload - // is implemented for more "constrained" scenarios. - // Component authors are responsible for their IAsyncDisposable implementations not taking - // forever. - if (componentState.Component is IAsyncDisposable asyncDisposable) + try { - try + var task = componentState.DisposeAsync(); + if (task.IsCompletedSuccessfully) { - var task = asyncDisposable.DisposeAsync(); - if (!task.IsCompletedSuccessfully) - { - asyncDisposables ??= new(); - asyncDisposables.Add(task.AsTask()); - } + // If it's a IValueTaskSource backed ValueTask, + // inform it its result has been read so it can reset + task.GetAwaiter().GetResult(); } - catch (Exception exception) + else { - exceptions ??= new List(); - exceptions.Add(exception); + asyncDisposables ??= new(); + asyncDisposables.Add(task.AsTask()); } } - else if (componentState.Component is IDisposable disposable) + catch (Exception exception) { - try - { - componentState.Dispose(); - } - catch (Exception exception) - { - exceptions ??= new List(); - exceptions.Add(exception); - } + exceptions ??= new List(); + exceptions.Add(exception); } } diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 8252c36a1a6d..f7de9f106215 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components.RenderTree; namespace Microsoft.AspNetCore.Components.Rendering; @@ -13,7 +12,7 @@ namespace Microsoft.AspNetCore.Components.Rendering; /// detail of . /// [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] -public class ComponentState : IDisposable +public class ComponentState : IAsyncDisposable { private readonly Renderer _renderer; private readonly IReadOnlyList _cascadingParameters; @@ -108,41 +107,6 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re batchBuilder.InvalidateParameterViews(); } - internal bool TryDisposeInBatch(RenderBatchBuilder batchBuilder, [NotNullWhen(false)] out Exception? exception) - { - _componentWasDisposed = true; - exception = null; - - try - { - if (Component is IDisposable disposable) - { - disposable.Dispose(); - } - } - catch (Exception ex) - { - exception = ex; - } - - CleanupComponentStateResources(batchBuilder); - - return exception == null; - } - - private void CleanupComponentStateResources(RenderBatchBuilder batchBuilder) - { - // We don't expect these things to throw. - RenderTreeDiffBuilder.DisposeFrames(batchBuilder, CurrentRenderTree.GetFrames()); - - if (_hasAnyCascadingParameterSubscriptions) - { - RemoveCascadingParameterSubscriptions(); - } - - DisposeBuffers(); - } - // Callers expect this method to always return a faulted task. internal Task NotifyRenderCompletedAsync() { @@ -254,15 +218,26 @@ private void RemoveCascadingParameterSubscriptions() } /// - /// Disposes this instance. + /// Disposes this instance and its associated component. /// - public void Dispose() + public virtual ValueTask DisposeAsync() { + _componentWasDisposed = true; DisposeBuffers(); - if (Component is IDisposable disposable) + // Components shouldn't need to implement IAsyncDisposable and IDisposable simultaneously, + // but in case they do, we prefer the async overload since we understand the sync overload + // is implemented for more "constrained" scenarios. + // Component authors are responsible for their IAsyncDisposable implementations not taking + // forever. + if (Component is IAsyncDisposable asyncDisposable) { - disposable.Dispose(); + return asyncDisposable.DisposeAsync(); + } + else + { + (Component as IDisposable)?.Dispose(); + return ValueTask.CompletedTask; } } @@ -273,33 +248,17 @@ private void DisposeBuffers() _latestDirectParametersSnapshot?.Dispose(); } - internal Task DisposeInBatchAsync(RenderBatchBuilder batchBuilder) + internal ValueTask DisposeInBatchAsync(RenderBatchBuilder batchBuilder) { - _componentWasDisposed = true; - - CleanupComponentStateResources(batchBuilder); + // We don't expect these things to throw. + RenderTreeDiffBuilder.DisposeFrames(batchBuilder, CurrentRenderTree.GetFrames()); - try - { - var result = ((IAsyncDisposable)Component).DisposeAsync(); - if (result.IsCompletedSuccessfully) - { - // If it's a IValueTaskSource backed ValueTask, - // inform it its result has been read so it can reset - result.GetAwaiter().GetResult(); - return Task.CompletedTask; - } - else - { - // We know we are dealing with an exception that happened asynchronously, so return a task - // to the caller so that he can unwrap it. - return result.AsTask(); - } - } - catch (Exception e) + if (_hasAnyCascadingParameterSubscriptions) { - return Task.FromException(e); + RemoveCascadingParameterSubscriptions(); } + + return DisposeAsync(); } private string GetDebuggerDisplay() diff --git a/src/Components/Components/src/RouteView.cs b/src/Components/Components/src/RouteView.cs index e3c9f6b4a54e..3d07cbc4f903 100644 --- a/src/Components/Components/src/RouteView.cs +++ b/src/Components/Components/src/RouteView.cs @@ -3,8 +3,10 @@ #nullable disable warnings +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using Microsoft.AspNetCore.Components.HotReload; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Routing; @@ -17,6 +19,15 @@ namespace Microsoft.AspNetCore.Components; public class RouteView : IComponent { private RenderHandle _renderHandle; + private static readonly ConcurrentDictionary _layoutAttributeCache = new(); + + static RouteView() + { + if (HotReloadManager.Default.MetadataUpdateSupported) + { + HotReloadManager.Default.OnDeltaApplied += _layoutAttributeCache.Clear; + } + } [Inject] private NavigationManager NavigationManager { get; set; } @@ -65,7 +76,8 @@ public Task SetParametersAsync(ParameterView parameters) [UnconditionalSuppressMessage("Trimming", "IL2118", Justification = "Layout components are preserved because the LayoutAttribute constructor parameter is correctly annotated.")] protected virtual void Render(RenderTreeBuilder builder) { - var pageLayoutType = RouteData.PageType.GetCustomAttribute()?.LayoutType + var pageLayoutType = _layoutAttributeCache + .GetOrAdd(RouteData.PageType, static type => type.GetCustomAttribute()?.LayoutType) ?? DefaultLayout; builder.OpenComponent(0); diff --git a/src/Components/Components/test/CascadingModelBinderTest.cs b/src/Components/Components/test/CascadingModelBinderTest.cs index e8a60445b8c9..c20703b51d8f 100644 --- a/src/Components/Components/test/CascadingModelBinderTest.cs +++ b/src/Components/Components/test/CascadingModelBinderTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.Extensions.DependencyInjection; @@ -18,6 +19,7 @@ public CascadingModelBinderTest() var serviceCollection = new ServiceCollection(); _navigationManager = new TestNavigationManager(); serviceCollection.AddSingleton(_navigationManager); + serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(); _renderer = new TestRenderer(services); } @@ -328,4 +330,23 @@ public TestComponent(RenderFragment renderFragment) protected override void BuildRenderTree(RenderTreeBuilder builder) => _renderFragment(builder); } + + private class TestFormValueSupplier : IFormValueSupplier + { + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool CanConvertSingleValue(Type type) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue) + { + boundValue = null; + return false; + } + } } diff --git a/src/Components/Components/test/CascadingParameterStateTest.cs b/src/Components/Components/test/CascadingParameterStateTest.cs index 648056c152c0..6edf09f31e21 100644 --- a/src/Components/Components/test/CascadingParameterStateTest.cs +++ b/src/Components/Components/test/CascadingParameterStateTest.cs @@ -1,8 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Test.Helpers; +using Moq; namespace Microsoft.AspNetCore.Components; @@ -355,6 +358,68 @@ public void FindCascadingParameters_CanOverrideNonNullValueWithNull() }); } + [Fact] + public void FindCascadingParameters_HandlesSupplyParameterFromFormValues() + { + // Arrange + var cascadingModelBinder = new CascadingModelBinder + { + FormValueSupplier = new TestFormValueSupplier() + { + FormName = "", + ValueType = typeof(string), + BindResult = true, + BoundValue = "some value" + }, + Navigation = Mock.Of(), + Name = "" + }; + + cascadingModelBinder.UpdateBindingInformation("https://localhost/"); + + var states = CreateAncestry( + cascadingModelBinder, + new FormParametersComponent()); + + // Act + var result = CascadingParameterState.FindCascadingParameters(states.Last()); + + // Assert + var supplier = Assert.Single(result); + Assert.Equal(cascadingModelBinder, supplier.ValueSupplier); + } + + [Fact] + public void FindCascadingParameters_HandlesSupplyParameterFromFormValues_WithName() + { + // Arrange + var cascadingModelBinder = new CascadingModelBinder + { + FormValueSupplier = new TestFormValueSupplier() + { + FormName = "some-name", + ValueType = typeof(string), + BindResult = true, + BoundValue = "some value" + }, + Navigation = new TestNavigationManager(), + Name = "" + }; + + cascadingModelBinder.UpdateBindingInformation("https://localhost/"); + + var states = CreateAncestry( + cascadingModelBinder, + new FormParametersComponentWithName()); + + // Act + var result = CascadingParameterState.FindCascadingParameters(states.Last()); + + // Assert + var supplier = Assert.Single(result); + Assert.Equal(cascadingModelBinder, supplier.ValueSupplier); + } + static ComponentState[] CreateAncestry(params IComponent[] components) { var result = new ComponentState[components.Length]; @@ -399,6 +464,16 @@ class ComponentWithNoParams : TestComponentBase { } + class FormParametersComponent : TestComponentBase + { + [SupplyParameterFromForm] public string FormParameter { get; set; } + } + + class FormParametersComponentWithName : TestComponentBase + { + [SupplyParameterFromForm(Name = "some-name")] public string FormParameter { get; set; } + } + class ComponentWithNoCascadingParams : TestComponentBase { [Parameter] public bool SomeRegularParameter { get; set; } @@ -443,4 +518,50 @@ class ValueType3 { } class CascadingValueTypeBaseClass { } class CascadingValueTypeDerivedClass : CascadingValueTypeBaseClass, ICascadingValueTypeDerivedClassInterface { } interface ICascadingValueTypeDerivedClassInterface { } + + private class TestFormValueSupplier : IFormValueSupplier + { + public string FormName { get; set; } + + public Type ValueType { get; set; } + + public object BoundValue { get; set; } + + public bool BindResult { get; set; } + + public bool CanBind(string formName, Type valueType) + { + return string.Equals(formName, FormName, StringComparison.Ordinal) && + valueType == ValueType; + } + + public bool CanConvertSingleValue(Type type) + { + return type == ValueType; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue) + { + boundValue = BoundValue; + return BindResult; + } + } + + class TestNavigationManager : NavigationManager + { + public TestNavigationManager() + { + Initialize("https://localhost:85/subdir/", "https://localhost:85/subdir/path?query=value#hash"); + } + } +} + +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public sealed class SupplyParameterFromFormAttribute : Attribute, IHostEnvironmentCascadingParameter +{ + /// + /// Gets or sets the name for the parameter. The name is used to match + /// the form data and decide whether or not the value needs to be bound. + /// + public string Name { get; set; } } diff --git a/src/Components/Components/test/ModelBindingContextTest.cs b/src/Components/Components/test/ModelBindingContextTest.cs index 4f82bdc4290d..76742c34cef2 100644 --- a/src/Components/Components/test/ModelBindingContextTest.cs +++ b/src/Components/Components/test/ModelBindingContextTest.cs @@ -8,7 +8,7 @@ public class ModelBindingContextTest [Fact] public void CanCreate_BindingContext_WithDefaultName() { - var context = new ModelBindingContext("", ""); + var context = new ModelBindingContext("", "", t => true); Assert.Equal("", context.Name); Assert.Equal("", context.BindingContextId); } @@ -16,7 +16,7 @@ public void CanCreate_BindingContext_WithDefaultName() [Fact] public void CanCreate_BindingContext_WithName() { - var context = new ModelBindingContext("name", "path?handler=name"); + var context = new ModelBindingContext("name", "path?handler=name", t => true); Assert.Equal("name", context.Name); Assert.Equal("path?handler=name", context.BindingContextId); } @@ -24,14 +24,14 @@ public void CanCreate_BindingContext_WithName() [Fact] public void Throws_WhenNameIsProvided_AndNoBindingContextId() { - var exception = Assert.Throws(() => new ModelBindingContext("name", "")); + var exception = Assert.Throws(() => new ModelBindingContext("name", "", t => true)); Assert.Equal("A root binding context needs to provide a name and explicit binding context id or none.", exception.Message); } [Fact] public void Throws_WhenBindingContextId_IsProvidedForDefaultName() { - var exception = Assert.Throws(() => new ModelBindingContext("", "context")); + var exception = Assert.Throws(() => new ModelBindingContext("", "context", t => true)); Assert.Equal("A root binding context needs to provide a name and explicit binding context id or none.", exception.Message); } } diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index 8fff5c2003d0..bcc032c7e080 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -181,7 +181,7 @@ public void IncomingParameterMatchesPropertyNotDeclaredAsParameter_Throws() Assert.Equal(default, target.IntProp); Assert.Equal( $"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' has a property matching the name '{nameof(HasPropertyWithoutParameterAttribute.IntProp)}', " + - $"but it does not have [{nameof(ParameterAttribute)}] or [{nameof(CascadingParameterAttribute)}] applied.", + $"but it does not have [{nameof(ParameterAttribute)}], [{nameof(CascadingParameterAttribute)}] or [{nameof(SupplyParameterFromFormAttribute)}] applied.", ex.Message); } diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index 29dd39f23805..02ecce2bbdcd 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -2334,9 +2334,8 @@ public void RenderBatch_HandlesSynchronousExceptionsInAsyncDisposableComponents( // Outer component is still alive and not disposed. Assert.False(component.Disposed); - var aex = Assert.IsType(Assert.Single(renderer.HandledExceptions)); - var innerException = Assert.Single(aex.Flatten().InnerExceptions); - Assert.Same(exception1, innerException); + var aex = Assert.Single(renderer.HandledExceptions); + Assert.Same(exception1, aex); } [Fact] @@ -2493,8 +2492,7 @@ public void RenderBatch_ReportsSynchronousCancelationsAsErrors() // Outer component is still alive and not disposed. Assert.False(component.Disposed); - var aex = Assert.IsType(Assert.Single(renderer.HandledExceptions)); - Assert.IsType(Assert.Single(aex.Flatten().InnerExceptions)); + Assert.IsType(Assert.Single(renderer.HandledExceptions)); } [Fact] diff --git a/src/Components/Components/test/RouteViewTest.cs b/src/Components/Components/test/RouteViewTest.cs index 92abd7e97b47..c791d7243363 100644 --- a/src/Components/Components/test/RouteViewTest.cs +++ b/src/Components/Components/test/RouteViewTest.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.Extensions.DependencyInjection; @@ -19,6 +21,7 @@ public RouteViewTest() var serviceCollection = new ServiceCollection(); _navigationManager = new RouteViewTestNavigationManager(); serviceCollection.AddSingleton(_navigationManager); + serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(); _renderer = new TestRenderer(services); @@ -237,4 +240,23 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddContent(2, "OtherLayout ends here"); } } + + private class TestFormValueSupplier : IFormValueSupplier + { + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool CanConvertSingleValue(Type type) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue) + { + boundValue = null; + return false; + } + } } diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ArrayCollectionFactory.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ArrayCollectionFactory.cs new file mode 100644 index 000000000000..2b6b690d120f --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ArrayCollectionFactory.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ArrayCollectionFactory : ICollectionFactory +{ + public static TElement[] ToResultCore(TElement[] buffer, int size) + { + var result = new TElement[size]; + Array.Copy(buffer, result, size); + return result; + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs new file mode 100644 index 000000000000..188ef4faa0ed --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ArrayPoolBufferAdapter.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal abstract class ArrayPoolBufferAdapter + : ICollectionBufferAdapter.PooledBuffer, TElement> + where TCollectionFactory : ICollectionFactory +{ + public static PooledBuffer CreateBuffer() => new() { Data = ArrayPool.Shared.Rent(16), Count = 0 }; + + public static PooledBuffer Add(ref PooledBuffer buffer, TElement element) + { + if (buffer.Count >= buffer.Data.Length) + { + var newBuffer = ArrayPool.Shared.Rent(buffer.Data.Length * 2); + Array.Copy(buffer.Data, newBuffer, buffer.Data.Length); + ArrayPool.Shared.Return(buffer.Data); + buffer.Data = newBuffer; + } + + buffer.Data[buffer.Count++] = element; + return buffer; + } + + public static TCollection ToResult(PooledBuffer buffer) + { + var result = TCollectionFactory.ToResultCore(buffer.Data, buffer.Count); + ArrayPool.Shared.Return(buffer.Data); + return result; + } + + public struct PooledBuffer + { + public TElement[] Data { get; set; } + public int Count { get; set; } + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ConcurrentBagBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ConcurrentBagBufferAdapter.cs new file mode 100644 index 000000000000..6d4590bc390d --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ConcurrentBagBufferAdapter.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ConcurrentBagBufferAdapter : ICollectionBufferAdapter, ConcurrentBag, TElement> +{ + public static ConcurrentBag CreateBuffer() => new(); + + public static ConcurrentBag Add(ref ConcurrentBag buffer, TElement element) + { + buffer.Add(element); + return buffer; + } + + public static ConcurrentBag ToResult(ConcurrentBag buffer) => buffer; +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ConcurrentQueueBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ConcurrentQueueBufferAdapter.cs new file mode 100644 index 000000000000..c54b08d8bd2b --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ConcurrentQueueBufferAdapter.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ConcurrentQueueBufferAdapter : ICollectionBufferAdapter, ConcurrentQueue, TElement> +{ + public static ConcurrentQueue CreateBuffer() => new(); + + public static ConcurrentQueue Add(ref ConcurrentQueue buffer, TElement element) + { + buffer.Enqueue(element); + return buffer; + } + + public static ConcurrentQueue ToResult(ConcurrentQueue buffer) => buffer; +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ConcurrentStackBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ConcurrentStackBufferAdapter.cs new file mode 100644 index 000000000000..349856559739 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ConcurrentStackBufferAdapter.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ConcurrentStackBufferAdapter : ICollectionBufferAdapter, ConcurrentStack, TElement> +{ + public static ConcurrentStack CreateBuffer() => new(); + + public static ConcurrentStack Add(ref ConcurrentStack buffer, TElement element) + { + buffer.Push(element); + return buffer; + } + + public static ConcurrentStack ToResult(ConcurrentStack buffer) => buffer; +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ICollectionBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ICollectionBufferAdapter.cs new file mode 100644 index 000000000000..58af62b4ca74 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ICollectionBufferAdapter.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal interface ICollectionBufferAdapter +{ + public static abstract TBuffer CreateBuffer(); + public static abstract TBuffer Add(ref TBuffer buffer, TElement element); + public static abstract TCollection ToResult(TBuffer buffer); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ICollectionFactory.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ICollectionFactory.cs new file mode 100644 index 000000000000..bad088ada5c2 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ICollectionFactory.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal interface ICollectionFactory +{ + public static abstract TCollection ToResultCore(TElement[] buffer, int size); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableArrayBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableArrayBufferAdapter.cs new file mode 100644 index 000000000000..9aa3dbfcabe8 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableArrayBufferAdapter.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ImmutableArrayBufferAdapter : ICollectionBufferAdapter, ImmutableArray.Builder, TElement> +{ + public static ImmutableArray.Builder CreateBuffer() => ImmutableArray.CreateBuilder(); + + public static ImmutableArray.Builder Add(ref ImmutableArray.Builder buffer, TElement element) + { + buffer.Add(element); + return buffer; + } + + public static ImmutableArray ToResult(ImmutableArray.Builder buffer) => buffer.ToImmutable(); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableHashSetBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableHashSetBufferAdapter.cs new file mode 100644 index 000000000000..1f93d977ff2e --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableHashSetBufferAdapter.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ImmutableHashSetBufferAdapter : ICollectionBufferAdapter, ImmutableHashSet.Builder, TElement> +{ + public static ImmutableHashSet.Builder CreateBuffer() => ImmutableHashSet.CreateBuilder(); + + public static ImmutableHashSet.Builder Add(ref ImmutableHashSet.Builder buffer, TElement element) + { + buffer.Add(element); + return buffer; + } + + public static ImmutableHashSet ToResult(ImmutableHashSet.Builder buffer) => buffer.ToImmutable(); + + public static CollectionConverter> CreateInterfaceConverter(FormDataConverter elementConverter) + { + return new CollectionConverter< + IImmutableSet, + StaticCastAdapter< + IImmutableSet, + ImmutableHashSet, + ImmutableHashSetBufferAdapter, + ImmutableHashSet.Builder, + TElement>, + ImmutableHashSet.Builder, + TElement>(elementConverter); + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableListBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableListBufferAdapter.cs new file mode 100644 index 000000000000..2a00632136a5 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableListBufferAdapter.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ImmutableListBufferAdapter : ICollectionBufferAdapter, ImmutableList.Builder, TElement> +{ + public static ImmutableList.Builder CreateBuffer() => ImmutableList.CreateBuilder(); + + public static ImmutableList.Builder Add(ref ImmutableList.Builder buffer, TElement element) + { + buffer.Add(element); + return buffer; + } + + public static ImmutableList ToResult(ImmutableList.Builder buffer) => buffer.ToImmutable(); + + public static CollectionConverter> CreateInterfaceConverter(FormDataConverter elementConverter) + { + return new CollectionConverter< + IImmutableList, + StaticCastAdapter< + IImmutableList, + ImmutableList, + ImmutableListBufferAdapter, + ImmutableList.Builder, + TElement>, + ImmutableList.Builder, + TElement>(elementConverter); + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableQueueBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableQueueBufferAdapter.cs new file mode 100644 index 000000000000..59ad0b1c5df6 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableQueueBufferAdapter.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Linq; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ImmutableQueueBufferAdapter + : ArrayPoolBufferAdapter, ImmutableQueueBufferAdapter.ImmutableQueueFactory, TElement> +{ + internal class ImmutableQueueFactory : ICollectionFactory, TElement> + { + public static ImmutableQueue ToResultCore(TElement[] buffer, int size) + { + return ImmutableQueue.CreateRange(buffer.Take(size)); + } + } + + public static CollectionConverter> CreateInterfaceConverter(FormDataConverter elementConverter) + { + return new CollectionConverter< + IImmutableQueue, + StaticCastAdapter< + IImmutableQueue, + ImmutableQueue, + ImmutableQueueBufferAdapter, + PooledBuffer, + TElement>, + PooledBuffer, + TElement>(elementConverter); + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableSortedSetBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableSortedSetBufferAdapter.cs new file mode 100644 index 000000000000..f9562f048e07 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableSortedSetBufferAdapter.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ImmutableSortedSetBufferAdapter : ICollectionBufferAdapter, ImmutableSortedSet.Builder, TElement> +{ + public static ImmutableSortedSet.Builder CreateBuffer() => ImmutableSortedSet.CreateBuilder(); + + public static ImmutableSortedSet.Builder Add(ref ImmutableSortedSet.Builder buffer, TElement element) + { + buffer.Add(element); + return buffer; + } + + public static ImmutableSortedSet ToResult(ImmutableSortedSet.Builder buffer) => buffer.ToImmutable(); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableStackBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableStackBufferAdapter.cs new file mode 100644 index 000000000000..bff06bab2302 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImmutableStackBufferAdapter.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Linq; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ImmutableStackBufferAdapter + : ArrayPoolBufferAdapter, ImmutableStackBufferAdapter.ImmutableStackFactory, TElement> +{ + internal class ImmutableStackFactory : ICollectionFactory, TElement> + { + public static ImmutableStack ToResultCore(TElement[] buffer, int size) + { + return ImmutableStack.CreateRange(buffer.Take(size)); + } + } + + public static CollectionConverter> CreateInterfaceConverter(FormDataConverter elementConverter) + { + return new CollectionConverter< + IImmutableStack, + StaticCastAdapter< + IImmutableStack, + ImmutableStack, + ImmutableStackBufferAdapter, + PooledBuffer, + TElement>, + PooledBuffer, + TElement>(elementConverter); + } +} + +internal class StaticCastAdapter + : ICollectionBufferAdapter + where TCollectionAdapter : ICollectionBufferAdapter + where TCollectionImplementation : TCollectionInterface +{ + public static TBuffer CreateBuffer() => TCollectionAdapter.CreateBuffer(); + + public static TBuffer Add(ref TBuffer buffer, TElement element) => TCollectionAdapter.Add(ref buffer, element); + + public static TCollectionInterface ToResult(TBuffer buffer) => TCollectionAdapter.ToResult(buffer); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImplementingCollectionBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImplementingCollectionBufferAdapter.cs new file mode 100644 index 000000000000..5659e483d75b --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ImplementingCollectionBufferAdapter.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ImplementingCollectionBufferAdapter : ICollectionBufferAdapter + where TBuffer : TCollection, ICollection, new() +{ + public static TBuffer CreateBuffer() => new(); + + public static TBuffer Add(ref TBuffer buffer, TElement element) + { + buffer.Add(element); + return buffer; + } + + public static TCollection ToResult(TBuffer buffer) => buffer; +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/QueueBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/QueueBufferAdapter.cs new file mode 100644 index 000000000000..59176a66e858 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/QueueBufferAdapter.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class QueueBufferAdapter : ICollectionBufferAdapter, Queue, TElement> +{ + public static Queue CreateBuffer() => new(); + + public static Queue Add(ref Queue buffer, TElement element) + { + buffer.Enqueue(element); + return buffer; + } + + public static Queue ToResult(Queue buffer) => buffer; +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ReadOnlyCollectionBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ReadOnlyCollectionBufferAdapter.cs new file mode 100644 index 000000000000..dad0f5471798 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/ReadOnlyCollectionBufferAdapter.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.ObjectModel; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ReadOnlyCollectionBufferAdapter : ICollectionBufferAdapter, IList, TElement> +{ + public static IList CreateBuffer() => new List(); + + public static IList Add(ref IList buffer, TElement element) + { + buffer.Add(element); + return buffer; + } + + public static ReadOnlyCollection ToResult(IList buffer) => new(buffer); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/StackBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/StackBufferAdapter.cs new file mode 100644 index 000000000000..04be226b0774 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionAdapters/StackBufferAdapter.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class StackBufferAdapter : ICollectionBufferAdapter, Stack, TElement> +{ + public static Stack CreateBuffer() => new(); + + public static Stack Add(ref Stack buffer, TElement element) + { + buffer.Push(element); + return buffer; + } + + public static Stack ToResult(Stack buffer) => buffer; +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CollectionConverter.cs b/src/Components/Endpoints/src/Binding/Converters/CollectionConverter.cs new file mode 100644 index 000000000000..4bbe309f394b --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CollectionConverter.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +// The algorithm for collections is always the same. There are two main cases: +// The collection can be modified, so we can add items to it. +// The collection cannot be modified, so we need to use a buffer and copy the items to it once we are done. +// When adding to the buffer, there are two cases: +// The buffer implements ICollection, so we can add items to it. +// The buffer does not implement ICollection, so we need to use custom code to add items to it. +// These aspects are captured in the TCollectionPolicy type parameter. +// Instead of creating a hierachy with virtual members, we are using generics and virtual interface dispatch to achieve the same result. +// This allows us to avoid virtual dispatch at runtime, and enables us to easily adapt to different types of collections. + +internal abstract class CollectionConverter : FormDataConverter +{ +} + +internal class CollectionConverter : CollectionConverter + where TCollectionPolicy : ICollectionBufferAdapter +{ + // Indexes up to 100 are pre-allocated to avoid allocations for common cases. + private static readonly string[] Indexes = new string[] { + "[0]", "[1]", "[2]", "[3]", "[4]", "[5]", "[6]", "[7]", "[8]", "[9]", + "[10]", "[11]", "[12]", "[13]", "[14]", "[15]", "[16]", "[17]", "[18]", "[19]", + "[20]", "[21]", "[22]", "[23]", "[24]", "[25]", "[26]", "[27]", "[28]", "[29]", + "[30]", "[31]", "[32]", "[33]", "[34]", "[35]", "[36]", "[37]", "[38]", "[39]", + "[40]", "[41]", "[42]", "[43]", "[44]", "[45]", "[46]", "[47]", "[48]", "[49]", + "[50]", "[51]", "[52]", "[53]", "[54]", "[55]", "[56]", "[57]", "[58]", "[59]", + "[60]", "[61]", "[62]", "[63]", "[64]", "[65]", "[66]", "[67]", "[68]", "[69]", + "[70]", "[71]", "[72]", "[73]", "[74]", "[75]", "[76]", "[77]", "[78]", "[79]", + "[80]", "[81]", "[82]", "[83]", "[84]", "[85]", "[86]", "[87]", "[88]", "[89]", + "[90]", "[91]", "[92]", "[93]", "[94]", "[95]", "[96]", "[97]", "[98]", "[99]", + }; + + private readonly FormDataConverter _elementConverter; + + public CollectionConverter(FormDataConverter elementConverter) + { + ArgumentNullException.ThrowIfNull(elementConverter); + _elementConverter = elementConverter; + } + + internal override bool TryRead( + ref FormDataReader context, + Type type, + FormDataMapperOptions options, + [NotNullWhen(true)] out TCollection? result, + out bool found) + { + TElement currentElement; + TBuffer buffer; + bool foundCurrentElement; + bool currentElementSuccess; + bool succeded; + // Even though we have indexes, we special case 0 an 1 and use literals directly. We leave them in the indexes + // collection because it makes other indexes align. + context.PushPrefix("[0]"); + succeded = _elementConverter.TryRead(ref context, typeof(TElement), options, out currentElement!, out found); + context.PopPrefix("[0]"); + if (!found) + { + result = default; + return succeded; + } + + // We already know we found an element; + found = true; + // At this point we have at least found one element, we can create the collection and assign it to it. + buffer = TCollectionPolicy.CreateBuffer(); + buffer = TCollectionPolicy.Add(ref buffer, currentElement!); + + // Read element 1 and set conditions to enter the loop + context.PushPrefix("[1]"); + currentElementSuccess = _elementConverter.TryRead(ref context, typeof(TElement), options, out currentElement!, out foundCurrentElement); + succeded = succeded && currentElementSuccess; + context.PopPrefix("[1]"); + // We need to iterate while we keep finding values, even if some of them have errors. This is because we don't want data to be lost just + // because we are not able to parse it. For example, if [5] = "asdf", we don't want to loose the values for [6], [7], etc. that can be + // valid. + // There will be a limit to how many errors we collect, at which point we will stop capturing errors. + // Similarly, over 100 elements, we'll start doing more work to compute the index prefix. We chose 100 because that's the default + // max collection size that we will support. + var index = 2; + for (; index < 100 && foundCurrentElement; index++) + { + // Add the current element + buffer = TCollectionPolicy.Add(ref buffer, currentElement!); + + // Get the precomputed prefix and try and bind the element. + var prefix = Indexes[index]; + context.PushPrefix(prefix); + currentElementSuccess = _elementConverter.TryRead(ref context, typeof(TElement), options, out currentElement!, out foundCurrentElement); + succeded = succeded && currentElementSuccess; + context.PopPrefix(prefix); + } + + if (!foundCurrentElement) + { + result = TCollectionPolicy.ToResult(buffer); + return succeded; + } + + // We have a "large" collection. Loop until we stop finding elements or reach the max collection size. + var maxCollectionSize = options.MaxCollectionSize; + + // We need to compute the prefix for the index, since it's not precomputed. + // The biggest UInt32 representation is 4294967295, which is 10 characters, so 16 chars is more than enough to + // hold the representation. + Span computedPrefix = stackalloc char[16]; + computedPrefix[0] = '['; + + // index is 100 here. + for (; index < maxCollectionSize && foundCurrentElement; index++) + { + // Add the current element + buffer = TCollectionPolicy.Add(ref buffer, currentElement!); + + // We need to compute the prefix for the index, since it's not precomputed. + if (!index.TryFormat(computedPrefix[1..], out var charsWritten, provider: CultureInfo.InvariantCulture)) + { + succeded = false; + break; + } + + computedPrefix[charsWritten + 1] = ']'; + context.PushPrefix(computedPrefix[..(charsWritten + 2)]); + currentElementSuccess = _elementConverter.TryRead(ref context, typeof(TElement), options, out currentElement!, out foundCurrentElement); + succeded = succeded && currentElementSuccess; + context.PopPrefix(computedPrefix[..(charsWritten + 2)]); + } + + if (!foundCurrentElement) + { + result = TCollectionPolicy.ToResult(buffer); + return succeded; + } + else + { + // We reached the max collection size. + buffer = TCollectionPolicy.Add(ref buffer, currentElement!); + result = TCollectionPolicy.ToResult(buffer); + + // Signal failure because we have stopped binding. + // We will include an error in the list of reported errors. + return false; + } + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/CompiledComplexTypeConverter.cs b/src/Components/Endpoints/src/Binding/Converters/CompiledComplexTypeConverter.cs new file mode 100644 index 000000000000..e78c99d18f09 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/CompiledComplexTypeConverter.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal class CompiledComplexTypeConverter(CompiledComplexTypeConverter.ConverterDelegate body) : FormDataConverter +{ + public delegate bool ConverterDelegate(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found); + + internal override bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out T? result, out bool found) => + body(ref context, type, options, out result, out found); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryBufferAdapter.cs new file mode 100644 index 000000000000..0e7939018f10 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryBufferAdapter.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +// Uses a concrete type that implements IDictionary as a buffer. +internal sealed class DictionaryBufferAdapter + : IDictionaryBufferAdapter + where TDictionaryType : IDictionary, new() + where TKey : IParsable +{ + public static TDictionaryType Add(ref TDictionaryType buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static TDictionaryType CreateBuffer() => new TDictionaryType(); + + public static TDictionaryType ToResult(TDictionaryType buffer) => buffer; +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryStaticCastAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryStaticCastAdapter.cs new file mode 100644 index 000000000000..6e722d2be90d --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/DictionaryStaticCastAdapter.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +// Adapts a concrete dictionary type into an interface. +internal sealed class DictionaryStaticCastAdapter + : IDictionaryBufferAdapter + where TDictionaryAdapter : IDictionaryBufferAdapter + where TDictionaryImplementation : TDictionaryInterface + where TKey : IParsable +{ + public static TBuffer CreateBuffer() => TDictionaryAdapter.CreateBuffer(); + + public static TBuffer Add(ref TBuffer buffer, TKey key, TValue element) => TDictionaryAdapter.Add(ref buffer, key, element); + + public static TDictionaryInterface ToResult(TBuffer buffer) => TDictionaryAdapter.ToResult(buffer); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/IDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/IDictionaryBufferAdapter.cs new file mode 100644 index 000000000000..e2b4bc6c8b89 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/IDictionaryBufferAdapter.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +// Interface for constructing a dictionary like instance using the +// dictionary converter. +// This interface abstracts over the different ways of constructing a dictionary. +// For example, Immutable types use a builder as a buffer, while other types +// use an instance of the dictionary itself as a buffer. +internal interface IDictionaryBufferAdapter + where TKey : IParsable +{ + public static abstract TBuffer CreateBuffer(); + + public static abstract TBuffer Add(ref TBuffer buffer, TKey key, TValue value); + + public static abstract TDictionary ToResult(TBuffer buffer); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableDictionaryBufferAdapter.cs new file mode 100644 index 000000000000..4e42ad552e0b --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableDictionaryBufferAdapter.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ImmutableDictionaryBufferAdapter + : IDictionaryBufferAdapter, ImmutableDictionary.Builder, TKey, TValue> + where TKey : IParsable +{ + public static ImmutableDictionary.Builder Add(ref ImmutableDictionary.Builder buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static ImmutableDictionary.Builder CreateBuffer() => ImmutableDictionary.CreateBuilder(); + + public static ImmutableDictionary ToResult(ImmutableDictionary.Builder buffer) => buffer.ToImmutable(); + + internal static DictionaryConverter> CreateInterfaceConverter(FormDataConverter valueTypeConverter) + { + return new DictionaryConverter, + DictionaryStaticCastAdapter< + IImmutableDictionary, + ImmutableDictionary, + ImmutableDictionaryBufferAdapter, + ImmutableDictionary.Builder, + TKey, + TValue>, + ImmutableDictionary.Builder, + TKey, + TValue>(valueTypeConverter); + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableSortedDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableSortedDictionaryBufferAdapter.cs new file mode 100644 index 000000000000..1ebde40f6b9f --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ImmutableSortedDictionaryBufferAdapter.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ImmutableSortedDictionaryBufferAdapter + : IDictionaryBufferAdapter, ImmutableSortedDictionary.Builder, TKey, TValue> + where TKey : IParsable +{ + public static ImmutableSortedDictionary.Builder Add(ref ImmutableSortedDictionary.Builder buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static ImmutableSortedDictionary.Builder CreateBuffer() => ImmutableSortedDictionary.CreateBuilder(); + + public static ImmutableSortedDictionary ToResult(ImmutableSortedDictionary.Builder buffer) => buffer.ToImmutable(); +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ReadOnlyDictionaryBufferAdapter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ReadOnlyDictionaryBufferAdapter.cs new file mode 100644 index 000000000000..6c5215a7caa9 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryAdapters/ReadOnlyDictionaryBufferAdapter.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.ObjectModel; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ReadOnlyDictionaryBufferAdapter + : IDictionaryBufferAdapter, Dictionary, TKey, TValue> + where TKey : IParsable +{ + public static Dictionary Add(ref Dictionary buffer, TKey key, TValue value) + { + buffer.Add(key, value); + return buffer; + } + + public static Dictionary CreateBuffer() => + new Dictionary(); + + public static ReadOnlyDictionary ToResult(Dictionary buffer) => + new ReadOnlyDictionary(buffer); + + internal static DictionaryConverter> CreateInterfaceConverter(FormDataConverter valueTypeConverter) + { + return new DictionaryConverter, + DictionaryStaticCastAdapter< + IReadOnlyDictionary, + ReadOnlyDictionary, + ReadOnlyDictionaryBufferAdapter, + Dictionary, + TKey, + TValue>, + Dictionary, + TKey, + TValue>(valueTypeConverter); + } + + internal static DictionaryConverter> CreateConverter(FormDataConverter valueTypeConverter) + { + return new DictionaryConverter, + ReadOnlyDictionaryBufferAdapter, + Dictionary, + TKey, + TValue>(valueTypeConverter); + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs b/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs new file mode 100644 index 000000000000..cc93af69dbb5 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/DictionaryConverter.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal abstract class DictionaryConverter : FormDataConverter +{ +} + +internal sealed class DictionaryConverter : DictionaryConverter + where TKey : IParsable + where TDictionaryPolicy : IDictionaryBufferAdapter +{ + private readonly FormDataConverter _valueConverter; + + public DictionaryConverter(FormDataConverter elementConverter) + { + ArgumentNullException.ThrowIfNull(elementConverter); + + _valueConverter = elementConverter; + } + + internal override bool TryRead( + ref FormDataReader context, + Type type, + FormDataMapperOptions options, + [NotNullWhen(true)] out TDictionary? result, + out bool found) + { + TValue currentValue; + TBuffer buffer; + bool foundCurrentValue; + bool currentElementSuccess; + bool succeded = true; + + found = context.GetKeys().GetEnumerator().MoveNext(); + if (!found) + { + result = default!; + return true; + } + + buffer = TDictionaryPolicy.CreateBuffer(); + + // We can't precompute dictionary anyKeys ahead of time, + // so the moment we find a dictionary, we request the list of anyKeys + // for the current location, which will involve parsing the form data anyKeys + // and building a tree of anyKeys. + var keyCount = 0; + var maxCollectionSize = options.MaxCollectionSize; + + foreach (var key in context.GetKeys()) + { + context.PushPrefix(key); + currentElementSuccess = _valueConverter.TryRead(ref context, typeof(TValue), options, out currentValue!, out foundCurrentValue); + context.PopPrefix(key); + + if (!TKey.TryParse(key[1..^1], CultureInfo.InvariantCulture, out var keyValue)) + { + succeded = false; + // Will report an error about unparsable key here. + + // Continue trying to bind the rest of the dictionary. + continue; + } + + TDictionaryPolicy.Add(ref buffer, keyValue!, currentValue); + keyCount++; + if (keyCount == maxCollectionSize) + { + succeded = false; + break; + } + } + + result = TDictionaryPolicy.ToResult(buffer); + return succeded; + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/NullableConverter.cs b/src/Components/Endpoints/src/Binding/Converters/NullableConverter.cs new file mode 100644 index 000000000000..5e3b2722d925 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/NullableConverter.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class NullableConverter : FormDataConverter where T : struct +{ + private readonly FormDataConverter _nonNullableConverter; + + public NullableConverter(FormDataConverter nonNullableConverter) + { + _nonNullableConverter = nonNullableConverter; + } + + internal override bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out T? result, out bool found) + { + if (!(_nonNullableConverter.TryRead(ref context, type, options, out var innerResult, out found) && found)) + { + result = null; + return false; + } + else + { + result = innerResult; + return true; + } + } +} diff --git a/src/Components/Endpoints/src/Binding/Converters/ParsableConverter.cs b/src/Components/Endpoints/src/Binding/Converters/ParsableConverter.cs new file mode 100644 index 000000000000..1542f6b0fdb7 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Converters/ParsableConverter.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ParsableConverter : FormDataConverter, ISingleValueConverter where T : IParsable +{ + internal override bool TryRead(ref FormDataReader reader, Type type, FormDataMapperOptions options, out T? result, out bool found) + { + found = reader.TryGetValue(out var value); + if (found && T.TryParse(value, reader.Culture, out result)) + { + return true; + } + else + { + result = default; + return false; + } + } +} diff --git a/src/Components/Endpoints/src/Binding/DefaultFormValuesSupplier.cs b/src/Components/Endpoints/src/Binding/DefaultFormValuesSupplier.cs new file mode 100644 index 000000000000..8f90abaef768 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/DefaultFormValuesSupplier.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using Microsoft.AspNetCore.Components.Binding; +using Microsoft.AspNetCore.Components.Endpoints.Binding; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +internal sealed class DefaultFormValuesSupplier : IFormValueSupplier +{ + private static readonly MethodInfo _method = typeof(DefaultFormValuesSupplier) + .GetMethod( + nameof(DeserializeCore), + BindingFlags.NonPublic | BindingFlags.Static) ?? + throw new InvalidOperationException($"Unable to find method '{nameof(DeserializeCore)}'."); + + private readonly HttpContextFormDataProvider _formData; + private readonly FormDataMapperOptions _options = new(); + private static readonly ConcurrentDictionary, FormDataMapperOptions, string, object>> _cache = + new(); + + public DefaultFormValuesSupplier(FormDataProvider formData) + { + _formData = (HttpContextFormDataProvider)formData; + } + + public bool CanBind(string formName, Type valueType) + { + return _formData.IsFormDataAvailable && + string.Equals(formName, _formData.Name, StringComparison.Ordinal) && + _options.ResolveConverter(valueType) != null; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue) + { + // This will func to a proper binder + if (!CanBind(formName, valueType)) + { + boundValue = null; + return false; + } + + var deserializer = _cache.GetOrAdd(valueType, CreateDeserializer); + + var result = deserializer(_formData.Entries, _options, "value"); + if (result != default) + { + // This is not correct, but works for primtive values. + // Will change the interface when we add support for complex types. + boundValue = result; + return true; + } + + boundValue = valueType.IsValueType ? Activator.CreateInstance(valueType) : null; + return false; + } + + private Func, FormDataMapperOptions, string, object> CreateDeserializer(Type type) => + _method.MakeGenericMethod(type) + .CreateDelegate, FormDataMapperOptions, string, object>>(); + + private static object? DeserializeCore(IReadOnlyDictionary form, FormDataMapperOptions options, string value) + { + // Form values are parsed according to the culture of the request, which is set to the current culture by the localization middleware. + // Some form input types use the invariant culture when sending the data to the server. For those cases, we'll + // provide a way to override the culture to use to parse that value. + var reader = new FormDataReader(form, CultureInfo.CurrentCulture); + reader.PushPrefix(value); + return FormDataMapper.Map(reader, options); + } + + public bool CanConvertSingleValue(Type type) + { + return _options.IsSingleValueConverter(type); + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/CollectionConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/CollectionConverterFactory.cs new file mode 100644 index 000000000000..0a9c17840d0b --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/CollectionConverterFactory.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal class CollectionConverterFactory : IFormDataConverterFactory +{ + public static readonly CollectionConverterFactory Instance = new(); + + public bool CanConvert(Type type, FormDataMapperOptions options) + { + var enumerable = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>)); + if (enumerable == null && !(type.IsArray && type.GetArrayRank() == 1)) + { + return false; + } + + var element = enumerable != null ? enumerable.GetGenericArguments()[0] : type.GetElementType()!; + + if (Activator.CreateInstance(typeof(TypedCollectionConverterFactory<,>) + .MakeGenericType(type, element!)) is not IFormDataConverterFactory factory) + { + return false; + } + + return factory.CanConvert(type, options); + } + + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(options); + + // There is an assumption here that if the type is a bindable collection, it's going to implement + // ICollection and IEnumerable. There could potentially be a type that implements ICollection + // multiple times for different T's explicitly, but that is not something we will support (nor something supported today). + var enumerableType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>)); + var elementType = enumerableType?.GetGenericArguments()[0]; + + // The collection converter heavily relies on generics to adapt to different collection types. + // Since reflection gets a bit tricky with generics, we instead close over the generic collection and + // element types to make it simpler to create the converter. + var factory = Activator.CreateInstance(typeof(TypedCollectionConverterFactory<,>) + .MakeGenericType(type, elementType!)) as IFormDataConverterFactory; + + if (factory == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + return factory.CreateConverter(type, options); + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs new file mode 100644 index 000000000000..667a76ffe3c2 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal class ConcreteTypeCollectionConverterFactory + : IFormDataConverterFactory +{ + public static readonly ConcreteTypeCollectionConverterFactory Instance = + new(); + + public bool CanConvert(Type _, FormDataMapperOptions options) => true; + + public FormDataConverter CreateConverter(Type _, FormDataMapperOptions options) + { + // Resolve the element type converter + var elementTypeConverter = options.ResolveConverter() ?? + throw new InvalidOperationException($"Unable to create converter for '{typeof(TCollection).FullName}'."); + + var customFactory = Activator.CreateInstance(typeof(CustomCollectionConverterFactory<>) + .MakeGenericType(typeof(TCollection), typeof(TElement), typeof(TCollection))) as CustomCollectionConverterFactory; + + if (customFactory == null) + { + throw new InvalidOperationException($"Unable to create converter for type '{typeof(TCollection).FullName}'."); + } + + return customFactory.CreateConverter(elementTypeConverter); + } + + private abstract class CustomCollectionConverterFactory + { + public abstract FormDataConverter CreateConverter(FormDataConverter elementConverter); + } + + private class CustomCollectionConverterFactory : CustomCollectionConverterFactory + where TCustomCollection : TCollection, ICollection, new() + { + public override FormDataConverter CreateConverter(FormDataConverter elementConverter) + { + return new CollectionConverter< + TCustomCollection, + ImplementingCollectionBufferAdapter, + TCustomCollection, + TElement>(elementConverter); + } + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/Collections/TypedCollectionConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Collections/TypedCollectionConverterFactory.cs new file mode 100644 index 000000000000..e3b922555748 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/Collections/TypedCollectionConverterFactory.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Collections.ObjectModel; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal abstract class TypedCollectionConverterFactory : IFormDataConverterFactory +{ + public abstract bool CanConvert(Type type, FormDataMapperOptions options); + + public abstract FormDataConverter CreateConverter(Type type, FormDataMapperOptions options); +} + +internal sealed class TypedCollectionConverterFactory : TypedCollectionConverterFactory +{ + public override bool CanConvert(Type _, FormDataMapperOptions options) + { + // Resolve the element type converter + var elementTypeConverter = options.ResolveConverter(); + + if (elementTypeConverter == null) + { + return false; + } + + // Arrays + var type = typeof(TCollection); + if (type.IsArray && type.GetArrayRank() == 1) + { + return true; + } + + if (!type.IsInterface && !type.IsAbstract && !type.IsGenericTypeDefinition) + { + return type switch + { + // Special collections + var _ when type == (typeof(Queue)) => true, + var _ when type == (typeof(Stack)) => true, + var _ when type == (typeof(ReadOnlyCollection)) => true, + + // Concurrent collections + var _ when type == (typeof(ConcurrentBag)) => true, + var _ when type == (typeof(ConcurrentStack)) => true, + var _ when type == (typeof(ConcurrentQueue)) => true, + + // Immutable collections + var _ when type == (typeof(ImmutableArray)) => true, + var _ when type == (typeof(ImmutableList)) => true, + var _ when type == (typeof(ImmutableHashSet)) => true, + var _ when type == (typeof(ImmutableSortedSet)) => true, + var _ when type == (typeof(ImmutableQueue)) => true, + var _ when type == (typeof(ImmutableStack)) => true, + + // Some of the types above implement ICollection, but do so in a very inneficient way, so we want to + // use special converters for them. + var _ when type.IsAssignableTo(typeof(ICollection)) && type.GetConstructor(Type.EmptyTypes) != null => true, + _ => false + }; + } + + if (type.IsInterface) + { + // At this point we are dealing with an interface. We test from the most specific to the least specific + // to find the best fit for the well-known set of interfaces we support. + return type switch + { + // System.Collections.Immutable + var _ when type == (typeof(IImmutableSet)) => true, + var _ when type == (typeof(IImmutableList)) => true, + var _ when type == (typeof(IImmutableQueue)) => true, + var _ when type == (typeof(IImmutableStack)) => true, + + // System.Collections.Generics + var _ when type == (typeof(IReadOnlySet)) => true, + var _ when type == (typeof(IReadOnlyList)) => true, + var _ when type == (typeof(IReadOnlyCollection)) => true, + var _ when type == (typeof(ISet)) => true, + var _ when type == (typeof(IList)) => true, + var _ when type == (typeof(ICollection)) => true, + + // Leave IEnumerable to last, since it's the least specific. + var _ when type == (typeof(IEnumerable)) => true, + + _ => throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."), + }; + + } + return false; + } + + // There are four patterns that we support: + // * The collection is an array: We use an array pool to buffer the elements and then create the final array. + // * The collection is a concrete type that implements ICollection and has a public parameterless constructor: + // We create an instance of that type as the buffer and add the elements to it directly. + // * The collection is a well-known type that we have an adapter for: Queue, Stack, ReadOnlyCollection, + // ImmutableArray, etc. We use a specific adapter tailored for that type. For example, for Queue we use + // the Queue directly as the buffer (queues don't implement ICollection, so the adapter uses Push instead), + // or for ImmutableXXX we either use ImmuttableXXX.CreateBuilder to create a builder we use as a buffer, + // or collect the collection into an array buffer and call CreateRange to build the final collection. + public override FormDataConverter CreateConverter(Type _, FormDataMapperOptions options) + { + // Resolve the element type converter + var elementTypeConverter = options.ResolveConverter() ?? + throw new InvalidOperationException($"Unable to create converter for '{typeof(TCollection).FullName}'."); + + // Arrays + var type = typeof(TCollection); + if (type.IsArray && type.GetArrayRank() == 1) + { + return new CollectionConverter< + TElement[], + ArrayPoolBufferAdapter, TElement>, + ArrayPoolBufferAdapter, TElement>.PooledBuffer, + TElement>(elementTypeConverter); + } + + if (!type.IsInterface && !type.IsAbstract && !type.IsGenericTypeDefinition) + { + return type switch + { + // Special collections + var _ when type.IsAssignableTo(typeof(Queue)) => + new CollectionConverter, QueueBufferAdapter, Queue, TElement>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(Stack)) => + new CollectionConverter, StackBufferAdapter, Stack, TElement>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(ReadOnlyCollection)) => + new CollectionConverter, ReadOnlyCollectionBufferAdapter, IList, TElement>(elementTypeConverter), + + // Concurrent collections + var _ when type.IsAssignableTo(typeof(ConcurrentBag)) => + new CollectionConverter, ConcurrentBagBufferAdapter, ConcurrentBag, TElement>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(ConcurrentStack)) => + new CollectionConverter, ConcurrentStackBufferAdapter, ConcurrentStack, TElement>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(ConcurrentQueue)) => + new CollectionConverter, ConcurrentQueueBufferAdapter, ConcurrentQueue, TElement>(elementTypeConverter), + + // Immutable collections + var _ when type.IsAssignableTo(typeof(ImmutableArray)) => + new CollectionConverter, ImmutableArrayBufferAdapter, ImmutableArray.Builder, TElement>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(ImmutableList)) => + new CollectionConverter, ImmutableListBufferAdapter, ImmutableList.Builder, TElement>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(ImmutableHashSet)) => + new CollectionConverter, ImmutableHashSetBufferAdapter, ImmutableHashSet.Builder, TElement>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(ImmutableSortedSet)) => + new CollectionConverter, ImmutableSortedSetBufferAdapter, ImmutableSortedSet.Builder, TElement>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(ImmutableQueue)) => + new CollectionConverter, ImmutableQueueBufferAdapter, ImmutableQueueBufferAdapter.PooledBuffer, TElement>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(ImmutableStack)) => + new CollectionConverter, ImmutableStackBufferAdapter, ImmutableStackBufferAdapter.PooledBuffer, TElement>(elementTypeConverter), + + // Some of the types above implement ICollection, but do so in a very inneficient way, so we want to + // use special converters for them. + var _ when type.IsAssignableTo(typeof(ICollection)) + => ConcreteTypeCollectionConverterFactory.Instance.CreateConverter(typeof(TCollection), options), + _ => throw new InvalidOperationException($"Unable to create converter for '{typeof(TCollection).FullName}'.") + }; + } + + if (type.IsInterface) + { + // At this point we are dealing with an interface. We test from the most specific to the least specific + // to find the best fit for the well-known set of interfaces we support. + return type switch + { + // System.Collections.Immutable + var _ when type.IsAssignableTo(typeof(IImmutableSet)) => + ImmutableHashSetBufferAdapter.CreateInterfaceConverter(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(IImmutableList)) => + ImmutableListBufferAdapter.CreateInterfaceConverter(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(IImmutableQueue)) => + ImmutableQueueBufferAdapter.CreateInterfaceConverter(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(IImmutableStack)) => + ImmutableStackBufferAdapter.CreateInterfaceConverter(elementTypeConverter), + + // System.Collections.Generics + var _ when type.IsAssignableTo(typeof(IReadOnlySet)) => + CreateConverter, HashSet>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(IReadOnlyList)) => + CreateConverter, List>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(IReadOnlyCollection)) => + CreateConverter, List>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(ISet)) => + CreateConverter, HashSet>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(IList)) => + CreateConverter, List>(elementTypeConverter), + var _ when type.IsAssignableTo(typeof(ICollection)) => + CreateConverter, List>(elementTypeConverter), + + // Leave IEnumerable to last, since it's the least specific. + var _ when type.IsAssignableTo(typeof(IEnumerable)) => + CreateConverter, List>(elementTypeConverter), + + _ => throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."), + }; + } + + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + + static FormDataConverter CreateConverter(FormDataConverter elementTypeConverter) + where TInterface : IEnumerable + where TImplementation : TInterface, ICollection, new() + { + return new CollectionConverter< + TInterface, + ImplementingCollectionBufferAdapter, + TImplementation, + TElement>(elementTypeConverter); + } + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs new file mode 100644 index 000000000000..68d22f323d09 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal abstract class ComplexTypeExpressionConverterFactory +{ + internal abstract FormDataConverter CreateConverter(Type type, FormDataMapperOptions options); +} diff --git a/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs new file mode 100644 index 000000000000..4b87ce6accff --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq.Expressions; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ComplexTypeExpressionConverterFactory : ComplexTypeExpressionConverterFactory +{ + internal override CompiledComplexTypeConverter CreateConverter(Type type, FormDataMapperOptions options) + { + var body = CreateConverterBody(type, options); + return new CompiledComplexTypeConverter(body); + } + + private CompiledComplexTypeConverter.ConverterDelegate CreateConverterBody(Type type, FormDataMapperOptions options) + { + var properties = PropertyHelper.GetVisibleProperties(type); + + var (readerParam, typeParam, optionsParam, resultParam, foundValueParam) = CreateFormDataConverterParameters(); + var parameters = new List() { readerParam, typeParam, optionsParam, resultParam, foundValueParam }; + + // Variables + var propertyFoundValue = Expression.Variable(typeof(bool), "foundValueForProperty"); + var succeeded = Expression.Variable(typeof(bool), "succeeded"); + var localFoundValueVar = Expression.Variable(typeof(bool), "localFoundValue"); + + var variables = new List() { propertyFoundValue, succeeded, localFoundValueVar }; + var propertyLocals = new List(); + + var body = new List() + { + Expression.Assign(succeeded, Expression.Constant(true)), + }; + + // Create the property blocks + + // var propertyConverter = options.ResolveConverter(typeof(string)); + // reader.PushPrefix("Property"); + // succeeded &= propertyConverter.TryRead(ref reader, typeof(string), options, out propertyVar, out foundProperty); + // found ||= foundProperty; + // reader.PopPrefix("Property"); + for (var i = 0; i < properties.Length; i++) + { + // Declare variable for the converter + var property = properties[i].Property; + var propertyConverterType = typeof(FormDataConverter<>).MakeGenericType(property.PropertyType); + var propertyConverterVar = Expression.Variable(propertyConverterType, $"{property.Name}Converter"); + variables.Add(propertyConverterVar); + + // Declare variable for property value. + var propertyVar = Expression.Variable(property.PropertyType, property.Name); + propertyLocals.Add(propertyVar); + + // Resolve and assign converter + + // Create the block to try and map the property and update variables. + // returnParam &= { PushPrefix(property.Name); var res = TryRead(...); PopPrefix(...); return res; } + // var propertyConverter = options.ResolveConverter()); + var propertyConverter = Expression.Assign( + propertyConverterVar, + Expression.Call( + optionsParam, + nameof(FormDataMapperOptions.ResolveConverter), + new[] { property.PropertyType }, + Array.Empty())); + body.Add(propertyConverter); + + // reader.PushPrefix("Property"); + body.Add(Expression.Call( + readerParam, + nameof(FormDataReader.PushPrefix), + Array.Empty(), + Expression.Constant(property.Name))); + + // succeeded &= propertyConverter.TryRead(ref reader, typeof(string), options, out propertyVar, out foundProperty); + var callTryRead = Expression.AndAssign( + succeeded, + Expression.Call( + propertyConverterVar, + nameof(FormDataConverter.TryRead), + Type.EmptyTypes, + readerParam, + typeParam, + optionsParam, + propertyVar, + propertyFoundValue)); + body.Add(callTryRead); + + // reader.PopPrefix("Property"); + body.Add(Expression.Call( + readerParam, + nameof(FormDataReader.PopPrefix), + Array.Empty(), + Expression.Constant(property.Name))); + + body.Add(Expression.OrAssign(localFoundValueVar, propertyFoundValue)); + } + + body.Add(Expression.IfThen( + localFoundValueVar, + Expression.Block(CreateInstanceAndAssignProperties(type, resultParam, properties, propertyLocals)))); + + // foundValue && !failures; + + body.Add(Expression.Assign(foundValueParam, localFoundValueVar)); + body.Add(succeeded); + + variables.AddRange(propertyLocals); + + return CreateConverterFunction(parameters, variables, body); + + static IEnumerable CreateInstanceAndAssignProperties( + Type model, + ParameterExpression resultParam, + PropertyHelper[] props, + List variables) + { + if (!model.IsValueType) + { + yield return Expression.Assign(resultParam, Expression.New(model)); + } + + for (var i = 0; i < props.Length; i++) + { + yield return Expression.Assign(Expression.Property(resultParam, props[i].Property), variables[i]); + } + } + } + + private static CompiledComplexTypeConverter.ConverterDelegate CreateConverterFunction( + List parameters, + List variables, + List body) + { + var lambda = Expression.Lambda.ConverterDelegate>( + Expression.Block(variables, body), + parameters); + + return lambda.Compile(); + } + + private static FormDataConverterReadParameters CreateFormDataConverterParameters() + { + return new( + Expression.Parameter(typeof(FormDataReader).MakeByRefType(), "reader"), + Expression.Parameter(typeof(Type), "type"), + Expression.Parameter(typeof(FormDataMapperOptions), "options"), + Expression.Parameter(typeof(T).MakeByRefType(), "result"), + Expression.Parameter(typeof(bool).MakeByRefType(), "foundValue")); + } + + private record struct FormDataConverterReadParameters( + ParameterExpression ReaderParam, + ParameterExpression TypeParam, + ParameterExpression OptionsParam, + ParameterExpression ResultParam, + ParameterExpression FoundValueParam); +} diff --git a/src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs new file mode 100644 index 000000000000..f633aca53e1e --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +// This factory is registered last, which means, dictionaries and collections, have already +// been processed by the time we get here. +internal class ComplexTypeConverterFactory : IFormDataConverterFactory +{ + internal static readonly ComplexTypeConverterFactory Instance = new(); + + public bool CanConvert(Type type, FormDataMapperOptions options) + { + if (type.GetConstructor(Type.EmptyTypes) == null && !type.IsValueType) + { + // For right now, require a public parameterless constructor. + return false; + } + if (type.IsGenericTypeDefinition) + { + return false; + } + + // Check that all properties have a valid converter. + var propertyHelper = PropertyHelper.GetVisibleProperties(type); + foreach (var helper in propertyHelper) + { + if (options.ResolveConverter(helper.Property.PropertyType) == null) + { + return false; + } + } + + return true; + } + + // We are going to compile a function that maps all the properties for the type. + // Beware that the code below is not the actual exact code, just a simplification to understand what is happening at a high level. + // The general flow is as follows. For a type like Address { Street, City, Country, ZipCode } + // we will generate a function that looks like: + // public bool TryRead(ref FormDataReader reader, Type type, FormDataSerializerOptions options, out Address? result, out bool found) + // { + // bool foundProperty; + // bool succeeded = true; + // string street; + // string city; + // string country; + // string zipCode; + // FormDataConveter streetConverter; + // FormDataConveter cityConverter; + // FormDataConveter countryConverter; + // FormDataConveter zipCodeConverter; + + // var streetConverter = options.ResolveConverter(typeof(string)); + // reader.PushPrefix("Street"); + // succeeded &= streetConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty); + // found ||= foundProperty; + // reader.PopPrefix("Street"); + // + // var cityConverter = options.ResolveConverter(typeof(string)); + // reader.PushPrefix("City"); + // succeeded &= ciryConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty); + // found ||= foundProperty; + // reader.PopPrefix("City"); + // + // var countryConverter = options.ResolveConverter(typeof(string)); + // reader.PushPrefix("Country"); + // succeeded &= countryConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty); + // found ||= foundProperty; + // reader.PopPrefix("Country"); + // + // var zipCodeConverter = options.ResolveConverter(typeof(string)); + // reader.PushPrefix("ZipCode"); + // succeeded &= zipCodeConverter.TryRead(ref reader, typeof(string), options, out street, out foundProperty); + // found ||= foundProperty; + // reader.PopPrefix("ZipCode"); + // + // if(found) + // { + // result = new Address(); + // result.Street = street; + // result.City = city; + // result.Country = country; + // result.ZipCode = zipCode; + // } + // else + // { + // result = null; + // } + // + // return succeeded; + // } + // + // The actual blocks above are going to be generated using System.Linq.Expressions. + // Instead of resolving the property converters every time, we might consider caching the converters in a dictionary and passing an + // extra parameter to the function with them in it. + // The final converter is something like + // internal class CompiledComplexTypeConverter + // (ConverterDelegate converterFunc) + // { + // public bool TryRead(ref FormDataReader reader, Type type, FormDataSerializerOptions options, out object? result, out bool found) + // { + // return converterFunc(ref reader, type, options, out result, out found); + // } + // } + + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) + { + if (Activator.CreateInstance(typeof(ComplexTypeExpressionConverterFactory<>).MakeGenericType(type)) + is not ComplexTypeExpressionConverterFactory factory) + { + throw new InvalidOperationException($"Could not create a converter factory for type {type}."); + } + + return factory.CreateConverter(type, options); + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs new file mode 100644 index 000000000000..a3f77d3600c6 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ConcreteTypeDictionaryConverterFactory : IFormDataConverterFactory + where TKey : IParsable +{ + public static readonly ConcreteTypeDictionaryConverterFactory Instance = new(); + + public bool CanConvert(Type type, FormDataMapperOptions options) => true; + + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) + { + // Resolve the element type converter + var keyConverter = options.ResolveConverter() ?? + throw new InvalidOperationException($"Unable to create converter for '{typeof(TDictionary).FullName}'."); + + var valueConverter = options.ResolveConverter() ?? + throw new InvalidOperationException($"Unable to create converter for '{typeof(TDictionary).FullName}'."); + + var customFactory = Activator.CreateInstance(typeof(CustomDictionaryConverterFactory<>) + .MakeGenericType(typeof(TDictionary), typeof(TKey), typeof(TValue), typeof(TDictionary))) as CustomDictionaryConverterFactory; + + if (customFactory == null) + { + throw new InvalidOperationException($"Unable to create converter for type '{typeof(TDictionary).FullName}'."); + } + + return customFactory.CreateConverter(keyConverter, valueConverter); + } + + private abstract class CustomDictionaryConverterFactory + { + public abstract FormDataConverter CreateConverter(FormDataConverter keyConverter, FormDataConverter valueConverter); + } + + private class CustomDictionaryConverterFactory : CustomDictionaryConverterFactory + where TCustomDictionary : TDictionary, IDictionary, new() + { + public override FormDataConverter CreateConverter(FormDataConverter keyConverter, FormDataConverter valueConverter) + { + return new DictionaryConverter< + TCustomDictionary, + DictionaryBufferAdapter, + TCustomDictionary, + TKey, + TValue>(valueConverter); + } + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs new file mode 100644 index 000000000000..a2822b4b9364 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Collections.ObjectModel; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class TypedDictionaryConverterFactory : IFormDataConverterFactory + where TKey : IParsable +{ + public bool CanConvert(Type type, FormDataMapperOptions options) + { + // Resolve the value type converter + var valueTypeConverter = options.ResolveConverter(); + if (valueTypeConverter == null) + { + return false; + } + + var keyTypeConverter = options.ResolveConverter(); + if (keyTypeConverter == null) + { + return false; + } + + if (type.IsInterface) + { + // At this point we are dealing with an interface. We test from the most specific to the least specific + // to find the best fit for the well-known set of interfaces we support. + return type switch + { + // System.Collections.Immutable + var _ when type == (typeof(IImmutableDictionary)) => true, + + // System.Collections.Generics + var _ when type == (typeof(IReadOnlyDictionary)) => true, + var _ when type == (typeof(IDictionary)) => true, + + _ => throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."), + }; + } + + if (!type.IsAbstract && !type.IsGenericTypeDefinition) + { + return type switch + { + // Immutable collections + var _ when type == (typeof(ImmutableDictionary)) => true, + var _ when type == (typeof(ImmutableSortedDictionary)) => true, + + // Concurrent collections + var _ when type == (typeof(ConcurrentDictionary)) => true, + + // Generic collections + var _ when type == (typeof(SortedList)) => true, + var _ when type == (typeof(SortedDictionary)) => true, + var _ when type == (typeof(Dictionary)) => true, + + var _ when type == (typeof(ReadOnlyDictionary)) => true, + + // Some of the types above implement IDictionary, but do so in a very inneficient way, so we want to + // use special converters for them. + var _ when type.IsAssignableTo(typeof(IDictionary)) && type.GetConstructor(Type.EmptyTypes) != null => true, + _ => false + }; + } + + return false; + } + + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) + { + // Resolve the value type converter + var valueTypeConverter = options.ResolveConverter(); + if (valueTypeConverter == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + if (type.IsInterface) + { + // At this point we are dealing with an interface. We test from the most specific to the least specific + // to find the best fit for the well-known set of interfaces we support. + return type switch + { + // System.Collections.Immutable + var _ when type == (typeof(IImmutableDictionary)) => + ImmutableDictionaryBufferAdapter.CreateInterfaceConverter(valueTypeConverter), + // System.Collections.Generics + var _ when type == (typeof(IReadOnlyDictionary)) => + ReadOnlyDictionaryBufferAdapter.CreateInterfaceConverter(valueTypeConverter), + var _ when type == (typeof(IDictionary)) => + new DictionaryConverter, + DictionaryStaticCastAdapter< + IDictionary, + Dictionary, + DictionaryBufferAdapter, TKey, TValue>, + Dictionary, + TKey, + TValue>, + Dictionary, TKey, TValue>(valueTypeConverter), + + _ => throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."), + }; + } + + if (!type.IsAbstract && !type.IsGenericTypeDefinition) + { + return type switch + { + var _ when type == (typeof(ReadOnlyDictionary)) => + ReadOnlyDictionaryBufferAdapter.CreateConverter(valueTypeConverter), + // Immutable collections + var _ when type == (typeof(ImmutableDictionary)) => + new DictionaryConverter< + ImmutableDictionary, + ImmutableDictionaryBufferAdapter, + ImmutableDictionary.Builder, + TKey, + TValue>(valueTypeConverter), + var _ when type == (typeof(ImmutableSortedDictionary)) => + new DictionaryConverter< + ImmutableSortedDictionary, + ImmutableSortedDictionaryBufferAdapter, + ImmutableSortedDictionary.Builder, + TKey, + TValue>(valueTypeConverter), + + // Concurrent collections + var _ when type == (typeof(ConcurrentDictionary)) => + ConcreteTypeDictionaryConverterFactory, TKey, TValue>.Instance.CreateConverter(type, options), + + // Generic collections + var _ when type == (typeof(SortedList)) => + ConcreteTypeDictionaryConverterFactory, TKey, TValue>.Instance.CreateConverter(type, options), + var _ when type == (typeof(SortedDictionary)) => + ConcreteTypeDictionaryConverterFactory, TKey, TValue>.Instance.CreateConverter(type, options), + var _ when type == (typeof(Dictionary)) => + ConcreteTypeDictionaryConverterFactory, TKey, TValue>.Instance.CreateConverter(type, options), + + // Some of the types above implement IDictionary, but do so in a very inneficient way, so we want to + // use special converters for them. + var _ when type.IsAssignableTo(typeof(IDictionary)) && type.GetConstructor(Type.EmptyTypes) != null => + ConcreteTypeDictionaryConverterFactory.Instance.CreateConverter(type, options), + _ => throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."), + }; + } + + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs new file mode 100644 index 000000000000..ee92552e357d --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal class DictionaryConverterFactory : IFormDataConverterFactory +{ + internal static readonly DictionaryConverterFactory Instance = new(); + + public bool CanConvert(Type type, FormDataMapperOptions options) + { + // Type must implement IDictionary IReadOnlyDictionary + // Note that IDictionary doesn't extend IReadOnlyDictionary, hence the need for two checks + var dictionaryType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IDictionary<,>)) ?? + ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IReadOnlyDictionary<,>)); + + if (dictionaryType == null) + { + return false; + } + + // Key type must implement IParsable + var keyType = dictionaryType?.GetGenericArguments()[0]; + if (keyType == null) + { + return false; + } + + var parsableKeyType = ClosedGenericMatcher.ExtractGenericInterface(keyType, typeof(IParsable<>)); + if (parsableKeyType == null) + { + return false; + } + + // Value must have a converter + var valueType = dictionaryType?.GetGenericArguments()[1]; + if (valueType == null) + { + return false; + } + + var converter = options.ResolveConverter(valueType); + if (converter == null) + { + return false; + } + + var factory = Activator.CreateInstance(typeof(TypedDictionaryConverterFactory<,,>) + .MakeGenericType(type, keyType, valueType)) as IFormDataConverterFactory; + + if (factory == null) + { + return false; + } + + return factory.CanConvert(type, options); + } + + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) + { + // Type must implement IDictionary IReadOnlyDictionary + // Note that IDictionary doesn't extend IReadOnlyDictionary, hence the need for two checks + var dictionaryType = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IDictionary<,>)) ?? + ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IReadOnlyDictionary<,>)); + if (dictionaryType == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + // Key type must implement IParsable + var keyType = dictionaryType?.GetGenericArguments()[0]; + if (keyType == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + var parsableKeyType = ClosedGenericMatcher.ExtractGenericInterface(keyType, typeof(IParsable<>)); + if (parsableKeyType == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + // Value must have a converter + var valueType = dictionaryType?.GetGenericArguments()[1]; + if (valueType == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + var converter = options.ResolveConverter(valueType); + if (converter == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + var factory = Activator.CreateInstance(typeof(TypedDictionaryConverterFactory<,,>) + .MakeGenericType(type, keyType, valueType)) as IFormDataConverterFactory; + + if (factory == null) + { + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } + + return factory.CreateConverter(type, options); + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/NullableConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/NullableConverterFactory.cs new file mode 100644 index 000000000000..ea635aab6cb4 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/NullableConverterFactory.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class NullableConverterFactory : IFormDataConverterFactory +{ + public static readonly NullableConverterFactory Instance = new(); + + public bool CanConvert(Type type, FormDataMapperOptions options) + { + var underlyingType = Nullable.GetUnderlyingType(type); + return underlyingType != null && options.ResolveConverter(underlyingType) != null; + } + + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) + { + var underlyingType = Nullable.GetUnderlyingType(type); + Debug.Assert(underlyingType != null); + + var underlyingConverter = options.ResolveConverter(underlyingType); + Debug.Assert(underlyingConverter != null); + + var expectedConverterType = typeof(NullableConverter<>).MakeGenericType(underlyingType); + Debug.Assert(expectedConverterType != null); + + return Activator.CreateInstance(expectedConverterType, underlyingConverter) as FormDataConverter ?? + throw new InvalidOperationException($"Unable to create converter for type '{type}'."); + } +} diff --git a/src/Components/Endpoints/src/Binding/Factories/ParsableConverterFactory.cs b/src/Components/Endpoints/src/Binding/Factories/ParsableConverterFactory.cs new file mode 100644 index 000000000000..73cc5fccf1b7 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/Factories/ParsableConverterFactory.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class ParsableConverterFactory : IFormDataConverterFactory +{ + public static readonly ParsableConverterFactory Instance = new(); + + public bool CanConvert(Type type, FormDataMapperOptions options) + { + return ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IParsable<>)) is not null; + } + + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) + { + return Activator.CreateInstance(typeof(ParsableConverter<>).MakeGenericType(type)) as FormDataConverter ?? + throw new InvalidOperationException($"Unable to create converter for '{type.FullName}'."); + } +} diff --git a/src/Components/Endpoints/src/Binding/FormDataConverter.cs b/src/Components/Endpoints/src/Binding/FormDataConverter.cs new file mode 100644 index 000000000000..9ff55226c19d --- /dev/null +++ b/src/Components/Endpoints/src/Binding/FormDataConverter.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +// Base type for all types that can map from form data to a .NET type. +internal class FormDataConverter +{ +} diff --git a/src/Components/Endpoints/src/Binding/FormDataConverterOfT.cs b/src/Components/Endpoints/src/Binding/FormDataConverterOfT.cs new file mode 100644 index 000000000000..4d1431bb5455 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/FormDataConverterOfT.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal abstract class FormDataConverter : FormDataConverter +{ + internal abstract bool TryRead(ref FormDataReader context, Type type, FormDataMapperOptions options, out T? result, out bool found); +} diff --git a/src/Components/Endpoints/src/Binding/FormDataMapper.cs b/src/Components/Endpoints/src/Binding/FormDataMapper.cs new file mode 100644 index 000000000000..c9b4b6c888f8 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/FormDataMapper.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal static class FormDataMapper +{ + public static T? Map( + FormDataReader reader, + FormDataMapperOptions options) + { + try + { + var converter = options.ResolveConverter(); + if (converter.TryRead(ref reader, typeof(T), options, out var result, out _)) + { + return result; + } + + // Always return the result, even if it has failures. This is because we do not want + // to loose the data that we were able to deserialize. + return result; + } + finally + { + } + } +} diff --git a/src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs b/src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs new file mode 100644 index 000000000000..f678a579ff0e --- /dev/null +++ b/src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal sealed class FormDataMapperOptions +{ + private readonly ConcurrentDictionary _converters = new(); + private readonly List> _factories = new(); + + public FormDataMapperOptions() + { + _converters = new(WellKnownConverters.Converters); + + _factories.Add((type, options) => ParsableConverterFactory.Instance.CanConvert(type, options) ? ParsableConverterFactory.Instance.CreateConverter(type, options) : null); + _factories.Add((type, options) => NullableConverterFactory.Instance.CanConvert(type, options) ? NullableConverterFactory.Instance.CreateConverter(type, options) : null); + _factories.Add((type, options) => DictionaryConverterFactory.Instance.CanConvert(type, options) ? DictionaryConverterFactory.Instance.CreateConverter(type, options) : null); + _factories.Add((type, options) => CollectionConverterFactory.Instance.CanConvert(type, options) ? CollectionConverterFactory.Instance.CreateConverter(type, options) : null); + _factories.Add((type, options) => ComplexTypeConverterFactory.Instance.CanConvert(type, options) ? ComplexTypeConverterFactory.Instance.CreateConverter(type, options) : null); + } + + // Not configurable for now, this is the max number of elements we will bind. This is important for + // security reasons, as we don't want to bind a huge collection and cause perf issues. + // Some examples of this are: + // Binding to collection using hashes, where the payload can be crafted to force the worst case on insertion + // which is O(n). + internal int MaxCollectionSize = 100; + + internal bool HasConverter(Type valueType) => _converters.ContainsKey(valueType); + + internal bool IsSingleValueConverter(Type type) + { + return _converters.TryGetValue(type, out var converter) && + converter is ISingleValueConverter; + } + + internal FormDataConverter ResolveConverter() + { + return (FormDataConverter)_converters.GetOrAdd(typeof(T), CreateConverter, this); + } + + private static FormDataConverter CreateConverter(Type type, FormDataMapperOptions options) + { + FormDataConverter? converter; + foreach (var factory in options._factories) + { + converter = factory(type, options); + if (converter != null) + { + return converter; + } + } + + throw new InvalidOperationException($"No converter registered for type '{type.FullName}'."); + } + + internal FormDataConverter ResolveConverter(Type type) + { + return _converters.GetOrAdd(type, CreateConverter, this); + } + + // For testing + internal void AddConverter(FormDataConverter converter) + { + _converters[typeof(T)] = converter; + } +} diff --git a/src/Components/Endpoints/src/Binding/FormDataReader.cs b/src/Components/Endpoints/src/Binding/FormDataReader.cs new file mode 100644 index 000000000000..e2f656af3736 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/FormDataReader.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal struct FormDataReader +{ + private readonly IReadOnlyDictionary _formCollection; + private string _prefix; + + public FormDataReader(IReadOnlyDictionary formCollection, CultureInfo culture) + { + _formCollection = formCollection; + _prefix = ""; + Culture = culture; + } + + public IFormatProvider Culture { get; internal set; } + + internal IEnumerable GetKeys() + { + return _formCollection.Keys; + } + + internal void PopPrefix(string _) + { + // For right now, we don't need to do anything with the prefix + _prefix = ""; + } + + internal void PopPrefix(ReadOnlySpan _) + { + // For right now, we don't need to do anything with the prefix + _prefix = ""; + } + + internal void PushPrefix(string prefix) + { + _prefix = prefix; + } + + internal void PushPrefix(ReadOnlySpan prefix) + { + // For right now, just make it a string + _prefix = prefix.ToString(); + } + + internal readonly bool TryGetValue([NotNullWhen(true)] out string? value) + { + if (_formCollection.TryGetValue(_prefix, out var rawValue) && rawValue.Count == 1) + { + value = rawValue[0]!; + return true; + } + else + { + value = null; + return false; + } + } +} diff --git a/src/Components/Endpoints/src/Binding/IFormDataConverterFactory.cs b/src/Components/Endpoints/src/Binding/IFormDataConverterFactory.cs new file mode 100644 index 000000000000..04a77cf83c49 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/IFormDataConverterFactory.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal interface IFormDataConverterFactory +{ + public bool CanConvert(Type type, FormDataMapperOptions options); + + public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options); +} diff --git a/src/Components/Endpoints/src/Binding/ISingleValueConverter.cs b/src/Components/Endpoints/src/Binding/ISingleValueConverter.cs new file mode 100644 index 000000000000..dd1a8467c4a3 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/ISingleValueConverter.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal interface ISingleValueConverter +{ +} diff --git a/src/Components/Endpoints/src/Binding/WellKnownConverters.cs b/src/Components/Endpoints/src/Binding/WellKnownConverters.cs new file mode 100644 index 000000000000..93031c02d958 --- /dev/null +++ b/src/Components/Endpoints/src/Binding/WellKnownConverters.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +internal static class WellKnownConverters +{ + public static readonly IReadOnlyDictionary Converters; + +#pragma warning disable CA1810 // Initialize reference type static fields inline + static WellKnownConverters() +#pragma warning restore CA1810 // Initialize reference type static fields inline + { + var converters = new Dictionary + { + // For the most common types, we avoid going through the factories and just + // create the converters directly. This is a performance optimization. + { typeof(string), new ParsableConverter() }, + { typeof(char), new ParsableConverter() }, + { typeof(bool), new ParsableConverter() }, + { typeof(byte), new ParsableConverter() }, + { typeof(sbyte), new ParsableConverter() }, + { typeof(ushort), new ParsableConverter() }, + { typeof(uint), new ParsableConverter() }, + { typeof(ulong), new ParsableConverter() }, + { typeof(Int128), new ParsableConverter() }, + { typeof(short), new ParsableConverter() }, + { typeof(int), new ParsableConverter() }, + { typeof(long), new ParsableConverter() }, + { typeof(UInt128), new ParsableConverter() }, + { typeof(Half), new ParsableConverter() }, + { typeof(float), new ParsableConverter() }, + { typeof(double), new ParsableConverter() }, + { typeof(decimal), new ParsableConverter() }, + { typeof(DateOnly), new ParsableConverter() }, + { typeof(DateTime), new ParsableConverter() }, + { typeof(DateTimeOffset), new ParsableConverter() }, + { typeof(TimeSpan), new ParsableConverter() }, + { typeof(TimeOnly), new ParsableConverter() }, + { typeof(Guid), new ParsableConverter() } + }; + + converters.Add(typeof(char?), new NullableConverter((FormDataConverter)converters[typeof(char)])); + converters.Add(typeof(bool?), new NullableConverter((FormDataConverter)converters[typeof(bool)])); + converters.Add(typeof(byte?), new NullableConverter((FormDataConverter)converters[typeof(byte)])); + converters.Add(typeof(sbyte?), new NullableConverter((FormDataConverter)converters[typeof(sbyte)])); + converters.Add(typeof(ushort?), new NullableConverter((FormDataConverter)converters[typeof(ushort)])); + converters.Add(typeof(uint?), new NullableConverter((FormDataConverter)converters[typeof(uint)])); + converters.Add(typeof(ulong?), new NullableConverter((FormDataConverter)converters[typeof(ulong)])); + converters.Add(typeof(Int128?), new NullableConverter((FormDataConverter)converters[typeof(Int128)])); + converters.Add(typeof(short?), new NullableConverter((FormDataConverter)converters[typeof(short)])); + converters.Add(typeof(int?), new NullableConverter((FormDataConverter)converters[typeof(int)])); + converters.Add(typeof(long?), new NullableConverter((FormDataConverter)converters[typeof(long)])); + converters.Add(typeof(UInt128?), new NullableConverter((FormDataConverter)converters[typeof(UInt128)])); + converters.Add(typeof(Half?), new NullableConverter((FormDataConverter)converters[typeof(Half)])); + converters.Add(typeof(float?), new NullableConverter((FormDataConverter)converters[typeof(float)])); + converters.Add(typeof(double?), new NullableConverter((FormDataConverter)converters[typeof(double)])); + converters.Add(typeof(decimal?), new NullableConverter((FormDataConverter)converters[typeof(decimal)])); + converters.Add(typeof(DateOnly?), new NullableConverter((FormDataConverter)converters[typeof(DateOnly)])); + converters.Add(typeof(DateTime?), new NullableConverter((FormDataConverter)converters[typeof(DateTime)])); + converters.Add(typeof(DateTimeOffset?), new NullableConverter((FormDataConverter)converters[typeof(DateTimeOffset)])); + converters.Add(typeof(TimeSpan?), new NullableConverter((FormDataConverter)converters[typeof(TimeSpan)])); + converters.Add(typeof(TimeOnly?), new NullableConverter((FormDataConverter)converters[typeof(TimeOnly)])); + converters.Add(typeof(Guid?), new NullableConverter((FormDataConverter)converters[typeof(Guid)])); + + Converters = converters; + } +} diff --git a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs index dbb16f13831f..b4b312882787 100644 --- a/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs +++ b/src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs @@ -123,7 +123,7 @@ private void UpdateEndpoints() if (!found) { - throw new InvalidOperationException($"Unable to find a provider for the render mode: {renderMode.GetType().FullName}. This generally" + + throw new InvalidOperationException($"Unable to find a provider for the render mode: {renderMode.GetType().FullName}. This generally " + $"means that a call to 'AddWebAssemblyComponents' or 'AddServerComponents' is missing. " + $"Alternatively call 'AddWebAssemblyRenderMode', 'AddServerRenderMode' might be missing if you have set UseDeclaredRenderModes = false."); } diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointsDetailedErrorsConfiguration.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointsDetailedErrorsConfiguration.cs index 54ee83d68c5d..ec88eca4ed21 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointsDetailedErrorsConfiguration.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointsDetailedErrorsConfiguration.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Components.Endpoints; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointsOptions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointsOptions.cs index 161bcf3b8348..1f2bd1491888 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointsOptions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsEndpointsOptions.cs @@ -1,7 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.DependencyInjection; +namespace Microsoft.AspNetCore.Components.Endpoints; internal class RazorComponentsEndpointsOptions { diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 8be227d6cca8..8ef454f5bbf9 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Endpoints; using Microsoft.AspNetCore.Components.Endpoints.DependencyInjection; using Microsoft.AspNetCore.Components.Forms; @@ -56,6 +57,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection // Form handling services.TryAddScoped(); + services.TryAddScoped(); return new DefaultRazorComponentsBuilder(services); } diff --git a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj index 27764a639bc0..a6fd0a24f8c6 100644 --- a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj +++ b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj @@ -32,18 +32,14 @@ + - + diff --git a/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs new file mode 100644 index 000000000000..34e3b52899b9 --- /dev/null +++ b/src/Components/Endpoints/test/Binding/FormDataMapperTests.cs @@ -0,0 +1,1136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Components.Endpoints.Binding; + +public class FormDataMapperTests +{ + [Theory] + [MemberData(nameof(PrimitiveTypesData))] + public void CanDeserialize_PrimitiveTypes(string value, Type type, object expected) + { + // Arrange + var collection = new Dictionary() { ["value"] = new StringValues(value) }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions(); + + // Act + var result = CallDeserialize(reader, options, type); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(NullableBasicTypes))] + public void CanDeserialize_NullablePrimitiveTypes(string value, Type type, object expected) + { + // Arrange + var collection = new Dictionary() { ["value"] = new StringValues(value) }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions(); + + // Act + var result = CallDeserialize(reader, options, type); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(NullNullableBasicTypes))] + public void CanDeserialize_NullValues(Type type) + { + // Arrange + var collection = new Dictionary() { }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions(); + + // Act + var result = CallDeserialize(reader, options, type); + + // Assert + Assert.Null(result); + } + + [Fact] + public void CanDeserialize_CustomParsableTypes() + { + // Arrange + var expected = new Point { X = 1, Y = 1 }; + var collection = new Dictionary() { ["value"] = new StringValues("(1,1)") }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map(reader, options); + + // Assert + Assert.Equal(expected, result); + } + +#nullable enable + [Fact] + public void CanDeserialize_NullableCustomParsableTypes() + { + // Arrange + var expected = new ValuePoint { X = 1, Y = 1 }; + var collection = new Dictionary() { ["value"] = new StringValues("(1,1)") }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map(reader, options); + + // Assert + Assert.Equal(expected, result); + } + + [Fact] + public void CanDeserialize_NullableCustomParsableTypes_NullValue() + { + // Arrange + var collection = new Dictionary() { }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map(reader, options); + + // Assert + Assert.Null(result); + } +#nullable disable + + [Fact] + public void Deserialize_Collections_NoElements_ReturnsNull() + { + // Arrange + var data = new Dictionary() { }; + var reader = new FormDataReader(data, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map>(reader, options); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Deserialize_Collections_SingleElement_ReturnsCollection() + { + // Arrange + var data = new Dictionary() { ["[0]"] = "10" }; + var reader = new FormDataReader(data, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map>(reader, options); + + // Assert + var value = Assert.Single(result); + Assert.Equal(10, value); + } + + [Theory] + [InlineData(99)] + [InlineData(100)] + [InlineData(101)] + public void Deserialize_Collections_HandlesComputedIndexesBoundaryCorrectly(int size) + { + // Arrange + var data = new Dictionary(Enumerable.Range(0, size) + .Select(i => new KeyValuePair( + $"[{i.ToString(CultureInfo.InvariantCulture)}]", + (i + 10).ToString(CultureInfo.InvariantCulture)))); + + var reader = new FormDataReader(data, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions + { + MaxCollectionSize = 110 + }; + + // Act + var result = FormDataMapper.Map>(reader, options); + + // Assert + Assert.Equal(size, result.Count); + } + + [Theory] + [InlineData(99)] + [InlineData(100)] + [InlineData(101)] + [InlineData(109)] + [InlineData(110)] + [InlineData(120)] + public void Deserialize_Collections_PoolsArraysCorrectly(int size) + { + // Arrange + var rented = new List(); + var returned = new List(); + + var data = new Dictionary(Enumerable.Range(0, size) + .Select(i => new KeyValuePair( + $"[{i.ToString(CultureInfo.InvariantCulture)}]", + (i + 10).ToString(CultureInfo.InvariantCulture)))); + + var reader = new FormDataReader(data, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions + { + MaxCollectionSize = 110 + }; + + var converter = new CollectionConverter< + int[], + TestArrayPoolBufferAdapter, + TestArrayPoolBufferAdapter.PooledBuffer, + int>(new ParsableConverter()); + + options.AddConverter(converter); + + TestArrayPoolBufferAdapter.OnRent += rented.Add; + TestArrayPoolBufferAdapter.OnReturn += returned.Add; + + // Act + var result = FormDataMapper.Map(reader, options); + + TestArrayPoolBufferAdapter.OnRent -= rented.Add; + TestArrayPoolBufferAdapter.OnReturn -= returned.Add; + + // Assert + Assert.Equal(rented.Count, returned.Count); + } + + [Theory] + [InlineData(110)] + [InlineData(120)] + public void Deserialize_Collections_ParsesUpToMaxCollectionSize(int size) + { + // Arrange + var data = new Dictionary(Enumerable.Range(0, size) + .Select(i => new KeyValuePair( + $"[{i.ToString(CultureInfo.InvariantCulture)}]", + (i + 10).ToString(CultureInfo.InvariantCulture)))); + + var reader = new FormDataReader(data, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions + { + MaxCollectionSize = 110 + }; + + // Act + var result = FormDataMapper.Map>(reader, options); + + // Assert + Assert.Equal(110, result.Count); + } + + [Fact] + public void CanDeserialize_Collections_IReadOnlySet() + { + // Arrange + var expected = new HashSet { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection, HashSet, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_IReadOnlyListOfT() + { + // Arrange + var expected = new List { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection, List, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_IReadOnlyCollection() + { + // Arrange + var expected = new List { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection, List, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ISet() + { + // Arrange + var expected = new HashSet { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection, HashSet, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_IListOfT() + { + // Arrange + var expected = new List { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection, List, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ICollectionOfT() + { + // Arrange + var expected = new List { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection, List, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_IEnumerableOfT() + { + // Arrange + var expected = new List { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection, List, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ArrayOfT() + { + // Arrange + var expected = new int[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection(expected); + } + + [Fact] + public void CanDeserialize_Collections_SortedSet() + { + // Arrange + var expected = new SortedSet(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, SortedSet, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_HashSet() + { + // Arrange + var expected = new HashSet { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection, HashSet, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ReadOnlyCollectionOfT() + { + // Arrange + var expected = new ReadOnlyCollection(new List { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ReadOnlyCollection, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_CollectionOfT() + { + // Arrange + var expected = new Collection(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, Collection, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ListOfT() + { + // Arrange + var expected = new List { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection, List, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_LinkedList() + { + // Arrange + var expected = new LinkedList(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, LinkedList, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_Queue() + { + // Arrange + var expected = new Queue(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, Queue, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_Stack() + { + // Arrange + var expected = new Stack(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, Stack, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ConcurrentBag() + { + // Arrange + var expected = new ConcurrentBag(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ConcurrentBag, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ConcurrentQueue() + { + // Arrange + var expected = new ConcurrentQueue(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ConcurrentQueue, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ConcurrentStack() + { + // Arrange + var expected = new ConcurrentStack(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ConcurrentStack, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ImmutableArray() + { + // Arrange + var expected = ImmutableArray.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ImmutableArray, int>(expected, sequenceEquals: true); + } + + [Fact] + public void CanDeserialize_Collections_ImmutableList() + { + // Arrange + var expected = ImmutableList.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ImmutableList, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ImmutableHashSet() + { + // Arrange + var expected = ImmutableHashSet.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ImmutableHashSet, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ImmutableSortedSet() + { + // Arrange + var expected = ImmutableSortedSet.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ImmutableSortedSet, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ImmutableQueue() + { + // Arrange + var expected = ImmutableQueue.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ImmutableQueue, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_ImmutableStack() + { + // Arrange + var expected = ImmutableStack.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ImmutableStack, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_IImmutableList() + { + // Arrange + var expected = ImmutableList.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ImmutableList, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_IImmutableSet() + { + // Arrange + var expected = ImmutableHashSet.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ImmutableHashSet, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_IImmutableQueue() + { + // Arrange + var expected = ImmutableQueue.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ImmutableQueue, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_IImmutableStack() + { + // Arrange + var expected = ImmutableStack.CreateRange(new[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }); + CanDeserialize_Collection, ImmutableStack, int>(expected); + } + + [Fact] + public void CanDeserialize_Collections_CustomCollection() + { + // Arrange + var expected = new CustomCollection { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }; + CanDeserialize_Collection, CustomCollection, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_Dictionary() + { + // Arrange + var expected = new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + CanDeserialize_Dictionary, Dictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_ConcurrentDictionary() + { + // Arrange + var expected = new ConcurrentDictionary(new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }); + CanDeserialize_Dictionary, ConcurrentDictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_ImmutableDictionary() + { + // Arrange + var expected = ImmutableDictionary.CreateRange(new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }); + CanDeserialize_Dictionary, ImmutableDictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_ImmutableSortedDictionary() + { + // Arrange + var expected = ImmutableSortedDictionary.CreateRange(new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }); + CanDeserialize_Dictionary, ImmutableSortedDictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_IImmutableDictionary() + { + // Arrange + var expected = ImmutableDictionary.CreateRange(new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }); + // Arrange + var collection = new Dictionary() + { + ["[0]"] = "10", + ["[1]"] = "11", + ["[2]"] = "12", + ["[3]"] = "13", + ["[4]"] = "14", + ["[5]"] = "15", + ["[6]"] = "16", + ["[7]"] = "17", + ["[8]"] = "18", + ["[9]"] = "19", + }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map>(reader, options); + + // Assert + var dictionary = Assert.IsType>(result); + Assert.Equal(expected.Count, dictionary.Count); + Assert.Equal(expected.OrderBy(o => o.Key).ToArray(), dictionary.OrderBy(o => o.Key).ToArray()); + } + + [Fact] + public void CanDeserialize_Dictionary_IDictionary() + { + // Arrange + var expected = new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + CanDeserialize_Dictionary, Dictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_SortedList() + { + // Arrange + var expected = new SortedList() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + CanDeserialize_Dictionary, SortedList, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_SortedDictionary() + { + // Arrange + var expected = new SortedDictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + CanDeserialize_Dictionary, SortedDictionary, int, int>(expected); + } + + [Fact] + public void CanDeserialize_Dictionary_IReadOnlyDictionary() + { + // Arrange + var expected = new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + var collection = new Dictionary() + { + ["[0]"] = "10", + ["[1]"] = "11", + ["[2]"] = "12", + ["[3]"] = "13", + ["[4]"] = "14", + ["[5]"] = "15", + ["[6]"] = "16", + ["[7]"] = "17", + ["[8]"] = "18", + ["[9]"] = "19", + }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map>(reader, options); + + // Assert + var dictionary = Assert.IsType>(result); + Assert.Equal(expected.Count, dictionary.Count); + Assert.Equal(expected.OrderBy(o => o.Key).ToArray(), dictionary.OrderBy(o => o.Key).ToArray()); + } + + [Fact] + public void CanDeserialize_Dictionary_ReadOnlyDictionary() + { + // Arrange + var expected = new Dictionary() { [0] = 10, [1] = 11, [2] = 12, [3] = 13, [4] = 14, [5] = 15, [6] = 16, [7] = 17, [8] = 18, [9] = 19, }; + var collection = new Dictionary() + { + ["[0]"] = "10", + ["[1]"] = "11", + ["[2]"] = "12", + ["[3]"] = "13", + ["[4]"] = "14", + ["[5]"] = "15", + ["[6]"] = "16", + ["[7]"] = "17", + ["[8]"] = "18", + ["[9]"] = "19", + }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map>(reader, options); + + // Assert + var dictionary = Assert.IsType>(result); + Assert.Equal(expected.Count, dictionary.Count); + Assert.Equal(expected.OrderBy(o => o.Key).ToArray(), dictionary.OrderBy(o => o.Key).ToArray()); + } + + [Fact] + public void Deserialize_EmptyDictionary_ReturnsNull() + { + // Arrange + var collection = new Dictionary() { }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map>(reader, options); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Deserialize_Dictionary_RespectsMaxCollectionSize() + { + // Arrange + var collection = new Dictionary() { }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map>(reader, options); + + // Assert + Assert.Null(result); + } + + private void CanDeserialize_Dictionary(TImplementation expected) + where TDictionary : IDictionary + where TImplementation : TDictionary + { + // Arrange + var collection = new Dictionary() + { + ["[0]"] = "10", + ["[1]"] = "11", + ["[2]"] = "12", + ["[3]"] = "13", + ["[4]"] = "14", + ["[5]"] = "15", + ["[6]"] = "16", + ["[7]"] = "17", + ["[8]"] = "18", + ["[9]"] = "19", + }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + var options = new FormDataMapperOptions(); + + // Act + var result = CallDeserialize(reader, options, typeof(TDictionary)); + + // Assert + var dictionary = Assert.IsType(result); + Assert.Equal(expected.Count, dictionary.Count); + Assert.Equal(expected.OrderBy(o => o.Key).ToArray(), dictionary.OrderBy(o => o.Key).ToArray()); + } + + private void CanDeserialize_Collection(TImplementation expected, bool sequenceEquals = false) + { + // Arrange + var collection = new Dictionary() + { + ["[0]"] = "10", + ["[1]"] = "11", + ["[2]"] = "12", + ["[3]"] = "13", + ["[4]"] = "14", + ["[5]"] = "15", + ["[6]"] = "16", + ["[7]"] = "17", + ["[8]"] = "18", + ["[9]"] = "19", + }; + var reader = new FormDataReader(collection, CultureInfo.InvariantCulture); + reader.PushPrefix("value"); + var options = new FormDataMapperOptions(); + + // Act + var result = CallDeserialize(reader, options, typeof(TCollection)); + + // Assert + var list = Assert.IsType(result); + if (!sequenceEquals) + { + Assert.Equal(expected, list); + } + else + { + Assert.True(((IEnumerable)expected).SequenceEqual((IEnumerable)list)); + } + } + + [Fact] + public void CanDeserialize_ComplexValueType_Address() + { + // Arrange + var expected = new Address() { City = "Redmond", Street = "1 Microsoft Way", Country = "United States", ZipCode = 98052 }; + var data = new Dictionary() + { + ["City"] = "Redmond", + ["Country"] = "United States", + ["Street"] = "1 Microsoft Way", + ["ZipCode"] = "98052", + }; + var reader = new FormDataReader(data, CultureInfo.InvariantCulture); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map
(reader, options); + + // Assert + Assert.Equal(expected.City, result.City); + Assert.Equal(expected.Street, result.Street); + Assert.Equal(expected.Country, result.Country); + Assert.Equal(expected.ZipCode, result.ZipCode); + } + + [Fact] + public void CanDeserialize_ComplexReferenceType_Customer() + { + // Arrange + var expected = new Customer() { Age = 20, Name = "John Doe", Email = "john.doe@example.com", IsPreferred = true }; + var data = new Dictionary() + { + ["Age"] = "20", + ["Name"] = "John Doe", + ["Email"] = "john.doe@example.com", + ["IsPreferred"] = "true", + }; + + var reader = new FormDataReader(data, CultureInfo.InvariantCulture); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map(reader, options); + + // Assert + Assert.NotNull(result); + Assert.Equal(expected.Age, result.Age); + Assert.Equal(expected.Name, result.Name); + Assert.Equal(expected.Email, result.Email); + Assert.Equal(expected.IsPreferred, result.IsPreferred); + } + + [Fact] + public void CanDeserialize_ComplexReferenceType_Inheritance() + { + // Arrange + var expected = new FrequentCustomer() { Age = 20, Name = "John Doe", Email = "john@example.com" , IsPreferred = true, TotalVisits = 10, PreferredStore = "Redmond", MonthlyFrequency = 0.8 }; + var data = new Dictionary() + { + ["Age"] = "20", + ["Name"] = "John Doe", + ["Email"] = "john@example.com", + ["IsPreferred"] = "true", + ["TotalVisits"] = "10", + ["PreferredStore"] = "Redmond", + ["MonthlyFrequency"] = "0.8", + }; + + var reader = new FormDataReader(data, CultureInfo.InvariantCulture); + var options = new FormDataMapperOptions(); + + // Act + var result = FormDataMapper.Map(reader, options); + Assert.NotNull(result); + Assert.Equal(expected.Age, result.Age); + Assert.Equal(expected.Name, result.Name); + Assert.Equal(expected.Email, result.Email); + Assert.Equal(expected.IsPreferred, result.IsPreferred); + Assert.Equal(expected.TotalVisits, result.TotalVisits); + Assert.Equal(expected.PreferredStore, result.PreferredStore); + Assert.Equal(expected.MonthlyFrequency, result.MonthlyFrequency); + } + + public static TheoryData NullableBasicTypes + { + get + { + var result = new TheoryData + { + // strings + { "C", typeof(char?), new char?('C')}, + // bool + { "true", typeof(bool?), new bool?(true)}, + // bytes + { "63", typeof(byte?), new byte?((byte)0b_0011_1111)}, + { "-63", typeof(sbyte?), new sbyte?((sbyte)-0b_0011_1111)}, + // numeric types + { "123", typeof(ushort?), new ushort?((ushort)123u)}, + { "456", typeof(uint?), new uint?(456u)}, + { "789", typeof(ulong?), new ulong?(789uL)}, + { "-101112", typeof(Int128?), new Int128?(-(Int128)101112)}, + { "-123", typeof(short?), new short?((short)-123)}, + { "-456", typeof(int?), new int?(-456)}, + { "-789", typeof(long?), new long?(-789L)}, + { "101112", typeof(UInt128?), new UInt128?((UInt128)101112)}, + // floating point types + { "12.56", typeof(Half?), new Half?((Half)12.56f)}, + { "6.28", typeof(float?), new float?(6.28f)}, + { "3.14", typeof(double?), new double?(3.14)}, + { "1.23", typeof(decimal?), new decimal?(1.23m)}, + // dates and times + { "04/20/2023", typeof(DateOnly?), new DateOnly?(new DateOnly(2023, 04, 20))}, + { "4/20/2023 12:56:34", typeof(DateTime?), new DateTime?(new DateTime(2023, 04, 20, 12, 56, 34))}, + { "4/20/2023 12:56:34 PM +02:00", typeof(DateTimeOffset?), new DateTimeOffset?(new DateTimeOffset(2023, 04, 20, 12, 56, 34, TimeSpan.FromHours(2)))}, + { "02:01:03", typeof(TimeSpan?), new TimeSpan?(new TimeSpan(02, 01, 03))}, + { "12:56:34", typeof(TimeOnly?), new TimeOnly?(new TimeOnly(12, 56, 34))}, + + // other types + { "a55eb3df-e984-42b5-85ca-4f68da8567d1", typeof(Guid?), new Guid?(new Guid("a55eb3df-e984-42b5-85ca-4f68da8567d1")) }, + }; + + return result; + } + } + + public static TheoryData NullNullableBasicTypes + { + get + { + var result = new TheoryData + { + // strings + { typeof(char?) }, + // bool + { typeof(bool?) }, + // bytes + { typeof(byte?) }, + { typeof(sbyte?) }, + // numeric types + { typeof(ushort?) }, + { typeof(uint?) }, + { typeof(ulong?) }, + { typeof(Int128?) }, + { typeof(short?) }, + { typeof(int?) }, + { typeof(long?) }, + { typeof(UInt128?) }, + // floating point types + { typeof(Half?) }, + { typeof(float?) }, + { typeof(double?) }, + { typeof(decimal?) }, + // dates and times + { typeof(DateOnly?) }, + { typeof(DateTime?) }, + { typeof(DateTimeOffset?) }, + { typeof(TimeSpan?) }, + { typeof(TimeOnly?) }, + + // other types + { typeof(Guid?) }, + }; + + return result; + } + } + + public static TheoryData PrimitiveTypesData + { + get + { + var result = new TheoryData + { + // strings + { "C", typeof(char), 'C' }, + { "hello", typeof(string), "hello" }, + // bool + { "true", typeof(bool), true }, + // bytes + { "63", typeof(byte), (byte)0b_0011_1111 }, + { "-63", typeof(sbyte), (sbyte)-0b_0011_1111 }, + // numeric types + { "123", typeof(ushort), (ushort)123u }, + { "456", typeof(uint), 456u }, + { "789", typeof(ulong), 789uL }, + { "-101112", typeof(Int128), -(Int128)101112 }, + { "-123", typeof(short), (short)-123 }, + { "-456", typeof(int), -456 }, + { "-789", typeof(long), -789L }, + { "101112", typeof(UInt128), (UInt128)101112 }, + // floating point types + { "12.56", typeof(Half), (Half)12.56f }, + { "6.28", typeof(float), 6.28f }, + { "3.14", typeof(double), 3.14 }, + { "1.23", typeof(decimal), 1.23m }, + // dates and times + { "04/20/2023", typeof(DateOnly), new DateOnly(2023, 04, 20) }, + { "4/20/2023 12:56:34", typeof(DateTime), new DateTime(2023, 04, 20, 12, 56, 34) }, + { "4/20/2023 12:56:34 PM +02:00", typeof(DateTimeOffset), new DateTimeOffset(2023, 04, 20, 12, 56, 34, TimeSpan.FromHours(2)) }, + { "02:01:03", typeof(TimeSpan), new TimeSpan(02, 01, 03) }, + { "12:56:34", typeof(TimeOnly), new TimeOnly(12, 56, 34) }, + + // other types + { "a55eb3df-e984-42b5-85ca-4f68da8567d1", typeof(Guid), new Guid("a55eb3df-e984-42b5-85ca-4f68da8567d1") } + }; + + return result; + } + } + + private object CallDeserialize(FormDataReader reader, FormDataMapperOptions options, Type type) + { + var method = typeof(FormDataMapper) + .GetMethod("Map", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public) ?? + throw new InvalidOperationException("Unable to find method 'Map'."); + + return method.MakeGenericMethod(type).Invoke(null, new object[] { reader, options })!; + } +} + +internal class Point : IParsable, IEquatable +{ + public int X { get; set; } + public int Y { get; set; } + + public static Point Parse(string s, IFormatProvider provider) + { + // Parses points. Points start with ( and end with ). + // Points define two components, X and Y, separated by a comma. + var components = s.Trim('(', ')').Split(','); + if (components.Length != 2) + { + throw new FormatException("Invalid point format."); + } + var result = new Point(); + result.X = int.Parse(components[0], provider); + result.Y = int.Parse(components[1], provider); + return result; + } + + public static bool TryParse([NotNullWhen(true)] string s, IFormatProvider provider, [MaybeNullWhen(false)] out Point result) + { + // Try parse points is similar to Parse, but returns a bool to indicate success. + // It also uses the out parameter to return the result. + try + { + result = Parse(s, provider); + return true; + } + catch (FormatException) + { + result = null; + return false; + } + } + + public override bool Equals(object obj) => Equals(obj as Point); + + public bool Equals(Point other) => other is not null && X == other.X && Y == other.Y; + + public override int GetHashCode() => HashCode.Combine(X, Y); + + public static bool operator ==(Point left, Point right) => EqualityComparer.Default.Equals(left, right); + + public static bool operator !=(Point left, Point right) => !(left == right); +} + +internal struct ValuePoint : IParsable, IEquatable +{ + public int X { get; set; } + + public int Y { get; set; } + + public static ValuePoint Parse(string s, IFormatProvider provider) + { + // Parses points. Points start with ( and end with ). + // Points define two components, X and Y, separated by a comma. + var components = s.Trim('(', ')').Split(','); + if (components.Length != 2) + { + throw new FormatException("Invalid point format."); + } + var result = new ValuePoint(); + result.X = int.Parse(components[0], provider); + result.Y = int.Parse(components[1], provider); + return result; + } + + public static bool TryParse([NotNullWhen(true)] string s, IFormatProvider provider, [MaybeNullWhen(false)] out ValuePoint result) + { + // Try parse points is similar to Parse, but returns a bool to indicate success. + // It also uses the out parameter to return the result. + try + { + result = Parse(s, provider); + return true; + } + catch (FormatException) + { + result = default; + return false; + } + } + + public override bool Equals(object obj) => Equals((ValuePoint)obj); + + public bool Equals(ValuePoint other) => X == other.X && Y == other.Y; + + public override int GetHashCode() => HashCode.Combine(X, Y); + + public static bool operator ==(ValuePoint left, ValuePoint right) => EqualityComparer.Default.Equals(left, right); + + public static bool operator !=(ValuePoint left, ValuePoint right) => !(left == right); +} + +internal abstract class TestArrayPoolBufferAdapter + : ICollectionBufferAdapter +{ + public static event Action OnRent; + public static event Action OnReturn; + + public static PooledBuffer CreateBuffer() => new() { Data = Rent(16), Count = 0 }; + + public static PooledBuffer Add(ref PooledBuffer buffer, int element) + { + if (buffer.Count >= buffer.Data.Length) + { + var newBuffer = Rent(buffer.Data.Length * 2); + Array.Copy(buffer.Data, newBuffer, buffer.Data.Length); + Return(buffer.Data); + buffer.Data = newBuffer; + } + + buffer.Data[buffer.Count++] = element; + return buffer; + } + + public static int[] ToResult(PooledBuffer buffer) + { + var result = new int[buffer.Count]; + Array.Copy(buffer.Data, result, buffer.Count); + Return(buffer.Data); + return result; + } + + public struct PooledBuffer + { + public int[] Data { get; set; } + public int Count { get; set; } + } + + public static int[] Rent(int size) + { + var result = ArrayPool.Shared.Rent(size); + OnRent?.Invoke(result); + return result; + } + + public static void Return(int[] array) + { + OnReturn?.Invoke(array); + ArrayPool.Shared.Return(array); + } +} + +internal struct Address +{ + public string Street { get; set; } + public string City { get; set; } + public string Country { get; set; } + public int ZipCode { get; set; } +} + +internal class Customer +{ + public string Name { get; set; } + public string Email { get; set; } + public int Age { get; set; } + public bool IsPreferred { get; set; } +} + +internal class FrequentCustomer : Customer +{ + public int TotalVisits { get; set; } + + public string PreferredStore { get; set; } + + public double MonthlyFrequency { get; set; } +} + +// Implements ICollection delegating to List _inner; +internal class CustomCollection : ICollection +{ + private readonly List _inner = new(); + + public int Count => _inner.Count; + public bool IsReadOnly => false; + public void Add(T item) => _inner.Add(item); + public void Clear() => _inner.Clear(); + public bool Contains(T item) => _inner.Contains(item); + public bool Remove(T item) => _inner.Remove(item); + public IEnumerator GetEnumerator() => _inner.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + public void CopyTo(T[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex); +} diff --git a/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor b/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor index 1684adfe5462..bb031aa6572f 100644 --- a/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor +++ b/src/Components/Samples/BlazorUnitedApp/Pages/Index.razor @@ -2,16 +2,12 @@ Index -

Hello, world!

- -@* - - - - - - *@ +

@Parameter

+ + + + @if (_submitted) { @@ -19,6 +15,8 @@ } @code{ + [SupplyParameterFromForm] string Parameter { get; set; } = "Hello, world!"; + bool _submitted = false; public void Submit() { diff --git a/src/Components/Shared/test/TestRenderer.cs b/src/Components/Shared/test/TestRenderer.cs index 1cc98044ca7f..ff25193733a9 100644 --- a/src/Components/Shared/test/TestRenderer.cs +++ b/src/Components/Shared/test/TestRenderer.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.Logging.Abstractions; @@ -60,6 +61,8 @@ protected internal override bool ShouldTrackNamedEventHandlers() public Task NextRenderResultTask { get; set; } = Task.CompletedTask; + private HashSet UndisposedComponentStates { get; } = new(); + public new int AssignRootComponentId(IComponent component) => base.AssignRootComponentId(component); @@ -145,4 +148,35 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) public new void ProcessPendingRender() => base.ProcessPendingRender(); + + protected override ComponentState CreateComponentState(int componentId, IComponent component, ComponentState parentComponentState) + => new TestRendererComponentState(this, componentId, component, parentComponentState); + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (UndisposedComponentStates.Count > 0) + { + throw new InvalidOperationException("Did not dispose all the ComponentState instances. This could lead to ArrayBuffer not returning buffers to its pool."); + } + } + + class TestRendererComponentState : ComponentState, IAsyncDisposable + { + private readonly TestRenderer _renderer; + + public TestRendererComponentState(Renderer renderer, int componentId, IComponent component, ComponentState parentComponentState) + : base(renderer, componentId, component, parentComponentState) + { + _renderer = (TestRenderer)renderer; + _renderer.UndisposedComponentStates.Add(this); + } + + public override ValueTask DisposeAsync() + { + _renderer.UndisposedComponentStates.Remove(this); + return base.DisposeAsync(); + } + } } diff --git a/src/Components/Web.JS/dist/Release/blazor.server.js b/src/Components/Web.JS/dist/Release/blazor.server.js index 4fcc222b4c5c..5a79f98b9a61 100644 --- a/src/Components/Web.JS/dist/Release/blazor.server.js +++ b/src/Components/Web.JS/dist/Release/blazor.server.js @@ -1 +1 @@ -(()=>{"use strict";var e,t,n,r={};r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),function(e){const t=[],n="__jsObjectId",r="__dotNetObject",o="__byte[]",i="__dotNetStream",s="__jsStreamReferenceLength";let a,c;class l{constructor(e){this._jsObject=e,this._cachedFunctions=new Map}findFunction(e){const t=this._cachedFunctions.get(e);if(t)return t;let n,r=this._jsObject;if(e.split(".").forEach((t=>{if(!(t in r))throw new Error(`Could not find '${e}' ('${t}' was undefined).`);n=r,r=r[t]})),r instanceof Function)return r=r.bind(n),this._cachedFunctions.set(e,r),r;throw new Error(`The value '${e}' is not a function.`)}getWrappedObject(){return this._jsObject}}const h={0:new l(window)};h[0]._cachedFunctions.set("import",(e=>("string"==typeof e&&e.startsWith("./")&&(e=new URL(e.substr(2),document.baseURI).toString()),import(e))));let d,u=1;function p(e){t.push(e)}function f(e){if(e&&"object"==typeof e){h[u]=new l(e);const t={[n]:u};return u++,t}throw new Error(`Cannot create a JSObjectReference from the value '${e}'.`)}function g(e){let t=-1;if(e instanceof ArrayBuffer&&(e=new Uint8Array(e)),e instanceof Blob)t=e.size;else{if(!(e.buffer instanceof ArrayBuffer))throw new Error("Supplied value is not a typed array or blob.");if(void 0===e.byteLength)throw new Error(`Cannot create a JSStreamReference from the value '${e}' as it doesn't have a byteLength.`);t=e.byteLength}const r={[s]:t};try{const t=f(e);r[n]=t[n]}catch(t){throw new Error(`Cannot create a JSStreamReference from the value '${e}'.`)}return r}function m(e,n){c=e;const r=n?JSON.parse(n,((e,n)=>t.reduce(((t,n)=>n(e,t)),n))):null;return c=void 0,r}function y(){if(void 0===a)throw new Error("No call dispatcher has been set.");if(null===a)throw new Error("There are multiple .NET runtimes present, so a default dispatcher could not be resolved. Use DotNetObject to invoke .NET instance methods.");return a}e.attachDispatcher=function(e){const t=new v(e);return void 0===a?a=t:a&&(a=null),t},e.attachReviver=p,e.invokeMethod=function(e,t,...n){return y().invokeDotNetStaticMethod(e,t,...n)},e.invokeMethodAsync=function(e,t,...n){return y().invokeDotNetStaticMethodAsync(e,t,...n)},e.createJSObjectReference=f,e.createJSStreamReference=g,e.disposeJSObjectReference=function(e){const t=e&&e[n];"number"==typeof t&&function(e){delete h[e]}(t)},function(e){e[e.Default=0]="Default",e[e.JSObjectReference=1]="JSObjectReference",e[e.JSStreamReference=2]="JSStreamReference",e[e.JSVoidResult=3]="JSVoidResult"}(d=e.JSCallResultType||(e.JSCallResultType={}));class v{constructor(e){this._dotNetCallDispatcher=e,this._byteArraysToBeRevived=new Map,this._pendingDotNetToJSStreams=new Map,this._pendingAsyncCalls={},this._nextAsyncCallId=1}getDotNetCallDispatcher(){return this._dotNetCallDispatcher}invokeJSFromDotNet(e,t,n,r){const o=m(this,t),i=C(b(e,r)(...o||[]),n);return null==i?null:k(this,i)}beginInvokeJSFromDotNet(e,t,n,r,o){const i=new Promise((e=>{const r=m(this,n);e(b(t,o)(...r||[]))}));e&&i.then((t=>k(this,[e,!0,C(t,r)]))).then((t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!0,t)),(t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!1,JSON.stringify([e,!1,w(t)]))))}endInvokeDotNetFromJS(e,t,n){const r=t?m(this,n):new Error(n);this.completePendingCall(parseInt(e,10),t,r)}invokeDotNetStaticMethod(e,t,...n){return this.invokeDotNetMethod(e,t,null,n)}invokeDotNetStaticMethodAsync(e,t,...n){return this.invokeDotNetMethodAsync(e,t,null,n)}invokeDotNetMethod(e,t,n,r){if(this._dotNetCallDispatcher.invokeDotNetFromJS){const o=k(this,r),i=this._dotNetCallDispatcher.invokeDotNetFromJS(e,t,n,o);return i?m(this,i):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeDotNetMethodAsync instead.")}invokeDotNetMethodAsync(e,t,n,r){if(e&&n)throw new Error(`For instance method calls, assemblyName should be null. Received '${e}'.`);const o=this._nextAsyncCallId++,i=new Promise(((e,t)=>{this._pendingAsyncCalls[o]={resolve:e,reject:t}}));try{const i=k(this,r);this._dotNetCallDispatcher.beginInvokeDotNetFromJS(o,e,t,n,i)}catch(e){this.completePendingCall(o,!1,e)}return i}receiveByteArray(e,t){this._byteArraysToBeRevived.set(e,t)}processByteArray(e){const t=this._byteArraysToBeRevived.get(e);return t?(this._byteArraysToBeRevived.delete(e),t):null}supplyDotNetStream(e,t){if(this._pendingDotNetToJSStreams.has(e)){const n=this._pendingDotNetToJSStreams.get(e);this._pendingDotNetToJSStreams.delete(e),n.resolve(t)}else{const n=new S;n.resolve(t),this._pendingDotNetToJSStreams.set(e,n)}}getDotNetStreamPromise(e){let t;if(this._pendingDotNetToJSStreams.has(e))t=this._pendingDotNetToJSStreams.get(e).streamPromise,this._pendingDotNetToJSStreams.delete(e);else{const n=new S;this._pendingDotNetToJSStreams.set(e,n),t=n.streamPromise}return t}completePendingCall(e,t,n){if(!this._pendingAsyncCalls.hasOwnProperty(e))throw new Error(`There is no pending async call with ID ${e}.`);const r=this._pendingAsyncCalls[e];delete this._pendingAsyncCalls[e],t?r.resolve(n):r.reject(n)}}function w(e){return e instanceof Error?`${e.message}\n${e.stack}`:e?e.toString():"null"}function b(e,t){const n=h[t];if(n)return n.findFunction(e);throw new Error(`JS object instance with ID ${t} does not exist (has it been disposed?).`)}e.findJSFunction=b;class _{constructor(e,t){this._id=e,this._callDispatcher=t}invokeMethod(e,...t){return this._callDispatcher.invokeDotNetMethod(null,e,this._id,t)}invokeMethodAsync(e,...t){return this._callDispatcher.invokeDotNetMethodAsync(null,e,this._id,t)}dispose(){this._callDispatcher.invokeDotNetMethodAsync(null,"__Dispose",this._id,null).catch((e=>console.error(e)))}serializeAsArg(){return{[r]:this._id}}}e.DotNetObject=_,p((function(e,t){if(t&&"object"==typeof t){if(t.hasOwnProperty(r))return new _(t[r],c);if(t.hasOwnProperty(n)){const e=t[n],r=h[e];if(r)return r.getWrappedObject();throw new Error(`JS object instance with Id '${e}' does not exist. It may have been disposed.`)}if(t.hasOwnProperty(o)){const e=t[o],n=c.processByteArray(e);if(void 0===n)throw new Error(`Byte array index '${e}' does not exist.`);return n}if(t.hasOwnProperty(i)){const e=t[i],n=c.getDotNetStreamPromise(e);return new E(n)}}return t}));class E{constructor(e){this._streamPromise=e}stream(){return this._streamPromise}async arrayBuffer(){return new Response(await this.stream()).arrayBuffer()}}class S{constructor(){this.streamPromise=new Promise(((e,t)=>{this.resolve=e,this.reject=t}))}}function C(e,t){switch(t){case d.Default:return e;case d.JSObjectReference:return f(e);case d.JSStreamReference:return g(e);case d.JSVoidResult:return null;default:throw new Error(`Invalid JS call result type '${t}'.`)}}let I=0;function k(e,t){I=0,c=e;const n=JSON.stringify(t,T);return c=void 0,n}function T(e,t){if(t instanceof _)return t.serializeAsArg();if(t instanceof Uint8Array){c.getDotNetCallDispatcher().sendByteArray(I,t);const e={[o]:I};return I++,e}return t}}(e||(e={})),function(e){e[e.prependFrame=1]="prependFrame",e[e.removeFrame=2]="removeFrame",e[e.setAttribute=3]="setAttribute",e[e.removeAttribute=4]="removeAttribute",e[e.updateText=5]="updateText",e[e.stepIn=6]="stepIn",e[e.stepOut=7]="stepOut",e[e.updateMarkup=8]="updateMarkup",e[e.permutationListEntry=9]="permutationListEntry",e[e.permutationListEnd=10]="permutationListEnd"}(t||(t={})),function(e){e[e.element=1]="element",e[e.text=2]="text",e[e.attribute=3]="attribute",e[e.component=4]="component",e[e.region=5]="region",e[e.elementReferenceCapture=6]="elementReferenceCapture",e[e.markup=8]="markup"}(n||(n={}));class o{constructor(e,t){this.componentId=e,this.fieldValue=t}static fromEvent(e,t){const n=t.target;if(n instanceof Element){const t=function(e){return e instanceof HTMLInputElement?e.type&&"checkbox"===e.type.toLowerCase()?{value:e.checked}:{value:e.value}:e instanceof HTMLSelectElement||e instanceof HTMLTextAreaElement?{value:e.value}:null}(n);if(t)return new o(e,t.value)}return null}}const i=new Map,s=new Map,a=[];function c(e){return i.get(e)}function l(e){const t=i.get(e);return(null==t?void 0:t.browserEventName)||e}function h(e,t){e.forEach((e=>i.set(e,t)))}function d(e){const t=[];for(let n=0;ne.selected)).map((e=>e.value))}}{const e=function(e){return!!e&&"INPUT"===e.tagName&&"checkbox"===e.getAttribute("type")}(t);return{value:e?!!t.checked:t.value}}}}),h(["copy","cut","paste"],{createEventArgs:e=>({type:e.type})}),h(["drag","dragend","dragenter","dragleave","dragover","dragstart","drop"],{createEventArgs:e=>{return{...u(t=e),dataTransfer:t.dataTransfer?{dropEffect:t.dataTransfer.dropEffect,effectAllowed:t.dataTransfer.effectAllowed,files:Array.from(t.dataTransfer.files).map((e=>e.name)),items:Array.from(t.dataTransfer.items).map((e=>({kind:e.kind,type:e.type}))),types:t.dataTransfer.types}:null};var t}}),h(["focus","blur","focusin","focusout"],{createEventArgs:e=>({type:e.type})}),h(["keydown","keyup","keypress"],{createEventArgs:e=>{return{key:(t=e).key,code:t.code,location:t.location,repeat:t.repeat,ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),h(["contextmenu","click","mouseover","mouseout","mousemove","mousedown","mouseup","mouseleave","mouseenter","dblclick"],{createEventArgs:e=>u(e)}),h(["error"],{createEventArgs:e=>{return{message:(t=e).message,filename:t.filename,lineno:t.lineno,colno:t.colno,type:t.type};var t}}),h(["loadstart","timeout","abort","load","loadend","progress"],{createEventArgs:e=>{return{lengthComputable:(t=e).lengthComputable,loaded:t.loaded,total:t.total,type:t.type};var t}}),h(["touchcancel","touchend","touchmove","touchenter","touchleave","touchstart"],{createEventArgs:e=>{return{detail:(t=e).detail,touches:d(t.touches),targetTouches:d(t.targetTouches),changedTouches:d(t.changedTouches),ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),h(["gotpointercapture","lostpointercapture","pointercancel","pointerdown","pointerenter","pointerleave","pointermove","pointerout","pointerover","pointerup"],{createEventArgs:e=>{return{...u(t=e),pointerId:t.pointerId,width:t.width,height:t.height,pressure:t.pressure,tiltX:t.tiltX,tiltY:t.tiltY,pointerType:t.pointerType,isPrimary:t.isPrimary};var t}}),h(["wheel","mousewheel"],{createEventArgs:e=>{return{...u(t=e),deltaX:t.deltaX,deltaY:t.deltaY,deltaZ:t.deltaZ,deltaMode:t.deltaMode};var t}}),h(["toggle"],{createEventArgs:()=>({})});const p=["date","datetime-local","month","time","week"],f=new Map;let g,m,y=0;const v={async add(e,t,n){if(!n)throw new Error("initialParameters must be an object, even if empty.");const r="__bl-dynamic-root:"+(++y).toString();f.set(r,e);const o=await _().invokeMethodAsync("AddRootComponent",t,r),i=new b(o,m[t]);return await i.setParameters(n),i}};class w{invoke(e){return this._callback(e)}setCallback(t){this._selfJSObjectReference||(this._selfJSObjectReference=e.createJSObjectReference(this)),this._callback=t}getJSObjectReference(){return this._selfJSObjectReference}dispose(){this._selfJSObjectReference&&e.disposeJSObjectReference(this._selfJSObjectReference)}}class b{constructor(e,t){this._jsEventCallbackWrappers=new Map,this._componentId=e;for(const e of t)"eventcallback"===e.type&&this._jsEventCallbackWrappers.set(e.name.toLowerCase(),new w)}setParameters(e){const t={},n=Object.entries(e||{}),r=n.length;for(const[e,r]of n){const n=this._jsEventCallbackWrappers.get(e.toLowerCase());n&&r?(n.setCallback(r),t[e]=n.getJSObjectReference()):t[e]=r}return _().invokeMethodAsync("SetRootComponentParameters",this._componentId,r,t)}async dispose(){if(null!==this._componentId){await _().invokeMethodAsync("RemoveRootComponent",this._componentId),this._componentId=null;for(const e of this._jsEventCallbackWrappers.values())e.dispose()}}}function _(){if(!g)throw new Error("Dynamic root components have not been enabled in this application.");return g}const E=[];let S;const C=new Promise((e=>{S=e}));function I(e,t,n){return T(e,t.eventHandlerId,(()=>k(e).invokeMethodAsync("DispatchEventAsync",t,n)))}function k(e){const t=E[e];if(!t)throw new Error(`No interop methods are registered for renderer ${e}`);return t}let T=(e,t,n)=>n();const D=U(["abort","blur","canplay","canplaythrough","change","cuechange","durationchange","emptied","ended","error","focus","load","loadeddata","loadedmetadata","loadend","loadstart","mouseenter","mouseleave","pointerenter","pointerleave","pause","play","playing","progress","ratechange","reset","scroll","seeked","seeking","stalled","submit","suspend","timeupdate","toggle","unload","volumechange","waiting","DOMNodeInsertedIntoDocument","DOMNodeRemovedFromDocument"]),x={submit:!0},R=U(["click","dblclick","mousedown","mousemove","mouseup"]);class P{constructor(e){this.browserRendererId=e,this.afterClickCallbacks=[];const t=++P.nextEventDelegatorId;this.eventsCollectionKey=`_blazorEvents_${t}`,this.eventInfoStore=new A(this.onGlobalEvent.bind(this))}setListener(e,t,n,r){const o=this.getEventHandlerInfosForElement(e,!0),i=o.getHandler(t);if(i)this.eventInfoStore.update(i.eventHandlerId,n);else{const i={element:e,eventName:t,eventHandlerId:n,renderingComponentId:r};this.eventInfoStore.add(i),o.setHandler(t,i)}}getHandler(e){return this.eventInfoStore.get(e)}removeListener(e){const t=this.eventInfoStore.remove(e);if(t){const e=t.element,n=this.getEventHandlerInfosForElement(e,!1);n&&n.removeHandler(t.eventName)}}notifyAfterClick(e){this.afterClickCallbacks.push(e),this.eventInfoStore.addGlobalListener("click")}setStopPropagation(e,t,n){this.getEventHandlerInfosForElement(e,!0).stopPropagation(t,n)}setPreventDefault(e,t,n){this.getEventHandlerInfosForElement(e,!0).preventDefault(t,n)}onGlobalEvent(e){if(!(e.target instanceof Element))return;this.dispatchGlobalEventToAllElements(e.type,e);const t=(n=e.type,s.get(n));var n;t&&t.forEach((t=>this.dispatchGlobalEventToAllElements(t,e))),"click"===e.type&&this.afterClickCallbacks.forEach((t=>t(e)))}dispatchGlobalEventToAllElements(e,t){const n=t.composedPath();let r=n.shift(),i=null,s=!1;const a=Object.prototype.hasOwnProperty.call(D,e);let l=!1;for(;r;){const u=r,p=this.getEventHandlerInfosForElement(u,!1);if(p){const n=p.getHandler(e);if(n&&(h=u,d=t.type,!((h instanceof HTMLButtonElement||h instanceof HTMLInputElement||h instanceof HTMLTextAreaElement||h instanceof HTMLSelectElement)&&Object.prototype.hasOwnProperty.call(R,d)&&h.disabled))){if(!s){const n=c(e);i=(null==n?void 0:n.createEventArgs)?n.createEventArgs(t):{},s=!0}Object.prototype.hasOwnProperty.call(x,t.type)&&t.preventDefault(),I(this.browserRendererId,{eventHandlerId:n.eventHandlerId,eventName:e,eventFieldInfo:o.fromEvent(n.renderingComponentId,t)},i)}p.stopPropagation(e)&&(l=!0),p.preventDefault(e)&&t.preventDefault()}r=a||l?void 0:n.shift()}var h,d}getEventHandlerInfosForElement(e,t){return Object.prototype.hasOwnProperty.call(e,this.eventsCollectionKey)?e[this.eventsCollectionKey]:t?e[this.eventsCollectionKey]=new N:null}}P.nextEventDelegatorId=0;class A{constructor(e){this.globalListener=e,this.infosByEventHandlerId={},this.countByEventName={},a.push(this.handleEventNameAliasAdded.bind(this))}add(e){if(this.infosByEventHandlerId[e.eventHandlerId])throw new Error(`Event ${e.eventHandlerId} is already tracked`);this.infosByEventHandlerId[e.eventHandlerId]=e,this.addGlobalListener(e.eventName)}get(e){return this.infosByEventHandlerId[e]}addGlobalListener(e){if(e=l(e),Object.prototype.hasOwnProperty.call(this.countByEventName,e))this.countByEventName[e]++;else{this.countByEventName[e]=1;const t=Object.prototype.hasOwnProperty.call(D,e);document.addEventListener(e,this.globalListener,t)}}update(e,t){if(Object.prototype.hasOwnProperty.call(this.infosByEventHandlerId,t))throw new Error(`Event ${t} is already tracked`);const n=this.infosByEventHandlerId[e];delete this.infosByEventHandlerId[e],n.eventHandlerId=t,this.infosByEventHandlerId[t]=n}remove(e){const t=this.infosByEventHandlerId[e];if(t){delete this.infosByEventHandlerId[e];const n=l(t.eventName);0==--this.countByEventName[n]&&(delete this.countByEventName[n],document.removeEventListener(n,this.globalListener))}return t}handleEventNameAliasAdded(e,t){if(Object.prototype.hasOwnProperty.call(this.countByEventName,e)){const n=this.countByEventName[e];delete this.countByEventName[e],document.removeEventListener(e,this.globalListener),this.addGlobalListener(t),this.countByEventName[t]+=n-1}}}class N{constructor(){this.handlers={},this.preventDefaultFlags=null,this.stopPropagationFlags=null}getHandler(e){return Object.prototype.hasOwnProperty.call(this.handlers,e)?this.handlers[e]:null}setHandler(e,t){this.handlers[e]=t}removeHandler(e){delete this.handlers[e]}preventDefault(e,t){return void 0!==t&&(this.preventDefaultFlags=this.preventDefaultFlags||{},this.preventDefaultFlags[e]=t),!!this.preventDefaultFlags&&this.preventDefaultFlags[e]}stopPropagation(e,t){return void 0!==t&&(this.stopPropagationFlags=this.stopPropagationFlags||{},this.stopPropagationFlags[e]=t),!!this.stopPropagationFlags&&this.stopPropagationFlags[e]}}function U(e){const t={};return e.forEach((e=>{t[e]=!0})),t}const M=G("_blazorLogicalChildren"),L=G("_blazorLogicalParent"),$=G("_blazorLogicalEnd");function B(e,t){if(e.childNodes.length>0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return M in e||(e[M]=[]),e}function O(e,t){const n=document.createComment("!");return F(n,e,t),n}function F(e,t,n){const r=e;if(e instanceof Comment&&J(r)&&J(r).length>0)throw new Error("Not implemented: inserting non-empty logical container");if(j(r))throw new Error("Not implemented: moving existing logical children");const o=J(t);if(n0;)H(n,0)}const r=n;r.parentNode.removeChild(r)}function j(e){return e[L]||null}function W(e,t){return J(e)[t]}function z(e){const t=V(e);return"http://www.w3.org/2000/svg"===t.namespaceURI&&"foreignObject"!==t.tagName}function J(e){return e[M]}function q(e,t){const n=J(e);t.forEach((e=>{e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=Y(e.moveRangeStart)})),t.forEach((t=>{const r=document.createComment("marker");t.moveToBeforeMarker=r;const o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):X(r,e)})),t.forEach((e=>{const t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd;let i=r;for(;i;){const e=i.nextSibling;if(n.insertBefore(i,t),i===o)break;i=e}n.removeChild(t)})),t.forEach((e=>{n[e.toSiblingIndex]=e.moveRangeStart}))}function V(e){if(e instanceof Element||e instanceof DocumentFragment)return e;if(e instanceof Comment)return e.parentNode;throw new Error("Not a valid logical element")}function K(e){const t=J(j(e));return t[Array.prototype.indexOf.call(t,e)+1]||null}function X(e,t){if(t instanceof Element||t instanceof DocumentFragment)t.appendChild(e);else{if(!(t instanceof Comment))throw new Error(`Cannot append node because the parent is not a valid logical element. Parent: ${t}`);{const n=K(t);n?n.parentNode.insertBefore(e,n):X(e,j(t))}}}function Y(e){if(e instanceof Element||e instanceof DocumentFragment)return e;const t=K(e);if(t)return t.previousSibling;{const t=j(e);return t instanceof Element||t instanceof DocumentFragment?t.lastChild:Y(t)}}function G(e){return"function"==typeof Symbol?Symbol():e}function Q(e){return`_bl_${e}`}const Z="__internalId";e.attachReviver(((e,t)=>t&&"object"==typeof t&&Object.prototype.hasOwnProperty.call(t,Z)&&"string"==typeof t[Z]?function(e){const t=`[${Q(e)}]`;return document.querySelector(t)}(t[Z]):t));const ee="_blazorDeferredValue",te=document.createElement("template"),ne=document.createElementNS("http://www.w3.org/2000/svg","g"),re={},oe="__internal_",ie="preventDefault_",se="stopPropagation_";class ae{constructor(e){this.rootComponentIds=new Set,this.childComponentLocations={},this.eventDelegator=new P(e),this.eventDelegator.notifyAfterClick((e=>{if(!ge)return;if(0!==e.button||function(e){return e.ctrlKey||e.shiftKey||e.altKey||e.metaKey}(e))return;if(e.defaultPrevented)return;const t=function(e){const t=!window._blazorDisableComposedPath&&e.composedPath&&e.composedPath();if(t){for(let e=0;edocument.baseURI,getLocationHref:()=>location.href,scrollToElement:Ie};function Ie(e){const t=document.getElementById(e);return!!t&&(t.scrollIntoView(),!0)}function ke(e,t,n=!1){const r=Me(e);!t.forceLoad&&$e(r)?Te(r,!1,t.replaceHistoryEntry,t.historyEntryState,n):function(e,t){if(location.href===e){const t=e+"?";history.replaceState(null,"",t),location.replace(e)}else t?location.replace(e):location.href=e}(e,t.replaceHistoryEntry)}async function Te(e,t,n,r,o=!1){if(Re(),function(e){const t=e.indexOf("#");return t>-1&&location.href.replace(location.hash,"")===e.substring(0,t)}(e))!function(e,t,n){De(e,t,n);const r=e.indexOf("#");r!==e.length-1&&Ie(e.substring(r+1))}(e,n,r);else{if(!o&&ye&&!await Pe(e,r,t))return;fe=!0,De(e,n,r),await Ae(t)}}function De(e,t,n){t?history.replaceState({userState:n,_index:ve},"",e):(ve++,history.pushState({userState:n,_index:ve},"",e))}function xe(e){return new Promise((t=>{const n=Ee;Ee=()=>{Ee=n,t()},history.go(e)}))}function Re(){Se&&(Se(!1),Se=null)}function Pe(e,t,n){return new Promise((r=>{Re(),_e?(we++,Se=r,_e(we,e,t,n)):r(!1)}))}async function Ae(e){var t;be&&await be(location.href,null===(t=history.state)||void 0===t?void 0:t.userState,e)}async function Ne(e){var t,n;Ee&&await Ee(e),ve=null!==(n=null===(t=history.state)||void 0===t?void 0:t._index)&&void 0!==n?n:0}let Ue;function Me(e){return Ue=Ue||document.createElement("a"),Ue.href=e,Ue.href}function Le(e,t){return e?e.tagName===t?e:Le(e.parentElement,t):null}function $e(e){const t=(n=document.baseURI).substring(0,n.lastIndexOf("/"));var n;const r=e.charAt(t.length);return e.startsWith(t)&&(""===r||"/"===r||"?"===r||"#"===r)}const Be={focus:function(e,t){if(e instanceof HTMLElement)e.focus({preventScroll:t});else{if(!(e instanceof SVGElement))throw new Error("Unable to focus an invalid element.");if(!e.hasAttribute("tabindex"))throw new Error("Unable to focus an SVG element that does not have a tabindex.");e.focus({preventScroll:t})}},focusBySelector:function(e,t){const n=document.querySelector(e);n&&(n.hasAttribute("tabindex")||(n.tabIndex=-1),n.focus({preventScroll:!0}))}},Oe={init:function(e,t,n,r=50){const o=He(t);(o||document.documentElement).style.overflowAnchor="none";const i=document.createRange();h(n.parentElement)&&(t.style.display="table-row",n.style.display="table-row");const s=new IntersectionObserver((function(r){r.forEach((r=>{var o;if(!r.isIntersecting)return;i.setStartAfter(t),i.setEndBefore(n);const s=i.getBoundingClientRect().height,a=null===(o=r.rootBounds)||void 0===o?void 0:o.height;r.target===t?e.invokeMethodAsync("OnSpacerBeforeVisible",r.intersectionRect.top-r.boundingClientRect.top,s,a):r.target===n&&n.offsetHeight>0&&e.invokeMethodAsync("OnSpacerAfterVisible",r.boundingClientRect.bottom-r.intersectionRect.bottom,s,a)}))}),{root:o,rootMargin:`${r}px`});s.observe(t),s.observe(n);const a=l(t),c=l(n);function l(e){const t={attributes:!0},n=new MutationObserver(((n,r)=>{h(e.parentElement)&&(r.disconnect(),e.style.display="table-row",r.observe(e,t)),s.unobserve(e),s.observe(e)}));return n.observe(e,t),n}function h(e){return null!==e&&(e instanceof HTMLTableElement&&""===e.style.display||"table"===e.style.display||e instanceof HTMLTableSectionElement&&""===e.style.display||"table-row-group"===e.style.display)}Fe[e._id]={intersectionObserver:s,mutationObserverBefore:a,mutationObserverAfter:c}},dispose:function(e){const t=Fe[e._id];t&&(t.intersectionObserver.disconnect(),t.mutationObserverBefore.disconnect(),t.mutationObserverAfter.disconnect(),e.dispose(),delete Fe[e._id])}},Fe={};function He(e){return e&&e!==document.body&&e!==document.documentElement?"visible"!==getComputedStyle(e).overflowY?e:He(e.parentElement):null}const je={getAndRemoveExistingTitle:function(){var e;const t=document.head?document.head.getElementsByTagName("title"):[];if(0===t.length)return null;let n=null;for(let r=t.length-1;r>=0;r--){const o=t[r],i=o.previousSibling;i instanceof Comment&&null!==j(i)||(null===n&&(n=o.textContent),null===(e=o.parentNode)||void 0===e||e.removeChild(o))}return n}},We={init:function(e,t){t._blazorInputFileNextFileId=0,t.addEventListener("click",(function(){t.value=""})),t.addEventListener("change",(function(){t._blazorFilesById={};const n=Array.prototype.map.call(t.files,(function(e){const n={id:++t._blazorInputFileNextFileId,lastModified:new Date(e.lastModified).toISOString(),name:e.name,size:e.size,contentType:e.type,readPromise:void 0,arrayBuffer:void 0,blob:e};return t._blazorFilesById[n.id]=n,n}));e.invokeMethodAsync("NotifyChange",n)}))},toImageFile:async function(e,t,n,r,o){const i=ze(e,t),s=await new Promise((function(e){const t=new Image;t.onload=function(){URL.revokeObjectURL(t.src),e(t)},t.onerror=function(){t.onerror=null,URL.revokeObjectURL(t.src)},t.src=URL.createObjectURL(i.blob)})),a=await new Promise((function(e){var t;const i=Math.min(1,r/s.width),a=Math.min(1,o/s.height),c=Math.min(i,a),l=document.createElement("canvas");l.width=Math.round(s.width*c),l.height=Math.round(s.height*c),null===(t=l.getContext("2d"))||void 0===t||t.drawImage(s,0,0,l.width,l.height),l.toBlob(e,n)})),c={id:++e._blazorInputFileNextFileId,lastModified:i.lastModified,name:i.name,size:(null==a?void 0:a.size)||0,contentType:n,blob:a||i.blob};return e._blazorFilesById[c.id]=c,c},readFileData:async function(e,t){return ze(e,t).blob}};function ze(e,t){const n=e._blazorFilesById[t];if(!n)throw new Error(`There is no file with ID ${t}. The file list may have changed. See https://aka.ms/aspnet/blazor-input-file-multiple-selections.`);return n}const Je=new Set,qe={enableNavigationPrompt:function(e){0===Je.size&&window.addEventListener("beforeunload",Ve),Je.add(e)},disableNavigationPrompt:function(e){Je.delete(e),0===Je.size&&window.removeEventListener("beforeunload",Ve)}};function Ve(e){e.preventDefault(),e.returnValue=!0}async function Ke(e,t,n){return e instanceof Blob?await async function(e,t,n){const r=e.slice(t,t+n),o=await r.arrayBuffer();return new Uint8Array(o)}(e,t,n):function(e,t,n){return new Uint8Array(e.buffer,e.byteOffset+t,n)}(e,t,n)}new Map;const Xe={navigateTo:function(e,t,n=!1){ke(e,t instanceof Object?t:{forceLoad:t,replaceHistoryEntry:n})},registerCustomEventType:function(e,t){if(!t)throw new Error("The options parameter is required.");if(i.has(e))throw new Error(`The event '${e}' is already registered.`);if(t.browserEventName){const n=s.get(t.browserEventName);n?n.push(e):s.set(t.browserEventName,[e]),a.forEach((n=>n(e,t.browserEventName)))}i.set(e,t)},rootComponents:v,_internal:{navigationManager:Ce,domWrapper:Be,Virtualize:Oe,PageTitle:je,InputFile:We,NavigationLock:qe,getJSDataStreamChunk:Ke,attachWebRendererInterop:function(t,n,r){const o=E.length;return E.push(t),Object.keys(n).length>0&&function(t,n,r){if(g)throw new Error("Dynamic root components have already been enabled.");g=t,m=n;for(const[t,o]of Object.entries(r)){const r=e.findJSFunction(t,0);for(const e of o)r(e,n[e])}}(k(o),n,r),S(),o}}};window.Blazor=Xe;const Ye=[0,2e3,1e4,3e4,null];class Ge{constructor(e){this._retryDelays=void 0!==e?[...e,null]:Ye}nextRetryDelayInMilliseconds(e){return this._retryDelays[e.previousRetryCount]}}class Qe{}Qe.Authorization="Authorization",Qe.Cookie="Cookie";class Ze{constructor(e,t,n){this.statusCode=e,this.statusText=t,this.content=n}}class et{get(e,t){return this.send({...t,method:"GET",url:e})}post(e,t){return this.send({...t,method:"POST",url:e})}delete(e,t){return this.send({...t,method:"DELETE",url:e})}getCookieString(e){return""}}class tt extends et{constructor(e,t){super(),this._innerClient=e,this._accessTokenFactory=t}async send(e){let t=!0;this._accessTokenFactory&&(!this._accessToken||e.url&&e.url.indexOf("/negotiate?")>0)&&(t=!1,this._accessToken=await this._accessTokenFactory()),this._setAuthorizationHeader(e);const n=await this._innerClient.send(e);return t&&401===n.statusCode&&this._accessTokenFactory?(this._accessToken=await this._accessTokenFactory(),this._setAuthorizationHeader(e),await this._innerClient.send(e)):n}_setAuthorizationHeader(e){e.headers||(e.headers={}),this._accessToken?e.headers[Qe.Authorization]=`Bearer ${this._accessToken}`:this._accessTokenFactory&&e.headers[Qe.Authorization]&&delete e.headers[Qe.Authorization]}getCookieString(e){return this._innerClient.getCookieString(e)}}class nt extends Error{constructor(e,t){const n=new.target.prototype;super(`${e}: Status code '${t}'`),this.statusCode=t,this.__proto__=n}}class rt extends Error{constructor(e="A timeout occurred."){const t=new.target.prototype;super(e),this.__proto__=t}}class ot extends Error{constructor(e="An abort occurred."){const t=new.target.prototype;super(e),this.__proto__=t}}class it extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="UnsupportedTransportError",this.__proto__=n}}class st extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="DisabledTransportError",this.__proto__=n}}class at extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="FailedToStartTransportError",this.__proto__=n}}class ct extends Error{constructor(e){const t=new.target.prototype;super(e),this.errorType="FailedToNegotiateWithServerError",this.__proto__=t}}class lt extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.innerErrors=t,this.__proto__=n}}var ht;!function(e){e[e.Trace=0]="Trace",e[e.Debug=1]="Debug",e[e.Information=2]="Information",e[e.Warning=3]="Warning",e[e.Error=4]="Error",e[e.Critical=5]="Critical",e[e.None=6]="None"}(ht||(ht={}));class dt{constructor(){}log(e,t){}}dt.instance=new dt;const ut="0.0.0-DEV_BUILD";class pt{static isRequired(e,t){if(null==e)throw new Error(`The '${t}' argument is required.`)}static isNotEmpty(e,t){if(!e||e.match(/^\s*$/))throw new Error(`The '${t}' argument should not be empty.`)}static isIn(e,t,n){if(!(e in t))throw new Error(`Unknown ${n} value: ${e}.`)}}class ft{static get isBrowser(){return"object"==typeof window&&"object"==typeof window.document}static get isWebWorker(){return"object"==typeof self&&"importScripts"in self}static get isReactNative(){return"object"==typeof window&&void 0===window.document}static get isNode(){return!this.isBrowser&&!this.isWebWorker&&!this.isReactNative}}function gt(e,t){let n="";return mt(e)?(n=`Binary data of length ${e.byteLength}`,t&&(n+=`. Content: '${function(e){const t=new Uint8Array(e);let n="";return t.forEach((e=>{n+=`0x${e<16?"0":""}${e.toString(16)} `})),n.substr(0,n.length-1)}(e)}'`)):"string"==typeof e&&(n=`String data of length ${e.length}`,t&&(n+=`. Content: '${e}'`)),n}function mt(e){return e&&"undefined"!=typeof ArrayBuffer&&(e instanceof ArrayBuffer||e.constructor&&"ArrayBuffer"===e.constructor.name)}async function yt(e,t,n,r,o,i){const s={},[a,c]=bt();s[a]=c,e.log(ht.Trace,`(${t} transport) sending data. ${gt(o,i.logMessageContent)}.`);const l=mt(o)?"arraybuffer":"text",h=await n.post(r,{content:o,headers:{...s,...i.headers},responseType:l,timeout:i.timeout,withCredentials:i.withCredentials});e.log(ht.Trace,`(${t} transport) request complete. Response status: ${h.statusCode}.`)}class vt{constructor(e,t){this._subject=e,this._observer=t}dispose(){const e=this._subject.observers.indexOf(this._observer);e>-1&&this._subject.observers.splice(e,1),0===this._subject.observers.length&&this._subject.cancelCallback&&this._subject.cancelCallback().catch((e=>{}))}}class wt{constructor(e){this._minLevel=e,this.out=console}log(e,t){if(e>=this._minLevel){const n=`[${(new Date).toISOString()}] ${ht[e]}: ${t}`;switch(e){case ht.Critical:case ht.Error:this.out.error(n);break;case ht.Warning:this.out.warn(n);break;case ht.Information:this.out.info(n);break;default:this.out.log(n)}}}}function bt(){let e="X-SignalR-User-Agent";return ft.isNode&&(e="User-Agent"),[e,_t(ut,Et(),ft.isNode?"NodeJS":"Browser",St())]}function _t(e,t,n,r){let o="Microsoft SignalR/";const i=e.split(".");return o+=`${i[0]}.${i[1]}`,o+=` (${e}; `,o+=t&&""!==t?`${t}; `:"Unknown OS; ",o+=`${n}`,o+=r?`; ${r}`:"; Unknown Runtime Version",o+=")",o}function Et(){if(!ft.isNode)return"";switch(process.platform){case"win32":return"Windows NT";case"darwin":return"macOS";case"linux":return"Linux";default:return process.platform}}function St(){if(ft.isNode)return process.versions.node}function Ct(e){return e.stack?e.stack:e.message?e.message:`${e}`}class It extends et{constructor(e){if(super(),this._logger=e,"undefined"==typeof fetch){const e=require;this._jar=new(e("tough-cookie").CookieJar),"undefined"==typeof fetch?this._fetchType=e("node-fetch"):this._fetchType=fetch,this._fetchType=e("fetch-cookie")(this._fetchType,this._jar)}else this._fetchType=fetch.bind(function(){if("undefined"!=typeof globalThis)return globalThis;if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if(void 0!==r.g)return r.g;throw new Error("could not find global")}());if("undefined"==typeof AbortController){const e=require;this._abortControllerType=e("abort-controller")}else this._abortControllerType=AbortController}async send(e){if(e.abortSignal&&e.abortSignal.aborted)throw new ot;if(!e.method)throw new Error("No method defined.");if(!e.url)throw new Error("No url defined.");const t=new this._abortControllerType;let n;e.abortSignal&&(e.abortSignal.onabort=()=>{t.abort(),n=new ot});let r,o=null;if(e.timeout){const r=e.timeout;o=setTimeout((()=>{t.abort(),this._logger.log(ht.Warning,"Timeout from HTTP request."),n=new rt}),r)}""===e.content&&(e.content=void 0),e.content&&(e.headers=e.headers||{},mt(e.content)?e.headers["Content-Type"]="application/octet-stream":e.headers["Content-Type"]="text/plain;charset=UTF-8");try{r=await this._fetchType(e.url,{body:e.content,cache:"no-cache",credentials:!0===e.withCredentials?"include":"same-origin",headers:{"X-Requested-With":"XMLHttpRequest",...e.headers},method:e.method,mode:"cors",redirect:"follow",signal:t.signal})}catch(e){if(n)throw n;throw this._logger.log(ht.Warning,`Error from HTTP request. ${e}.`),e}finally{o&&clearTimeout(o),e.abortSignal&&(e.abortSignal.onabort=null)}if(!r.ok){const e=await kt(r,"text");throw new nt(e||r.statusText,r.status)}const i=kt(r,e.responseType),s=await i;return new Ze(r.status,r.statusText,s)}getCookieString(e){return""}}function kt(e,t){let n;switch(t){case"arraybuffer":n=e.arrayBuffer();break;case"text":default:n=e.text();break;case"blob":case"document":case"json":throw new Error(`${t} is not supported.`)}return n}class Tt extends et{constructor(e){super(),this._logger=e}send(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new ot):e.method?e.url?new Promise(((t,n)=>{const r=new XMLHttpRequest;r.open(e.method,e.url,!0),r.withCredentials=void 0===e.withCredentials||e.withCredentials,r.setRequestHeader("X-Requested-With","XMLHttpRequest"),""===e.content&&(e.content=void 0),e.content&&(mt(e.content)?r.setRequestHeader("Content-Type","application/octet-stream"):r.setRequestHeader("Content-Type","text/plain;charset=UTF-8"));const o=e.headers;o&&Object.keys(o).forEach((e=>{r.setRequestHeader(e,o[e])})),e.responseType&&(r.responseType=e.responseType),e.abortSignal&&(e.abortSignal.onabort=()=>{r.abort(),n(new ot)}),e.timeout&&(r.timeout=e.timeout),r.onload=()=>{e.abortSignal&&(e.abortSignal.onabort=null),r.status>=200&&r.status<300?t(new Ze(r.status,r.statusText,r.response||r.responseText)):n(new nt(r.response||r.responseText||r.statusText,r.status))},r.onerror=()=>{this._logger.log(ht.Warning,`Error from HTTP request. ${r.status}: ${r.statusText}.`),n(new nt(r.statusText,r.status))},r.ontimeout=()=>{this._logger.log(ht.Warning,"Timeout from HTTP request."),n(new rt)},r.send(e.content)})):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}}class Dt extends et{constructor(e){if(super(),"undefined"!=typeof fetch)this._httpClient=new It(e);else{if("undefined"==typeof XMLHttpRequest)throw new Error("No usable HttpClient found.");this._httpClient=new Tt(e)}}send(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new ot):e.method?e.url?this._httpClient.send(e):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}getCookieString(e){return this._httpClient.getCookieString(e)}}var xt,Rt,Pt,At;!function(e){e[e.None=0]="None",e[e.WebSockets=1]="WebSockets",e[e.ServerSentEvents=2]="ServerSentEvents",e[e.LongPolling=4]="LongPolling"}(xt||(xt={})),function(e){e[e.Text=1]="Text",e[e.Binary=2]="Binary"}(Rt||(Rt={}));class Nt{constructor(){this._isAborted=!1,this.onabort=null}abort(){this._isAborted||(this._isAborted=!0,this.onabort&&this.onabort())}get signal(){return this}get aborted(){return this._isAborted}}class Ut{get pollAborted(){return this._pollAbort.aborted}constructor(e,t,n){this._httpClient=e,this._logger=t,this._pollAbort=new Nt,this._options=n,this._running=!1,this.onreceive=null,this.onclose=null}async connect(e,t){if(pt.isRequired(e,"url"),pt.isRequired(t,"transferFormat"),pt.isIn(t,Rt,"transferFormat"),this._url=e,this._logger.log(ht.Trace,"(LongPolling transport) Connecting."),t===Rt.Binary&&"undefined"!=typeof XMLHttpRequest&&"string"!=typeof(new XMLHttpRequest).responseType)throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");const[n,r]=bt(),o={[n]:r,...this._options.headers},i={abortSignal:this._pollAbort.signal,headers:o,timeout:1e5,withCredentials:this._options.withCredentials};t===Rt.Binary&&(i.responseType="arraybuffer");const s=`${e}&_=${Date.now()}`;this._logger.log(ht.Trace,`(LongPolling transport) polling: ${s}.`);const a=await this._httpClient.get(s,i);200!==a.statusCode?(this._logger.log(ht.Error,`(LongPolling transport) Unexpected response code: ${a.statusCode}.`),this._closeError=new nt(a.statusText||"",a.statusCode),this._running=!1):this._running=!0,this._receiving=this._poll(this._url,i)}async _poll(e,t){try{for(;this._running;)try{const n=`${e}&_=${Date.now()}`;this._logger.log(ht.Trace,`(LongPolling transport) polling: ${n}.`);const r=await this._httpClient.get(n,t);204===r.statusCode?(this._logger.log(ht.Information,"(LongPolling transport) Poll terminated by server."),this._running=!1):200!==r.statusCode?(this._logger.log(ht.Error,`(LongPolling transport) Unexpected response code: ${r.statusCode}.`),this._closeError=new nt(r.statusText||"",r.statusCode),this._running=!1):r.content?(this._logger.log(ht.Trace,`(LongPolling transport) data received. ${gt(r.content,this._options.logMessageContent)}.`),this.onreceive&&this.onreceive(r.content)):this._logger.log(ht.Trace,"(LongPolling transport) Poll timed out, reissuing.")}catch(e){this._running?e instanceof rt?this._logger.log(ht.Trace,"(LongPolling transport) Poll timed out, reissuing."):(this._closeError=e,this._running=!1):this._logger.log(ht.Trace,`(LongPolling transport) Poll errored after shutdown: ${e.message}`)}}finally{this._logger.log(ht.Trace,"(LongPolling transport) Polling complete."),this.pollAborted||this._raiseOnClose()}}async send(e){return this._running?yt(this._logger,"LongPolling",this._httpClient,this._url,e,this._options):Promise.reject(new Error("Cannot send until the transport is connected"))}async stop(){this._logger.log(ht.Trace,"(LongPolling transport) Stopping polling."),this._running=!1,this._pollAbort.abort();try{await this._receiving,this._logger.log(ht.Trace,`(LongPolling transport) sending DELETE request to ${this._url}.`);const e={},[t,n]=bt();e[t]=n;const r={headers:{...e,...this._options.headers},timeout:this._options.timeout,withCredentials:this._options.withCredentials};await this._httpClient.delete(this._url,r),this._logger.log(ht.Trace,"(LongPolling transport) DELETE request sent.")}finally{this._logger.log(ht.Trace,"(LongPolling transport) Stop finished."),this._raiseOnClose()}}_raiseOnClose(){if(this.onclose){let e="(LongPolling transport) Firing onclose event.";this._closeError&&(e+=" Error: "+this._closeError),this._logger.log(ht.Trace,e),this.onclose(this._closeError)}}}class Mt{constructor(e,t,n,r){this._httpClient=e,this._accessToken=t,this._logger=n,this._options=r,this.onreceive=null,this.onclose=null}async connect(e,t){return pt.isRequired(e,"url"),pt.isRequired(t,"transferFormat"),pt.isIn(t,Rt,"transferFormat"),this._logger.log(ht.Trace,"(SSE transport) Connecting."),this._url=e,this._accessToken&&(e+=(e.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(this._accessToken)}`),new Promise(((n,r)=>{let o,i=!1;if(t===Rt.Text){if(ft.isBrowser||ft.isWebWorker)o=new this._options.EventSource(e,{withCredentials:this._options.withCredentials});else{const t=this._httpClient.getCookieString(e),n={};n.Cookie=t;const[r,i]=bt();n[r]=i,o=new this._options.EventSource(e,{withCredentials:this._options.withCredentials,headers:{...n,...this._options.headers}})}try{o.onmessage=e=>{if(this.onreceive)try{this._logger.log(ht.Trace,`(SSE transport) data received. ${gt(e.data,this._options.logMessageContent)}.`),this.onreceive(e.data)}catch(e){return void this._close(e)}},o.onerror=e=>{i?this._close():r(new Error("EventSource failed to connect. The connection could not be found on the server, either the connection ID is not present on the server, or a proxy is refusing/buffering the connection. If you have multiple servers check that sticky sessions are enabled."))},o.onopen=()=>{this._logger.log(ht.Information,`SSE connected to ${this._url}`),this._eventSource=o,i=!0,n()}}catch(e){return void r(e)}}else r(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"))}))}async send(e){return this._eventSource?yt(this._logger,"SSE",this._httpClient,this._url,e,this._options):Promise.reject(new Error("Cannot send until the transport is connected"))}stop(){return this._close(),Promise.resolve()}_close(e){this._eventSource&&(this._eventSource.close(),this._eventSource=void 0,this.onclose&&this.onclose(e))}}class Lt{constructor(e,t,n,r,o,i){this._logger=n,this._accessTokenFactory=t,this._logMessageContent=r,this._webSocketConstructor=o,this._httpClient=e,this.onreceive=null,this.onclose=null,this._headers=i}async connect(e,t){let n;return pt.isRequired(e,"url"),pt.isRequired(t,"transferFormat"),pt.isIn(t,Rt,"transferFormat"),this._logger.log(ht.Trace,"(WebSockets transport) Connecting."),this._accessTokenFactory&&(n=await this._accessTokenFactory()),new Promise(((r,o)=>{let i;e=e.replace(/^http/,"ws");const s=this._httpClient.getCookieString(e);let a=!1;if(ft.isReactNative){const t={},[r,o]=bt();t[r]=o,n&&(t[Qe.Authorization]=`Bearer ${n}`),s&&(t[Qe.Cookie]=s),i=new this._webSocketConstructor(e,void 0,{headers:{...t,...this._headers}})}else n&&(e+=(e.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(n)}`);i||(i=new this._webSocketConstructor(e)),t===Rt.Binary&&(i.binaryType="arraybuffer"),i.onopen=t=>{this._logger.log(ht.Information,`WebSocket connected to ${e}.`),this._webSocket=i,a=!0,r()},i.onerror=e=>{let t=null;t="undefined"!=typeof ErrorEvent&&e instanceof ErrorEvent?e.error:"There was an error with the transport",this._logger.log(ht.Information,`(WebSockets transport) ${t}.`)},i.onmessage=e=>{if(this._logger.log(ht.Trace,`(WebSockets transport) data received. ${gt(e.data,this._logMessageContent)}.`),this.onreceive)try{this.onreceive(e.data)}catch(e){return void this._close(e)}},i.onclose=e=>{if(a)this._close(e);else{let t=null;t="undefined"!=typeof ErrorEvent&&e instanceof ErrorEvent?e.error:"WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.",o(new Error(t))}}}))}send(e){return this._webSocket&&this._webSocket.readyState===this._webSocketConstructor.OPEN?(this._logger.log(ht.Trace,`(WebSockets transport) sending data. ${gt(e,this._logMessageContent)}.`),this._webSocket.send(e),Promise.resolve()):Promise.reject("WebSocket is not in the OPEN state")}stop(){return this._webSocket&&this._close(void 0),Promise.resolve()}_close(e){this._webSocket&&(this._webSocket.onclose=()=>{},this._webSocket.onmessage=()=>{},this._webSocket.onerror=()=>{},this._webSocket.close(),this._webSocket=void 0),this._logger.log(ht.Trace,"(WebSockets transport) socket closed."),this.onclose&&(!this._isCloseEvent(e)||!1!==e.wasClean&&1e3===e.code?e instanceof Error?this.onclose(e):this.onclose():this.onclose(new Error(`WebSocket closed with status code: ${e.code} (${e.reason||"no reason given"}).`)))}_isCloseEvent(e){return e&&"boolean"==typeof e.wasClean&&"number"==typeof e.code}}class $t{constructor(e,t={}){var n;if(this._stopPromiseResolver=()=>{},this.features={},this._negotiateVersion=1,pt.isRequired(e,"url"),this._logger=void 0===(n=t.logger)?new wt(ht.Information):null===n?dt.instance:void 0!==n.log?n:new wt(n),this.baseUrl=this._resolveUrl(e),(t=t||{}).logMessageContent=void 0!==t.logMessageContent&&t.logMessageContent,"boolean"!=typeof t.withCredentials&&void 0!==t.withCredentials)throw new Error("withCredentials option was not a 'boolean' or 'undefined' value");t.withCredentials=void 0===t.withCredentials||t.withCredentials,t.timeout=void 0===t.timeout?1e5:t.timeout,"undefined"==typeof WebSocket||t.WebSocket||(t.WebSocket=WebSocket),"undefined"==typeof EventSource||t.EventSource||(t.EventSource=EventSource),this._httpClient=new tt(t.httpClient||new Dt(this._logger),t.accessTokenFactory),this._connectionState="Disconnected",this._connectionStarted=!1,this._options=t,this.onreceive=null,this.onclose=null}async start(e){if(e=e||Rt.Binary,pt.isIn(e,Rt,"transferFormat"),this._logger.log(ht.Debug,`Starting connection with transfer format '${Rt[e]}'.`),"Disconnected"!==this._connectionState)return Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state."));if(this._connectionState="Connecting",this._startInternalPromise=this._startInternal(e),await this._startInternalPromise,"Disconnecting"===this._connectionState){const e="Failed to start the HttpConnection before stop() was called.";return this._logger.log(ht.Error,e),await this._stopPromise,Promise.reject(new ot(e))}if("Connected"!==this._connectionState){const e="HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!";return this._logger.log(ht.Error,e),Promise.reject(new ot(e))}this._connectionStarted=!0}send(e){return"Connected"!==this._connectionState?Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State.")):(this._sendQueue||(this._sendQueue=new Bt(this.transport)),this._sendQueue.send(e))}async stop(e){return"Disconnected"===this._connectionState?(this._logger.log(ht.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnected state.`),Promise.resolve()):"Disconnecting"===this._connectionState?(this._logger.log(ht.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnecting state.`),this._stopPromise):(this._connectionState="Disconnecting",this._stopPromise=new Promise((e=>{this._stopPromiseResolver=e})),await this._stopInternal(e),void await this._stopPromise)}async _stopInternal(e){this._stopError=e;try{await this._startInternalPromise}catch(e){}if(this.transport){try{await this.transport.stop()}catch(e){this._logger.log(ht.Error,`HttpConnection.transport.stop() threw error '${e}'.`),this._stopConnection()}this.transport=void 0}else this._logger.log(ht.Debug,"HttpConnection.transport is undefined in HttpConnection.stop() because start() failed.")}async _startInternal(e){let t=this.baseUrl;this._accessTokenFactory=this._options.accessTokenFactory,this._httpClient._accessTokenFactory=this._accessTokenFactory;try{if(this._options.skipNegotiation){if(this._options.transport!==xt.WebSockets)throw new Error("Negotiation can only be skipped when using the WebSocket transport directly.");this.transport=this._constructTransport(xt.WebSockets),await this._startTransport(t,e)}else{let n=null,r=0;do{if(n=await this._getNegotiationResponse(t),"Disconnecting"===this._connectionState||"Disconnected"===this._connectionState)throw new ot("The connection was stopped during negotiation.");if(n.error)throw new Error(n.error);if(n.ProtocolVersion)throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.");if(n.url&&(t=n.url),n.accessToken){const e=n.accessToken;this._accessTokenFactory=()=>e,this._httpClient._accessToken=e,this._httpClient._accessTokenFactory=void 0}r++}while(n.url&&r<100);if(100===r&&n.url)throw new Error("Negotiate redirection limit exceeded.");await this._createTransport(t,this._options.transport,n,e)}this.transport instanceof Ut&&(this.features.inherentKeepAlive=!0),"Connecting"===this._connectionState&&(this._logger.log(ht.Debug,"The HttpConnection connected successfully."),this._connectionState="Connected")}catch(e){return this._logger.log(ht.Error,"Failed to start the connection: "+e),this._connectionState="Disconnected",this.transport=void 0,this._stopPromiseResolver(),Promise.reject(e)}}async _getNegotiationResponse(e){const t={},[n,r]=bt();t[n]=r;const o=this._resolveNegotiateUrl(e);this._logger.log(ht.Debug,`Sending negotiation request: ${o}.`);try{const e=await this._httpClient.post(o,{content:"",headers:{...t,...this._options.headers},timeout:this._options.timeout,withCredentials:this._options.withCredentials});if(200!==e.statusCode)return Promise.reject(new Error(`Unexpected status code returned from negotiate '${e.statusCode}'`));const n=JSON.parse(e.content);return(!n.negotiateVersion||n.negotiateVersion<1)&&(n.connectionToken=n.connectionId),n}catch(e){let t="Failed to complete negotiation with the server: "+e;return e instanceof nt&&404===e.statusCode&&(t+=" Either this is not a SignalR endpoint or there is a proxy blocking the connection."),this._logger.log(ht.Error,t),Promise.reject(new ct(t))}}_createConnectUrl(e,t){return t?e+(-1===e.indexOf("?")?"?":"&")+`id=${t}`:e}async _createTransport(e,t,n,r){let o=this._createConnectUrl(e,n.connectionToken);if(this._isITransport(t))return this._logger.log(ht.Debug,"Connection was provided an instance of ITransport, using that directly."),this.transport=t,await this._startTransport(o,r),void(this.connectionId=n.connectionId);const i=[],s=n.availableTransports||[];let a=n;for(const n of s){const s=this._resolveTransportOrError(n,t,r);if(s instanceof Error)i.push(`${n.transport} failed:`),i.push(s);else if(this._isITransport(s)){if(this.transport=s,!a){try{a=await this._getNegotiationResponse(e)}catch(e){return Promise.reject(e)}o=this._createConnectUrl(e,a.connectionToken)}try{return await this._startTransport(o,r),void(this.connectionId=a.connectionId)}catch(e){if(this._logger.log(ht.Error,`Failed to start the transport '${n.transport}': ${e}`),a=void 0,i.push(new at(`${n.transport} failed: ${e}`,xt[n.transport])),"Connecting"!==this._connectionState){const e="Failed to select transport before stop() was called.";return this._logger.log(ht.Debug,e),Promise.reject(new ot(e))}}}}return i.length>0?Promise.reject(new lt(`Unable to connect to the server with any of the available transports. ${i.join(" ")}`,i)):Promise.reject(new Error("None of the transports supported by the client are supported by the server."))}_constructTransport(e){switch(e){case xt.WebSockets:if(!this._options.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new Lt(this._httpClient,this._accessTokenFactory,this._logger,this._options.logMessageContent,this._options.WebSocket,this._options.headers||{});case xt.ServerSentEvents:if(!this._options.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new Mt(this._httpClient,this._httpClient._accessToken,this._logger,this._options);case xt.LongPolling:return new Ut(this._httpClient,this._logger,this._options);default:throw new Error(`Unknown transport: ${e}.`)}}_startTransport(e,t){return this.transport.onreceive=this.onreceive,this.transport.onclose=e=>this._stopConnection(e),this.transport.connect(e,t)}_resolveTransportOrError(e,t,n){const r=xt[e.transport];if(null==r)return this._logger.log(ht.Debug,`Skipping transport '${e.transport}' because it is not supported by this client.`),new Error(`Skipping transport '${e.transport}' because it is not supported by this client.`);if(!function(e,t){return!e||0!=(t&e)}(t,r))return this._logger.log(ht.Debug,`Skipping transport '${xt[r]}' because it was disabled by the client.`),new st(`'${xt[r]}' is disabled by the client.`,r);if(!(e.transferFormats.map((e=>Rt[e])).indexOf(n)>=0))return this._logger.log(ht.Debug,`Skipping transport '${xt[r]}' because it does not support the requested transfer format '${Rt[n]}'.`),new Error(`'${xt[r]}' does not support ${Rt[n]}.`);if(r===xt.WebSockets&&!this._options.WebSocket||r===xt.ServerSentEvents&&!this._options.EventSource)return this._logger.log(ht.Debug,`Skipping transport '${xt[r]}' because it is not supported in your environment.'`),new it(`'${xt[r]}' is not supported in your environment.`,r);this._logger.log(ht.Debug,`Selecting transport '${xt[r]}'.`);try{return this._constructTransport(r)}catch(e){return e}}_isITransport(e){return e&&"object"==typeof e&&"connect"in e}_stopConnection(e){if(this._logger.log(ht.Debug,`HttpConnection.stopConnection(${e}) called while in state ${this._connectionState}.`),this.transport=void 0,e=this._stopError||e,this._stopError=void 0,"Disconnected"!==this._connectionState){if("Connecting"===this._connectionState)throw this._logger.log(ht.Warning,`Call to HttpConnection.stopConnection(${e}) was ignored because the connection is still in the connecting state.`),new Error(`HttpConnection.stopConnection(${e}) was called while the connection is still in the connecting state.`);if("Disconnecting"===this._connectionState&&this._stopPromiseResolver(),e?this._logger.log(ht.Error,`Connection disconnected with error '${e}'.`):this._logger.log(ht.Information,"Connection disconnected."),this._sendQueue&&(this._sendQueue.stop().catch((e=>{this._logger.log(ht.Error,`TransportSendQueue.stop() threw error '${e}'.`)})),this._sendQueue=void 0),this.connectionId=void 0,this._connectionState="Disconnected",this._connectionStarted){this._connectionStarted=!1;try{this.onclose&&this.onclose(e)}catch(t){this._logger.log(ht.Error,`HttpConnection.onclose(${e}) threw error '${t}'.`)}}}else this._logger.log(ht.Debug,`Call to HttpConnection.stopConnection(${e}) was ignored because the connection is already in the disconnected state.`)}_resolveUrl(e){if(0===e.lastIndexOf("https://",0)||0===e.lastIndexOf("http://",0))return e;if(!ft.isBrowser)throw new Error(`Cannot resolve '${e}'.`);const t=window.document.createElement("a");return t.href=e,this._logger.log(ht.Information,`Normalizing '${e}' to '${t.href}'.`),t.href}_resolveNegotiateUrl(e){const t=e.indexOf("?");let n=e.substring(0,-1===t?e.length:t);return"/"!==n[n.length-1]&&(n+="/"),n+="negotiate",n+=-1===t?"":e.substring(t),-1===n.indexOf("negotiateVersion")&&(n+=-1===t?"?":"&",n+="negotiateVersion="+this._negotiateVersion),n}}class Bt{constructor(e){this._transport=e,this._buffer=[],this._executing=!0,this._sendBufferedData=new Ot,this._transportResult=new Ot,this._sendLoopPromise=this._sendLoop()}send(e){return this._bufferData(e),this._transportResult||(this._transportResult=new Ot),this._transportResult.promise}stop(){return this._executing=!1,this._sendBufferedData.resolve(),this._sendLoopPromise}_bufferData(e){if(this._buffer.length&&typeof this._buffer[0]!=typeof e)throw new Error(`Expected data to be of type ${typeof this._buffer} but was of type ${typeof e}`);this._buffer.push(e),this._sendBufferedData.resolve()}async _sendLoop(){for(;;){if(await this._sendBufferedData.promise,!this._executing){this._transportResult&&this._transportResult.reject("Connection stopped.");break}this._sendBufferedData=new Ot;const e=this._transportResult;this._transportResult=void 0;const t="string"==typeof this._buffer[0]?this._buffer.join(""):Bt._concatBuffers(this._buffer);this._buffer.length=0;try{await this._transport.send(t),e.resolve()}catch(t){e.reject(t)}}}static _concatBuffers(e){const t=e.map((e=>e.byteLength)).reduce(((e,t)=>e+t)),n=new Uint8Array(t);let r=0;for(const t of e)n.set(new Uint8Array(t),r),r+=t.byteLength;return n.buffer}}class Ot{constructor(){this.promise=new Promise(((e,t)=>[this._resolver,this._rejecter]=[e,t]))}resolve(){this._resolver()}reject(e){this._rejecter(e)}}class Ft{static write(e){return`${e}${Ft.RecordSeparator}`}static parse(e){if(e[e.length-1]!==Ft.RecordSeparator)throw new Error("Message is incomplete.");const t=e.split(Ft.RecordSeparator);return t.pop(),t}}Ft.RecordSeparatorCode=30,Ft.RecordSeparator=String.fromCharCode(Ft.RecordSeparatorCode);class Ht{writeHandshakeRequest(e){return Ft.write(JSON.stringify(e))}parseHandshakeResponse(e){let t,n;if(mt(e)){const r=new Uint8Array(e),o=r.indexOf(Ft.RecordSeparatorCode);if(-1===o)throw new Error("Message is incomplete.");const i=o+1;t=String.fromCharCode.apply(null,Array.prototype.slice.call(r.slice(0,i))),n=r.byteLength>i?r.slice(i).buffer:null}else{const r=e,o=r.indexOf(Ft.RecordSeparator);if(-1===o)throw new Error("Message is incomplete.");const i=o+1;t=r.substring(0,i),n=r.length>i?r.substring(i):null}const r=Ft.parse(t),o=JSON.parse(r[0]);if(o.type)throw new Error("Expected a handshake response from the server.");return[n,o]}}!function(e){e[e.Invocation=1]="Invocation",e[e.StreamItem=2]="StreamItem",e[e.Completion=3]="Completion",e[e.StreamInvocation=4]="StreamInvocation",e[e.CancelInvocation=5]="CancelInvocation",e[e.Ping=6]="Ping",e[e.Close=7]="Close"}(Pt||(Pt={}));class jt{constructor(){this.observers=[]}next(e){for(const t of this.observers)t.next(e)}error(e){for(const t of this.observers)t.error&&t.error(e)}complete(){for(const e of this.observers)e.complete&&e.complete()}subscribe(e){return this.observers.push(e),new vt(this,e)}}!function(e){e.Disconnected="Disconnected",e.Connecting="Connecting",e.Connected="Connected",e.Disconnecting="Disconnecting",e.Reconnecting="Reconnecting"}(At||(At={}));class Wt{static create(e,t,n,r,o,i){return new Wt(e,t,n,r,o,i)}constructor(e,t,n,r,o,i){this._nextKeepAlive=0,this._freezeEventListener=()=>{this._logger.log(ht.Warning,"The page is being frozen, this will likely lead to the connection being closed and messages being lost. For more information see the docs at https://learn.microsoft.com/aspnet/core/signalr/javascript-client#bsleep")},pt.isRequired(e,"connection"),pt.isRequired(t,"logger"),pt.isRequired(n,"protocol"),this.serverTimeoutInMilliseconds=null!=o?o:3e4,this.keepAliveIntervalInMilliseconds=null!=i?i:15e3,this._logger=t,this._protocol=n,this.connection=e,this._reconnectPolicy=r,this._handshakeProtocol=new Ht,this.connection.onreceive=e=>this._processIncomingData(e),this.connection.onclose=e=>this._connectionClosed(e),this._callbacks={},this._methods={},this._closedCallbacks=[],this._reconnectingCallbacks=[],this._reconnectedCallbacks=[],this._invocationId=0,this._receivedHandshakeResponse=!1,this._connectionState=At.Disconnected,this._connectionStarted=!1,this._cachedPingMessage=this._protocol.writeMessage({type:Pt.Ping})}get state(){return this._connectionState}get connectionId(){return this.connection&&this.connection.connectionId||null}get baseUrl(){return this.connection.baseUrl||""}set baseUrl(e){if(this._connectionState!==At.Disconnected&&this._connectionState!==At.Reconnecting)throw new Error("The HubConnection must be in the Disconnected or Reconnecting state to change the url.");if(!e)throw new Error("The HubConnection url must be a valid url.");this.connection.baseUrl=e}start(){return this._startPromise=this._startWithStateTransitions(),this._startPromise}async _startWithStateTransitions(){if(this._connectionState!==At.Disconnected)return Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state."));this._connectionState=At.Connecting,this._logger.log(ht.Debug,"Starting HubConnection.");try{await this._startInternal(),ft.isBrowser&&window.document.addEventListener("freeze",this._freezeEventListener),this._connectionState=At.Connected,this._connectionStarted=!0,this._logger.log(ht.Debug,"HubConnection connected successfully.")}catch(e){return this._connectionState=At.Disconnected,this._logger.log(ht.Debug,`HubConnection failed to start successfully because of error '${e}'.`),Promise.reject(e)}}async _startInternal(){this._stopDuringStartError=void 0,this._receivedHandshakeResponse=!1;const e=new Promise(((e,t)=>{this._handshakeResolver=e,this._handshakeRejecter=t}));await this.connection.start(this._protocol.transferFormat);try{const t={protocol:this._protocol.name,version:this._protocol.version};if(this._logger.log(ht.Debug,"Sending handshake request."),await this._sendMessage(this._handshakeProtocol.writeHandshakeRequest(t)),this._logger.log(ht.Information,`Using HubProtocol '${this._protocol.name}'.`),this._cleanupTimeout(),this._resetTimeoutPeriod(),this._resetKeepAliveInterval(),await e,this._stopDuringStartError)throw this._stopDuringStartError;this.connection.features.inherentKeepAlive||await this._sendMessage(this._cachedPingMessage)}catch(e){throw this._logger.log(ht.Debug,`Hub handshake failed with error '${e}' during start(). Stopping HubConnection.`),this._cleanupTimeout(),this._cleanupPingTimer(),await this.connection.stop(e),e}}async stop(){const e=this._startPromise;this._stopPromise=this._stopInternal(),await this._stopPromise;try{await e}catch(e){}}_stopInternal(e){return this._connectionState===At.Disconnected?(this._logger.log(ht.Debug,`Call to HubConnection.stop(${e}) ignored because it is already in the disconnected state.`),Promise.resolve()):this._connectionState===At.Disconnecting?(this._logger.log(ht.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnecting state.`),this._stopPromise):(this._connectionState=At.Disconnecting,this._logger.log(ht.Debug,"Stopping HubConnection."),this._reconnectDelayHandle?(this._logger.log(ht.Debug,"Connection stopped during reconnect delay. Done reconnecting."),clearTimeout(this._reconnectDelayHandle),this._reconnectDelayHandle=void 0,this._completeClose(),Promise.resolve()):(this._cleanupTimeout(),this._cleanupPingTimer(),this._stopDuringStartError=e||new ot("The connection was stopped before the hub handshake could complete."),this.connection.stop(e)))}stream(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._createStreamInvocation(e,t,r);let i;const s=new jt;return s.cancelCallback=()=>{const e=this._createCancelInvocation(o.invocationId);return delete this._callbacks[o.invocationId],i.then((()=>this._sendWithProtocol(e)))},this._callbacks[o.invocationId]=(e,t)=>{t?s.error(t):e&&(e.type===Pt.Completion?e.error?s.error(new Error(e.error)):s.complete():s.next(e.item))},i=this._sendWithProtocol(o).catch((e=>{s.error(e),delete this._callbacks[o.invocationId]})),this._launchStreams(n,i),s}_sendMessage(e){return this._resetKeepAliveInterval(),this.connection.send(e)}_sendWithProtocol(e){return this._sendMessage(this._protocol.writeMessage(e))}send(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._sendWithProtocol(this._createInvocation(e,t,!0,r));return this._launchStreams(n,o),o}invoke(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._createInvocation(e,t,!1,r);return new Promise(((e,t)=>{this._callbacks[o.invocationId]=(n,r)=>{r?t(r):n&&(n.type===Pt.Completion?n.error?t(new Error(n.error)):e(n.result):t(new Error(`Unexpected message type: ${n.type}`)))};const r=this._sendWithProtocol(o).catch((e=>{t(e),delete this._callbacks[o.invocationId]}));this._launchStreams(n,r)}))}on(e,t){e&&t&&(e=e.toLowerCase(),this._methods[e]||(this._methods[e]=[]),-1===this._methods[e].indexOf(t)&&this._methods[e].push(t))}off(e,t){if(!e)return;e=e.toLowerCase();const n=this._methods[e];if(n)if(t){const r=n.indexOf(t);-1!==r&&(n.splice(r,1),0===n.length&&delete this._methods[e])}else delete this._methods[e]}onclose(e){e&&this._closedCallbacks.push(e)}onreconnecting(e){e&&this._reconnectingCallbacks.push(e)}onreconnected(e){e&&this._reconnectedCallbacks.push(e)}_processIncomingData(e){if(this._cleanupTimeout(),this._receivedHandshakeResponse||(e=this._processHandshakeResponse(e),this._receivedHandshakeResponse=!0),e){const t=this._protocol.parseMessages(e,this._logger);for(const e of t)switch(e.type){case Pt.Invocation:this._invokeClientMethod(e);break;case Pt.StreamItem:case Pt.Completion:{const t=this._callbacks[e.invocationId];if(t){e.type===Pt.Completion&&delete this._callbacks[e.invocationId];try{t(e)}catch(e){this._logger.log(ht.Error,`Stream callback threw error: ${Ct(e)}`)}}break}case Pt.Ping:break;case Pt.Close:{this._logger.log(ht.Information,"Close message received from server.");const t=e.error?new Error("Server returned an error on close: "+e.error):void 0;!0===e.allowReconnect?this.connection.stop(t):this._stopPromise=this._stopInternal(t);break}default:this._logger.log(ht.Warning,`Invalid message type: ${e.type}.`)}}this._resetTimeoutPeriod()}_processHandshakeResponse(e){let t,n;try{[n,t]=this._handshakeProtocol.parseHandshakeResponse(e)}catch(e){const t="Error parsing handshake response: "+e;this._logger.log(ht.Error,t);const n=new Error(t);throw this._handshakeRejecter(n),n}if(t.error){const e="Server returned handshake error: "+t.error;this._logger.log(ht.Error,e);const n=new Error(e);throw this._handshakeRejecter(n),n}return this._logger.log(ht.Debug,"Server handshake complete."),this._handshakeResolver(),n}_resetKeepAliveInterval(){this.connection.features.inherentKeepAlive||(this._nextKeepAlive=(new Date).getTime()+this.keepAliveIntervalInMilliseconds,this._cleanupPingTimer())}_resetTimeoutPeriod(){if(!(this.connection.features&&this.connection.features.inherentKeepAlive||(this._timeoutHandle=setTimeout((()=>this.serverTimeout()),this.serverTimeoutInMilliseconds),void 0!==this._pingServerHandle))){let e=this._nextKeepAlive-(new Date).getTime();e<0&&(e=0),this._pingServerHandle=setTimeout((async()=>{if(this._connectionState===At.Connected)try{await this._sendMessage(this._cachedPingMessage)}catch{this._cleanupPingTimer()}}),e)}}serverTimeout(){this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server."))}async _invokeClientMethod(e){const t=e.target.toLowerCase(),n=this._methods[t];if(!n)return this._logger.log(ht.Warning,`No client method with the name '${t}' found.`),void(e.invocationId&&(this._logger.log(ht.Warning,`No result given for '${t}' method and invocation ID '${e.invocationId}'.`),await this._sendWithProtocol(this._createCompletionMessage(e.invocationId,"Client didn't provide a result.",null))));const r=n.slice(),o=!!e.invocationId;let i,s,a;for(const n of r)try{const r=i;i=await n.apply(this,e.arguments),o&&i&&r&&(this._logger.log(ht.Error,`Multiple results provided for '${t}'. Sending error to server.`),a=this._createCompletionMessage(e.invocationId,"Client provided multiple results.",null)),s=void 0}catch(e){s=e,this._logger.log(ht.Error,`A callback for the method '${t}' threw error '${e}'.`)}a?await this._sendWithProtocol(a):o?(s?a=this._createCompletionMessage(e.invocationId,`${s}`,null):void 0!==i?a=this._createCompletionMessage(e.invocationId,null,i):(this._logger.log(ht.Warning,`No result given for '${t}' method and invocation ID '${e.invocationId}'.`),a=this._createCompletionMessage(e.invocationId,"Client didn't provide a result.",null)),await this._sendWithProtocol(a)):i&&this._logger.log(ht.Error,`Result given for '${t}' method but server is not expecting a result.`)}_connectionClosed(e){this._logger.log(ht.Debug,`HubConnection.connectionClosed(${e}) called while in state ${this._connectionState}.`),this._stopDuringStartError=this._stopDuringStartError||e||new ot("The underlying connection was closed before the hub handshake could complete."),this._handshakeResolver&&this._handshakeResolver(),this._cancelCallbacksWithError(e||new Error("Invocation canceled due to the underlying connection being closed.")),this._cleanupTimeout(),this._cleanupPingTimer(),this._connectionState===At.Disconnecting?this._completeClose(e):this._connectionState===At.Connected&&this._reconnectPolicy?this._reconnect(e):this._connectionState===At.Connected&&this._completeClose(e)}_completeClose(e){if(this._connectionStarted){this._connectionState=At.Disconnected,this._connectionStarted=!1,ft.isBrowser&&window.document.removeEventListener("freeze",this._freezeEventListener);try{this._closedCallbacks.forEach((t=>t.apply(this,[e])))}catch(t){this._logger.log(ht.Error,`An onclose callback called with error '${e}' threw error '${t}'.`)}}}async _reconnect(e){const t=Date.now();let n=0,r=void 0!==e?e:new Error("Attempting to reconnect due to a unknown error."),o=this._getNextRetryDelay(n++,0,r);if(null===o)return this._logger.log(ht.Debug,"Connection not reconnecting because the IRetryPolicy returned null on the first reconnect attempt."),void this._completeClose(e);if(this._connectionState=At.Reconnecting,e?this._logger.log(ht.Information,`Connection reconnecting because of error '${e}'.`):this._logger.log(ht.Information,"Connection reconnecting."),0!==this._reconnectingCallbacks.length){try{this._reconnectingCallbacks.forEach((t=>t.apply(this,[e])))}catch(t){this._logger.log(ht.Error,`An onreconnecting callback called with error '${e}' threw error '${t}'.`)}if(this._connectionState!==At.Reconnecting)return void this._logger.log(ht.Debug,"Connection left the reconnecting state in onreconnecting callback. Done reconnecting.")}for(;null!==o;){if(this._logger.log(ht.Information,`Reconnect attempt number ${n} will start in ${o} ms.`),await new Promise((e=>{this._reconnectDelayHandle=setTimeout(e,o)})),this._reconnectDelayHandle=void 0,this._connectionState!==At.Reconnecting)return void this._logger.log(ht.Debug,"Connection left the reconnecting state during reconnect delay. Done reconnecting.");try{if(await this._startInternal(),this._connectionState=At.Connected,this._logger.log(ht.Information,"HubConnection reconnected successfully."),0!==this._reconnectedCallbacks.length)try{this._reconnectedCallbacks.forEach((e=>e.apply(this,[this.connection.connectionId])))}catch(e){this._logger.log(ht.Error,`An onreconnected callback called with connectionId '${this.connection.connectionId}; threw error '${e}'.`)}return}catch(e){if(this._logger.log(ht.Information,`Reconnect attempt failed because of error '${e}'.`),this._connectionState!==At.Reconnecting)return this._logger.log(ht.Debug,`Connection moved to the '${this._connectionState}' from the reconnecting state during reconnect attempt. Done reconnecting.`),void(this._connectionState===At.Disconnecting&&this._completeClose());r=e instanceof Error?e:new Error(e.toString()),o=this._getNextRetryDelay(n++,Date.now()-t,r)}}this._logger.log(ht.Information,`Reconnect retries have been exhausted after ${Date.now()-t} ms and ${n} failed attempts. Connection disconnecting.`),this._completeClose()}_getNextRetryDelay(e,t,n){try{return this._reconnectPolicy.nextRetryDelayInMilliseconds({elapsedMilliseconds:t,previousRetryCount:e,retryReason:n})}catch(n){return this._logger.log(ht.Error,`IRetryPolicy.nextRetryDelayInMilliseconds(${e}, ${t}) threw error '${n}'.`),null}}_cancelCallbacksWithError(e){const t=this._callbacks;this._callbacks={},Object.keys(t).forEach((n=>{const r=t[n];try{r(null,e)}catch(t){this._logger.log(ht.Error,`Stream 'error' callback called with '${e}' threw error: ${Ct(t)}`)}}))}_cleanupPingTimer(){this._pingServerHandle&&(clearTimeout(this._pingServerHandle),this._pingServerHandle=void 0)}_cleanupTimeout(){this._timeoutHandle&&clearTimeout(this._timeoutHandle)}_createInvocation(e,t,n,r){if(n)return 0!==r.length?{arguments:t,streamIds:r,target:e,type:Pt.Invocation}:{arguments:t,target:e,type:Pt.Invocation};{const n=this._invocationId;return this._invocationId++,0!==r.length?{arguments:t,invocationId:n.toString(),streamIds:r,target:e,type:Pt.Invocation}:{arguments:t,invocationId:n.toString(),target:e,type:Pt.Invocation}}}_launchStreams(e,t){if(0!==e.length){t||(t=Promise.resolve());for(const n in e)e[n].subscribe({complete:()=>{t=t.then((()=>this._sendWithProtocol(this._createCompletionMessage(n))))},error:e=>{let r;r=e instanceof Error?e.message:e&&e.toString?e.toString():"Unknown error",t=t.then((()=>this._sendWithProtocol(this._createCompletionMessage(n,r))))},next:e=>{t=t.then((()=>this._sendWithProtocol(this._createStreamItemMessage(n,e))))}})}}_replaceStreamingParams(e){const t=[],n=[];for(let r=0;r=55296&&o<=56319&&r65535&&(h-=65536,i.push(h>>>10&1023|55296),h=56320|1023&h),i.push(h)}else i.push(a);i.length>=on&&(s+=String.fromCharCode.apply(String,i),i.length=0)}return i.length>0&&(s+=String.fromCharCode.apply(String,i)),s}var an,cn=Zt?new TextDecoder:null,ln=Zt?"undefined"!=typeof process&&"force"!==(null===(Xt=null===process||void 0===process?void 0:process.env)||void 0===Xt?void 0:Xt.TEXT_DECODER)?200:0:Yt,hn=function(e,t){this.type=e,this.data=t},dn=(an=function(e,t){return an=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},an(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}an(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),un=function(e){function t(n){var r=e.call(this,n)||this,o=Object.create(t.prototype);return Object.setPrototypeOf(r,o),Object.defineProperty(r,"name",{configurable:!0,enumerable:!1,value:t.name}),r}return dn(t,e),t}(Error),pn={type:-1,encode:function(e){var t,n,r,o;return e instanceof Date?function(e){var t,n=e.sec,r=e.nsec;if(n>=0&&r>=0&&n<=17179869183){if(0===r&&n<=4294967295){var o=new Uint8Array(4);return(t=new DataView(o.buffer)).setUint32(0,n),o}var i=n/4294967296,s=4294967295&n;return o=new Uint8Array(8),(t=new DataView(o.buffer)).setUint32(0,r<<2|3&i),t.setUint32(4,s),o}return o=new Uint8Array(12),(t=new DataView(o.buffer)).setUint32(0,r),Gt(t,4,n),o}((r=1e6*((t=e.getTime())-1e3*(n=Math.floor(t/1e3))),{sec:n+(o=Math.floor(r/1e9)),nsec:r-1e9*o})):null},decode:function(e){var t=function(e){var t=new DataView(e.buffer,e.byteOffset,e.byteLength);switch(e.byteLength){case 4:return{sec:t.getUint32(0),nsec:0};case 8:var n=t.getUint32(0);return{sec:4294967296*(3&n)+t.getUint32(4),nsec:n>>>2};case 12:return{sec:Qt(t,4),nsec:t.getUint32(0)};default:throw new un("Unrecognized data size for timestamp (expected 4, 8, or 12): ".concat(e.length))}}(e);return new Date(1e3*t.sec+t.nsec/1e6)}},fn=function(){function e(){this.builtInEncoders=[],this.builtInDecoders=[],this.encoders=[],this.decoders=[],this.register(pn)}return e.prototype.register=function(e){var t=e.type,n=e.encode,r=e.decode;if(t>=0)this.encoders[t]=n,this.decoders[t]=r;else{var o=1+t;this.builtInEncoders[o]=n,this.builtInDecoders[o]=r}},e.prototype.tryToEncode=function(e,t){for(var n=0;nthis.maxDepth)throw new Error("Too deep objects in depth ".concat(t));null==e?this.encodeNil():"boolean"==typeof e?this.encodeBoolean(e):"number"==typeof e?this.encodeNumber(e):"string"==typeof e?this.encodeString(e):this.encodeObject(e,t)},e.prototype.ensureBufferSizeToWrite=function(e){var t=this.pos+e;this.view.byteLength=0?e<128?this.writeU8(e):e<256?(this.writeU8(204),this.writeU8(e)):e<65536?(this.writeU8(205),this.writeU16(e)):e<4294967296?(this.writeU8(206),this.writeU32(e)):(this.writeU8(207),this.writeU64(e)):e>=-32?this.writeU8(224|e+32):e>=-128?(this.writeU8(208),this.writeI8(e)):e>=-32768?(this.writeU8(209),this.writeI16(e)):e>=-2147483648?(this.writeU8(210),this.writeI32(e)):(this.writeU8(211),this.writeI64(e)):this.forceFloat32?(this.writeU8(202),this.writeF32(e)):(this.writeU8(203),this.writeF64(e))},e.prototype.writeStringHeader=function(e){if(e<32)this.writeU8(160+e);else if(e<256)this.writeU8(217),this.writeU8(e);else if(e<65536)this.writeU8(218),this.writeU16(e);else{if(!(e<4294967296))throw new Error("Too long string: ".concat(e," bytes in UTF-8"));this.writeU8(219),this.writeU32(e)}},e.prototype.encodeString=function(e){if(e.length>nn){var t=en(e);this.ensureBufferSizeToWrite(5+t),this.writeStringHeader(t),rn(e,this.bytes,this.pos),this.pos+=t}else t=en(e),this.ensureBufferSizeToWrite(5+t),this.writeStringHeader(t),function(e,t,n){for(var r=e.length,o=n,i=0;i>6&31|192;else{if(s>=55296&&s<=56319&&i>12&15|224,t[o++]=s>>6&63|128):(t[o++]=s>>18&7|240,t[o++]=s>>12&63|128,t[o++]=s>>6&63|128)}t[o++]=63&s|128}else t[o++]=s}}(e,this.bytes,this.pos),this.pos+=t},e.prototype.encodeObject=function(e,t){var n=this.extensionCodec.tryToEncode(e,this.context);if(null!=n)this.encodeExtension(n);else if(Array.isArray(e))this.encodeArray(e,t);else if(ArrayBuffer.isView(e))this.encodeBinary(e);else{if("object"!=typeof e)throw new Error("Unrecognized object: ".concat(Object.prototype.toString.apply(e)));this.encodeMap(e,t)}},e.prototype.encodeBinary=function(e){var t=e.byteLength;if(t<256)this.writeU8(196),this.writeU8(t);else if(t<65536)this.writeU8(197),this.writeU16(t);else{if(!(t<4294967296))throw new Error("Too large binary: ".concat(t));this.writeU8(198),this.writeU32(t)}var n=gn(e);this.writeU8a(n)},e.prototype.encodeArray=function(e,t){var n=e.length;if(n<16)this.writeU8(144+n);else if(n<65536)this.writeU8(220),this.writeU16(n);else{if(!(n<4294967296))throw new Error("Too large array: ".concat(n));this.writeU8(221),this.writeU32(n)}for(var r=0,o=e;r0&&e<=this.maxKeyLength},e.prototype.find=function(e,t,n){e:for(var r=0,o=this.caches[n-1];r=this.maxLengthPerKey?n[Math.random()*n.length|0]=r:n.push(r)},e.prototype.decode=function(e,t,n){var r=this.find(e,t,n);if(null!=r)return this.hit++,r;this.miss++;var o=sn(e,t,n),i=Uint8Array.prototype.slice.call(e,t,t+n);return this.store(i,o),o},e}(),Sn=function(e,t){var n,r,o,i,s={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function a(i){return function(a){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return s.label++,{value:i[1],done:!1};case 5:s.label++,r=i[1],i=[0];continue;case 7:i=s.ops.pop(),s.trys.pop();continue;default:if(!((o=(o=s.trys).length>0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]1||a(e,t)}))})}function a(e,t){try{(n=o[e](t)).value instanceof In?Promise.resolve(n.value.v).then(c,l):h(i[0][2],n)}catch(e){h(i[0][3],e)}var n}function c(e){a("next",e)}function l(e){a("throw",e)}function h(e,t){e(t),i.shift(),i.length&&a(i[0][0],i[0][1])}},Tn=-1,Dn=new DataView(new ArrayBuffer(0)),xn=new Uint8Array(Dn.buffer),Rn=function(){try{Dn.getInt8(0)}catch(e){return e.constructor}throw new Error("never reached")}(),Pn=new Rn("Insufficient data"),An=new En,Nn=function(){function e(e,t,n,r,o,i,s,a){void 0===e&&(e=fn.defaultCodec),void 0===t&&(t=void 0),void 0===n&&(n=Yt),void 0===r&&(r=Yt),void 0===o&&(o=Yt),void 0===i&&(i=Yt),void 0===s&&(s=Yt),void 0===a&&(a=An),this.extensionCodec=e,this.context=t,this.maxStrLength=n,this.maxBinLength=r,this.maxArrayLength=o,this.maxMapLength=i,this.maxExtLength=s,this.keyDecoder=a,this.totalPos=0,this.pos=0,this.view=Dn,this.bytes=xn,this.headByte=Tn,this.stack=[]}return e.prototype.reinitializeState=function(){this.totalPos=0,this.headByte=Tn,this.stack.length=0},e.prototype.setBuffer=function(e){this.bytes=gn(e),this.view=function(e){if(e instanceof ArrayBuffer)return new DataView(e);var t=gn(e);return new DataView(t.buffer,t.byteOffset,t.byteLength)}(this.bytes),this.pos=0},e.prototype.appendBuffer=function(e){if(this.headByte!==Tn||this.hasRemaining(1)){var t=this.bytes.subarray(this.pos),n=gn(e),r=new Uint8Array(t.length+n.length);r.set(t),r.set(n,t.length),this.setBuffer(r)}else this.setBuffer(e)},e.prototype.hasRemaining=function(e){return this.view.byteLength-this.pos>=e},e.prototype.createExtraByteError=function(e){var t=this.view,n=this.pos;return new RangeError("Extra ".concat(t.byteLength-n," of ").concat(t.byteLength," byte(s) found at buffer[").concat(e,"]"))},e.prototype.decode=function(e){this.reinitializeState(),this.setBuffer(e);var t=this.doDecodeSync();if(this.hasRemaining(1))throw this.createExtraByteError(this.pos);return t},e.prototype.decodeMulti=function(e){return Sn(this,(function(t){switch(t.label){case 0:this.reinitializeState(),this.setBuffer(e),t.label=1;case 1:return this.hasRemaining(1)?[4,this.doDecodeSync()]:[3,3];case 2:return t.sent(),[3,1];case 3:return[2]}}))},e.prototype.decodeAsync=function(e){var t,n,r,o,i,s,a;return i=this,void 0,a=function(){var i,s,a,c,l,h,d,u;return Sn(this,(function(p){switch(p.label){case 0:i=!1,p.label=1;case 1:p.trys.push([1,6,7,12]),t=Cn(e),p.label=2;case 2:return[4,t.next()];case 3:if((n=p.sent()).done)return[3,5];if(a=n.value,i)throw this.createExtraByteError(this.totalPos);this.appendBuffer(a);try{s=this.doDecodeSync(),i=!0}catch(e){if(!(e instanceof Rn))throw e}this.totalPos+=this.pos,p.label=4;case 4:return[3,2];case 5:return[3,12];case 6:return c=p.sent(),r={error:c},[3,12];case 7:return p.trys.push([7,,10,11]),n&&!n.done&&(o=t.return)?[4,o.call(t)]:[3,9];case 8:p.sent(),p.label=9;case 9:return[3,11];case 10:if(r)throw r.error;return[7];case 11:return[7];case 12:if(i){if(this.hasRemaining(1))throw this.createExtraByteError(this.totalPos);return[2,s]}throw h=(l=this).headByte,d=l.pos,u=l.totalPos,new RangeError("Insufficient data in parsing ".concat(wn(h)," at ").concat(u," (").concat(d," in the current buffer)"))}}))},new((s=void 0)||(s=Promise))((function(e,t){function n(e){try{o(a.next(e))}catch(e){t(e)}}function r(e){try{o(a.throw(e))}catch(e){t(e)}}function o(t){var o;t.done?e(t.value):(o=t.value,o instanceof s?o:new s((function(e){e(o)}))).then(n,r)}o((a=a.apply(i,[])).next())}))},e.prototype.decodeArrayStream=function(e){return this.decodeMultiAsync(e,!0)},e.prototype.decodeStream=function(e){return this.decodeMultiAsync(e,!1)},e.prototype.decodeMultiAsync=function(e,t){return kn(this,arguments,(function(){var n,r,o,i,s,a,c,l,h;return Sn(this,(function(d){switch(d.label){case 0:n=t,r=-1,d.label=1;case 1:d.trys.push([1,13,14,19]),o=Cn(e),d.label=2;case 2:return[4,In(o.next())];case 3:if((i=d.sent()).done)return[3,12];if(s=i.value,t&&0===r)throw this.createExtraByteError(this.totalPos);this.appendBuffer(s),n&&(r=this.readArraySize(),n=!1,this.complete()),d.label=4;case 4:d.trys.push([4,9,,10]),d.label=5;case 5:return[4,In(this.doDecodeSync())];case 6:return[4,d.sent()];case 7:return d.sent(),0==--r?[3,8]:[3,5];case 8:return[3,10];case 9:if(!((a=d.sent())instanceof Rn))throw a;return[3,10];case 10:this.totalPos+=this.pos,d.label=11;case 11:return[3,2];case 12:return[3,19];case 13:return c=d.sent(),l={error:c},[3,19];case 14:return d.trys.push([14,,17,18]),i&&!i.done&&(h=o.return)?[4,In(h.call(o))]:[3,16];case 15:d.sent(),d.label=16;case 16:return[3,18];case 17:if(l)throw l.error;return[7];case 18:return[7];case 19:return[2]}}))}))},e.prototype.doDecodeSync=function(){e:for(;;){var e=this.readHeadByte(),t=void 0;if(e>=224)t=e-256;else if(e<192)if(e<128)t=e;else if(e<144){if(0!=(r=e-128)){this.pushMapState(r),this.complete();continue e}t={}}else if(e<160){if(0!=(r=e-144)){this.pushArrayState(r),this.complete();continue e}t=[]}else{var n=e-160;t=this.decodeUtf8String(n,0)}else if(192===e)t=null;else if(194===e)t=!1;else if(195===e)t=!0;else if(202===e)t=this.readF32();else if(203===e)t=this.readF64();else if(204===e)t=this.readU8();else if(205===e)t=this.readU16();else if(206===e)t=this.readU32();else if(207===e)t=this.readU64();else if(208===e)t=this.readI8();else if(209===e)t=this.readI16();else if(210===e)t=this.readI32();else if(211===e)t=this.readI64();else if(217===e)n=this.lookU8(),t=this.decodeUtf8String(n,1);else if(218===e)n=this.lookU16(),t=this.decodeUtf8String(n,2);else if(219===e)n=this.lookU32(),t=this.decodeUtf8String(n,4);else if(220===e){if(0!==(r=this.readU16())){this.pushArrayState(r),this.complete();continue e}t=[]}else if(221===e){if(0!==(r=this.readU32())){this.pushArrayState(r),this.complete();continue e}t=[]}else if(222===e){if(0!==(r=this.readU16())){this.pushMapState(r),this.complete();continue e}t={}}else if(223===e){if(0!==(r=this.readU32())){this.pushMapState(r),this.complete();continue e}t={}}else if(196===e){var r=this.lookU8();t=this.decodeBinary(r,1)}else if(197===e)r=this.lookU16(),t=this.decodeBinary(r,2);else if(198===e)r=this.lookU32(),t=this.decodeBinary(r,4);else if(212===e)t=this.decodeExtension(1,0);else if(213===e)t=this.decodeExtension(2,0);else if(214===e)t=this.decodeExtension(4,0);else if(215===e)t=this.decodeExtension(8,0);else if(216===e)t=this.decodeExtension(16,0);else if(199===e)r=this.lookU8(),t=this.decodeExtension(r,1);else if(200===e)r=this.lookU16(),t=this.decodeExtension(r,2);else{if(201!==e)throw new un("Unrecognized type byte: ".concat(wn(e)));r=this.lookU32(),t=this.decodeExtension(r,4)}this.complete();for(var o=this.stack;o.length>0;){var i=o[o.length-1];if(0===i.type){if(i.array[i.position]=t,i.position++,i.position!==i.size)continue e;o.pop(),t=i.array}else{if(1===i.type){if("string"!=(s=typeof t)&&"number"!==s)throw new un("The type of key must be string or number but "+typeof t);if("__proto__"===t)throw new un("The key __proto__ is not allowed");i.key=t,i.type=2;continue e}if(i.map[i.key]=t,i.readCount++,i.readCount!==i.size){i.key=null,i.type=1;continue e}o.pop(),t=i.map}}return t}var s},e.prototype.readHeadByte=function(){return this.headByte===Tn&&(this.headByte=this.readU8()),this.headByte},e.prototype.complete=function(){this.headByte=Tn},e.prototype.readArraySize=function(){var e=this.readHeadByte();switch(e){case 220:return this.readU16();case 221:return this.readU32();default:if(e<160)return e-144;throw new un("Unrecognized array type byte: ".concat(wn(e)))}},e.prototype.pushMapState=function(e){if(e>this.maxMapLength)throw new un("Max length exceeded: map length (".concat(e,") > maxMapLengthLength (").concat(this.maxMapLength,")"));this.stack.push({type:1,size:e,key:null,readCount:0,map:{}})},e.prototype.pushArrayState=function(e){if(e>this.maxArrayLength)throw new un("Max length exceeded: array length (".concat(e,") > maxArrayLength (").concat(this.maxArrayLength,")"));this.stack.push({type:0,size:e,array:new Array(e),position:0})},e.prototype.decodeUtf8String=function(e,t){var n;if(e>this.maxStrLength)throw new un("Max length exceeded: UTF-8 byte length (".concat(e,") > maxStrLength (").concat(this.maxStrLength,")"));if(this.bytes.byteLengthln?function(e,t,n){var r=e.subarray(t,t+n);return cn.decode(r)}(this.bytes,o,e):sn(this.bytes,o,e),this.pos+=t+e,r},e.prototype.stateIsMapKey=function(){return this.stack.length>0&&1===this.stack[this.stack.length-1].type},e.prototype.decodeBinary=function(e,t){if(e>this.maxBinLength)throw new un("Max length exceeded: bin length (".concat(e,") > maxBinLength (").concat(this.maxBinLength,")"));if(!this.hasRemaining(e+t))throw Pn;var n=this.pos+t,r=this.bytes.subarray(n,n+e);return this.pos+=t+e,r},e.prototype.decodeExtension=function(e,t){if(e>this.maxExtLength)throw new un("Max length exceeded: ext length (".concat(e,") > maxExtLength (").concat(this.maxExtLength,")"));var n=this.view.getInt8(this.pos+t),r=this.decodeBinary(e,t+1);return this.extensionCodec.decode(r,n,this.context)},e.prototype.lookU8=function(){return this.view.getUint8(this.pos)},e.prototype.lookU16=function(){return this.view.getUint16(this.pos)},e.prototype.lookU32=function(){return this.view.getUint32(this.pos)},e.prototype.readU8=function(){var e=this.view.getUint8(this.pos);return this.pos++,e},e.prototype.readI8=function(){var e=this.view.getInt8(this.pos);return this.pos++,e},e.prototype.readU16=function(){var e=this.view.getUint16(this.pos);return this.pos+=2,e},e.prototype.readI16=function(){var e=this.view.getInt16(this.pos);return this.pos+=2,e},e.prototype.readU32=function(){var e=this.view.getUint32(this.pos);return this.pos+=4,e},e.prototype.readI32=function(){var e=this.view.getInt32(this.pos);return this.pos+=4,e},e.prototype.readU64=function(){var e,t,n=(e=this.view,t=this.pos,4294967296*e.getUint32(t)+e.getUint32(t+4));return this.pos+=8,n},e.prototype.readI64=function(){var e=Qt(this.view,this.pos);return this.pos+=8,e},e.prototype.readF32=function(){var e=this.view.getFloat32(this.pos);return this.pos+=4,e},e.prototype.readF64=function(){var e=this.view.getFloat64(this.pos);return this.pos+=8,e},e}();class Un{static write(e){let t=e.byteLength||e.length;const n=[];do{let e=127&t;t>>=7,t>0&&(e|=128),n.push(e)}while(t>0);t=e.byteLength||e.length;const r=new Uint8Array(n.length+t);return r.set(n,0),r.set(e,n.length),r.buffer}static parse(e){const t=[],n=new Uint8Array(e),r=[0,7,14,21,28];for(let o=0;o7)throw new Error("Messages bigger than 2GB are not supported.");if(!(n.byteLength>=o+s+a))throw new Error("Incomplete message.");t.push(n.slice?n.slice(o+s,o+s+a):n.subarray(o+s,o+s+a)),o=o+s+a}return t}}const Mn=new Uint8Array([145,Pt.Ping]);class Ln{constructor(e){this.name="messagepack",this.version=1,this.transferFormat=Rt.Binary,this._errorResult=1,this._voidResult=2,this._nonVoidResult=3,e=e||{},this._encoder=new vn(e.extensionCodec,e.context,e.maxDepth,e.initialBufferSize,e.sortKeys,e.forceFloat32,e.ignoreUndefined,e.forceIntegerToFloat),this._decoder=new Nn(e.extensionCodec,e.context,e.maxStrLength,e.maxBinLength,e.maxArrayLength,e.maxMapLength,e.maxExtLength)}parseMessages(e,t){if(!(n=e)||"undefined"==typeof ArrayBuffer||!(n instanceof ArrayBuffer||n.constructor&&"ArrayBuffer"===n.constructor.name))throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer.");var n;null===t&&(t=dt.instance);const r=Un.parse(e),o=[];for(const e of r){const n=this._parseMessage(e,t);n&&o.push(n)}return o}writeMessage(e){switch(e.type){case Pt.Invocation:return this._writeInvocation(e);case Pt.StreamInvocation:return this._writeStreamInvocation(e);case Pt.StreamItem:return this._writeStreamItem(e);case Pt.Completion:return this._writeCompletion(e);case Pt.Ping:return Un.write(Mn);case Pt.CancelInvocation:return this._writeCancelInvocation(e);default:throw new Error("Invalid message type.")}}_parseMessage(e,t){if(0===e.length)throw new Error("Invalid payload.");const n=this._decoder.decode(e);if(0===n.length||!(n instanceof Array))throw new Error("Invalid payload.");const r=n[0];switch(r){case Pt.Invocation:return this._createInvocationMessage(this._readHeaders(n),n);case Pt.StreamItem:return this._createStreamItemMessage(this._readHeaders(n),n);case Pt.Completion:return this._createCompletionMessage(this._readHeaders(n),n);case Pt.Ping:return this._createPingMessage(n);case Pt.Close:return this._createCloseMessage(n);default:return t.log(ht.Information,"Unknown message type '"+r+"' ignored."),null}}_createCloseMessage(e){if(e.length<2)throw new Error("Invalid payload for Close message.");return{allowReconnect:e.length>=3?e[2]:void 0,error:e[1],type:Pt.Close}}_createPingMessage(e){if(e.length<1)throw new Error("Invalid payload for Ping message.");return{type:Pt.Ping}}_createInvocationMessage(e,t){if(t.length<5)throw new Error("Invalid payload for Invocation message.");const n=t[2];return n?{arguments:t[4],headers:e,invocationId:n,streamIds:[],target:t[3],type:Pt.Invocation}:{arguments:t[4],headers:e,streamIds:[],target:t[3],type:Pt.Invocation}}_createStreamItemMessage(e,t){if(t.length<4)throw new Error("Invalid payload for StreamItem message.");return{headers:e,invocationId:t[2],item:t[3],type:Pt.StreamItem}}_createCompletionMessage(e,t){if(t.length<4)throw new Error("Invalid payload for Completion message.");const n=t[3];if(n!==this._voidResult&&t.length<5)throw new Error("Invalid payload for Completion message.");let r,o;switch(n){case this._errorResult:r=t[4];break;case this._nonVoidResult:o=t[4]}return{error:r,headers:e,invocationId:t[2],result:o,type:Pt.Completion}}_writeInvocation(e){let t;return t=e.streamIds?this._encoder.encode([Pt.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments,e.streamIds]):this._encoder.encode([Pt.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments]),Un.write(t.slice())}_writeStreamInvocation(e){let t;return t=e.streamIds?this._encoder.encode([Pt.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments,e.streamIds]):this._encoder.encode([Pt.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments]),Un.write(t.slice())}_writeStreamItem(e){const t=this._encoder.encode([Pt.StreamItem,e.headers||{},e.invocationId,e.item]);return Un.write(t.slice())}_writeCompletion(e){const t=e.error?this._errorResult:void 0!==e.result?this._nonVoidResult:this._voidResult;let n;switch(t){case this._errorResult:n=this._encoder.encode([Pt.Completion,e.headers||{},e.invocationId,t,e.error]);break;case this._voidResult:n=this._encoder.encode([Pt.Completion,e.headers||{},e.invocationId,t]);break;case this._nonVoidResult:n=this._encoder.encode([Pt.Completion,e.headers||{},e.invocationId,t,e.result])}return Un.write(n.slice())}_writeCancelInvocation(e){const t=this._encoder.encode([Pt.CancelInvocation,e.headers||{},e.invocationId]);return Un.write(t.slice())}_readHeaders(e){const t=e[1];if("object"!=typeof t)throw new Error("Invalid headers.");return t}}let $n=!1;function Bn(){const e=document.querySelector("#blazor-error-ui");e&&(e.style.display="block"),$n||($n=!0,document.querySelectorAll("#blazor-error-ui .reload").forEach((e=>{e.onclick=function(e){location.reload(),e.preventDefault()}})),document.querySelectorAll("#blazor-error-ui .dismiss").forEach((e=>{e.onclick=function(e){const t=document.querySelector("#blazor-error-ui");t&&(t.style.display="none"),e.preventDefault()}})))}const On="function"==typeof TextDecoder?new TextDecoder("utf-8"):null,Fn=On?On.decode.bind(On):function(e){let t=0;const n=e.length,r=[],o=[];for(;t65535&&(o-=65536,r.push(o>>>10&1023|55296),o=56320|1023&o),r.push(o)}r.length>1024&&(o.push(String.fromCharCode.apply(null,r)),r.length=0)}return o.push(String.fromCharCode.apply(null,r)),o.join("")},Hn=Math.pow(2,32),jn=Math.pow(2,21)-1;function Wn(e,t){return e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24}function zn(e,t){return e[t]+(e[t+1]<<8)+(e[t+2]<<16)+(e[t+3]<<24>>>0)}function Jn(e,t){const n=zn(e,t+4);if(n>jn)throw new Error(`Cannot read uint64 with high order part ${n}, because the result would exceed Number.MAX_SAFE_INTEGER.`);return n*Hn+zn(e,t)}class qn{constructor(e){this.batchData=e;const t=new Yn(e);this.arrayRangeReader=new Gn(e),this.arrayBuilderSegmentReader=new Qn(e),this.diffReader=new Vn(e),this.editReader=new Kn(e,t),this.frameReader=new Xn(e,t)}updatedComponents(){return Wn(this.batchData,this.batchData.length-20)}referenceFrames(){return Wn(this.batchData,this.batchData.length-16)}disposedComponentIds(){return Wn(this.batchData,this.batchData.length-12)}disposedEventHandlerIds(){return Wn(this.batchData,this.batchData.length-8)}updatedComponentsEntry(e,t){const n=e+4*t;return Wn(this.batchData,n)}referenceFramesEntry(e,t){return e+20*t}disposedComponentIdsEntry(e,t){const n=e+4*t;return Wn(this.batchData,n)}disposedEventHandlerIdsEntry(e,t){const n=e+8*t;return Jn(this.batchData,n)}}class Vn{constructor(e){this.batchDataUint8=e}componentId(e){return Wn(this.batchDataUint8,e)}edits(e){return e+4}editsEntry(e,t){return e+16*t}}class Kn{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}editType(e){return Wn(this.batchDataUint8,e)}siblingIndex(e){return Wn(this.batchDataUint8,e+4)}newTreeIndex(e){return Wn(this.batchDataUint8,e+8)}moveToSiblingIndex(e){return Wn(this.batchDataUint8,e+8)}removedAttributeName(e){const t=Wn(this.batchDataUint8,e+12);return this.stringReader.readString(t)}}class Xn{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}frameType(e){return Wn(this.batchDataUint8,e)}subtreeLength(e){return Wn(this.batchDataUint8,e+4)}elementReferenceCaptureId(e){const t=Wn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}componentId(e){return Wn(this.batchDataUint8,e+8)}elementName(e){const t=Wn(this.batchDataUint8,e+8);return this.stringReader.readString(t)}textContent(e){const t=Wn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}markupContent(e){const t=Wn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeName(e){const t=Wn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeValue(e){const t=Wn(this.batchDataUint8,e+8);return this.stringReader.readString(t)}attributeEventHandlerId(e){return Jn(this.batchDataUint8,e+12)}}class Yn{constructor(e){this.batchDataUint8=e,this.stringTableStartIndex=Wn(e,e.length-4)}readString(e){if(-1===e)return null;{const n=Wn(this.batchDataUint8,this.stringTableStartIndex+4*e),r=function(e,t){let n=0,r=0;for(let o=0;o<4;o++){const i=e[t+o];if(n|=(127&i)<this.nextBatchId)return this.fatalError?(this.logger.log(Zn.Debug,`Received a new batch ${e} but errored out on a previous batch ${this.nextBatchId-1}`),void await n.send("OnRenderCompleted",this.nextBatchId-1,this.fatalError.toString())):void this.logger.log(Zn.Debug,`Waiting for batch ${this.nextBatchId}. Batch ${e} not processed.`);try{this.nextBatchId++,this.logger.log(Zn.Debug,`Applying batch ${e}.`),function(e,t){const n=pe[e];if(!n)throw new Error(`There is no browser renderer with ID ${e}.`);const r=t.arrayRangeReader,o=t.updatedComponents(),i=r.values(o),s=r.count(o),a=t.referenceFrames(),c=r.values(a),l=t.diffReader;for(let e=0;e=this.minLevel){const n=`[${(new Date).toISOString()}] ${Zn[e]}: ${t}`;switch(e){case Zn.Critical:case Zn.Error:console.error(n);break;case Zn.Warning:console.warn(n);break;case Zn.Information:console.info(n);break;default:console.log(n)}}}}class rr{constructor(e,t){this.circuitId=void 0,this.components=e,this.applicationState=t}reconnect(e){if(!this.circuitId)throw new Error("Circuit host not initialized.");return e.state!==At.Connected?Promise.resolve(!1):e.invoke("ConnectCircuit",this.circuitId)}initialize(e){if(this.circuitId)throw new Error(`Circuit host '${this.circuitId}' already initialized.`);this.circuitId=e}async startCircuit(e){if(e.state!==At.Connected)return!1;const t=await e.invoke("StartCircuit",Ce.getBaseURI(),Ce.getLocationHref(),JSON.stringify(this.components.map((e=>e.toRecord()))),this.applicationState||"");return!!t&&(this.initialize(t),!0)}resolveElement(e){const t=function(e){const t=f.get(e);if(t)return f.delete(e),t}(e);if(t)return B(t,!0);const n=Number.parseInt(e);if(!Number.isNaN(n))return function(e,t){if(!e.parentNode)throw new Error(`Comment not connected to the DOM ${e.textContent}`);const n=e.parentNode,r=B(n,!0),o=J(r);return Array.from(n.childNodes).forEach((e=>o.push(e))),e[L]=r,t&&(e[$]=t,B(t)),B(e)}(this.components[n].start,this.components[n].end);throw new Error(`Invalid sequence number or identifier '${e}'.`)}}const or={configureSignalR:e=>{},logLevel:Zn.Warning,reconnectionOptions:{maxRetries:8,retryIntervalMilliseconds:2e4,dialogId:"components-reconnect-modal"}};class ir{constructor(e,t,n,r){this.maxRetries=t,this.document=n,this.logger=r,this.addedToDom=!1,this.modal=this.document.createElement("div"),this.modal.id=e,this.maxRetries=t,this.modal.style.cssText=["position: fixed","top: 0","right: 0","bottom: 0","left: 0","z-index: 1050","display: none","overflow: hidden","background-color: #fff","opacity: 0.8","text-align: center","font-weight: bold","transition: visibility 0s linear 500ms"].join(";"),this.message=this.document.createElement("h5"),this.message.style.cssText="margin-top: 20px",this.button=this.document.createElement("button"),this.button.style.cssText="margin:5px auto 5px",this.button.textContent="Retry";const o=this.document.createElement("a");o.addEventListener("click",(()=>location.reload())),o.textContent="reload",this.reloadParagraph=this.document.createElement("p"),this.reloadParagraph.textContent="Alternatively, ",this.reloadParagraph.appendChild(o),this.modal.appendChild(this.message),this.modal.appendChild(this.button),this.modal.appendChild(this.reloadParagraph),this.loader=this.getLoader(),this.message.after(this.loader),this.button.addEventListener("click",(async()=>{this.show();try{await Xe.reconnect()||this.rejected()}catch(e){this.logger.log(Zn.Error,e),this.failed()}}))}show(){this.addedToDom||(this.addedToDom=!0,this.document.body.appendChild(this.modal)),this.modal.style.display="block",this.loader.style.display="inline-block",this.button.style.display="none",this.reloadParagraph.style.display="none",this.message.textContent="Attempting to reconnect to the server...",this.modal.style.visibility="hidden",setTimeout((()=>{this.modal.style.visibility="visible"}),0)}update(e){this.message.textContent=`Attempting to reconnect to the server: ${e} of ${this.maxRetries}`}hide(){this.modal.style.display="none"}failed(){this.button.style.display="block",this.reloadParagraph.style.display="none",this.loader.style.display="none";const e=this.document.createTextNode("Reconnection failed. Try "),t=this.document.createElement("a");t.textContent="reloading",t.setAttribute("href",""),t.addEventListener("click",(()=>location.reload()));const n=this.document.createTextNode(" the page if you're unable to reconnect.");this.message.replaceChildren(e,t,n)}rejected(){this.button.style.display="none",this.reloadParagraph.style.display="none",this.loader.style.display="none";const e=this.document.createTextNode("Could not reconnect to the server. "),t=this.document.createElement("a");t.textContent="Reload",t.setAttribute("href",""),t.addEventListener("click",(()=>location.reload()));const n=this.document.createTextNode(" the page to restore functionality.");this.message.replaceChildren(e,t,n)}getLoader(){const e=this.document.createElement("div");return e.style.cssText=["border: 0.3em solid #f3f3f3","border-top: 0.3em solid #3498db","border-radius: 50%","width: 2em","height: 2em","display: inline-block"].join(";"),e.animate([{transform:"rotate(0deg)"},{transform:"rotate(360deg)"}],{duration:2e3,iterations:1/0}),e}}class sr{constructor(e,t,n){this.dialog=e,this.maxRetries=t,this.document=n,this.document=n;const r=this.document.getElementById(sr.MaxRetriesId);r&&(r.innerText=this.maxRetries.toString())}show(){this.removeClasses(),this.dialog.classList.add(sr.ShowClassName)}update(e){const t=this.document.getElementById(sr.CurrentAttemptId);t&&(t.innerText=e.toString())}hide(){this.removeClasses(),this.dialog.classList.add(sr.HideClassName)}failed(){this.removeClasses(),this.dialog.classList.add(sr.FailedClassName)}rejected(){this.removeClasses(),this.dialog.classList.add(sr.RejectedClassName)}removeClasses(){this.dialog.classList.remove(sr.ShowClassName,sr.HideClassName,sr.FailedClassName,sr.RejectedClassName)}}sr.ShowClassName="components-reconnect-show",sr.HideClassName="components-reconnect-hide",sr.FailedClassName="components-reconnect-failed",sr.RejectedClassName="components-reconnect-rejected",sr.MaxRetriesId="components-reconnect-max-retries",sr.CurrentAttemptId="components-reconnect-current-attempt";class ar{constructor(e,t,n){this._currentReconnectionProcess=null,this._logger=e,this._reconnectionDisplay=t,this._reconnectCallback=n||Xe.reconnect}onConnectionDown(e,t){if(!this._reconnectionDisplay){const t=document.getElementById(e.dialogId);this._reconnectionDisplay=t?new sr(t,e.maxRetries,document):new ir(e.dialogId,e.maxRetries,document,this._logger)}this._currentReconnectionProcess||(this._currentReconnectionProcess=new cr(e,this._logger,this._reconnectCallback,this._reconnectionDisplay))}onConnectionUp(){this._currentReconnectionProcess&&(this._currentReconnectionProcess.dispose(),this._currentReconnectionProcess=null)}}class cr{constructor(e,t,n,r){this.logger=t,this.reconnectCallback=n,this.isDisposed=!1,this.reconnectDisplay=r,this.reconnectDisplay.show(),this.attemptPeriodicReconnection(e)}dispose(){this.isDisposed=!0,this.reconnectDisplay.hide()}async attemptPeriodicReconnection(e){for(let t=0;tcr.MaximumFirstRetryInterval?cr.MaximumFirstRetryInterval:e.retryIntervalMilliseconds;if(await this.delay(n),this.isDisposed)break;try{return await this.reconnectCallback()?void 0:void this.reconnectDisplay.rejected()}catch(e){this.logger.log(Zn.Error,e)}}this.reconnectDisplay.failed()}delay(e){return new Promise((t=>setTimeout(t,e)))}}cr.MaximumFirstRetryInterval=3e3;const lr=/^\s*Blazor-Component-State:(?[a-zA-Z0-9+/=]+)$/;function hr(e){var t;if(e.nodeType===Node.COMMENT_NODE){const n=e.textContent||"",r=lr.exec(n),o=r&&r.groups&&r.groups.state;return o&&(null===(t=e.parentNode)||void 0===t||t.removeChild(e)),o}if(!e.hasChildNodes())return;const n=e.childNodes;for(let e=0;e.*)$/);function pr(e,t){const n=e.currentElement;if(n&&n.nodeType===Node.COMMENT_NODE&&n.textContent){const r=ur.exec(n.textContent),o=r&&r.groups&&r.groups.descriptor;if(!o)return;try{const r=function(e){const t=JSON.parse(e),{type:n}=t;if("server"!==n&&"webassembly"!==n)throw new Error(`Invalid component type '${n}'.`);return t}(o);switch(t){case"webassembly":return function(e,t,n){const{type:r,assembly:o,typeName:i,parameterDefinitions:s,parameterValues:a,prerenderId:c}=e,l=c?fr(c,n):void 0;if(c&&!l)throw new Error(`Could not find an end component comment for '${t}'.`);if("webassembly"===r){if(!o)throw new Error("assembly must be defined when using a descriptor.");if(!i)throw new Error("typeName must be defined when using a descriptor.");return{type:r,assembly:o,typeName:i,parameterDefinitions:s&&atob(s),parameterValues:a&&atob(a),start:t,prerenderId:c,end:l}}}(r,n,e);case"server":return function(e,t,n){const{type:r,descriptor:o,sequence:i,prerenderId:s}=e,a=s?fr(s,n):void 0;if(s&&!a)throw new Error(`Could not find an end component comment for '${t}'.`);if("server"===r){if(!o)throw new Error("descriptor must be defined when using a descriptor.");if(void 0===i)throw new Error("sequence must be defined when using a descriptor.");if(!Number.isInteger(i))throw new Error(`Error parsing the sequence '${i}' for component '${JSON.stringify(e)}'`);return{type:r,sequence:i,descriptor:o,start:t,prerenderId:s,end:a}}}(r,n,e)}}catch(e){throw new Error(`Found malformed component comment at ${n.textContent}`)}}}function fr(e,t){for(;t.next()&&t.currentElement;){const n=t.currentElement;if(n.nodeType!==Node.COMMENT_NODE)continue;if(!n.textContent)continue;const r=ur.exec(n.textContent),o=r&&r[1];if(o)return gr(o,e),n}}function gr(e,t){const n=JSON.parse(e);if(1!==Object.keys(n).length)throw new Error(`Invalid end of component comment: '${e}'`);const r=n.prerenderId;if(!r)throw new Error(`End of component comment must have a value for the prerendered property: '${e}'`);if(r!==t)throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${t}', '${r}'`)}class mr{constructor(e){this.childNodes=e,this.currentIndex=-1,this.length=e.length}next(){return this.currentIndex++,this.currentIndexasync function(e,n){const r=function(e){const t=document.baseURI;return t.endsWith("/")?`${t}${e}`:`${t}/${e}`}(n),o=await import(r);if(void 0===o)return;const{beforeStart:i,afterStarted:s}=o;return s&&e.afterStartedCallbacks.push(s),i?i(...t):void 0}(this,e))))}async invokeAfterStartedCallbacks(e){await C,await Promise.all(this.afterStartedCallbacks.map((t=>t(e))))}}let br,_r,Er,Sr=!1;async function Cr(t,n){const r=function(e){const t={...or,...e};return e&&e.reconnectionOptions&&(t.reconnectionOptions={...or.reconnectionOptions,...e.reconnectionOptions}),t}(t),o=await async function(e){const t=await fetch("_blazor/initializers",{method:"GET",credentials:"include",cache:"no-cache"}),n=await t.json(),r=new wr;return await r.importInitializersAsync(n,[e]),r}(r),i=new nr(r.logLevel);Xe.reconnect=async e=>{if(Sr)return!1;const t=e||await Ir(r,i,_r);return await _r.reconnect(t)?(r.reconnectionHandler.onConnectionUp(),!0):(i.log(Zn.Information,"Reconnection attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server."),!1)},Xe.defaultReconnectionHandler=new ar(i),r.reconnectionHandler=r.reconnectionHandler||Xe.defaultReconnectionHandler,i.log(Zn.Information,"Starting up Blazor server-side application.");const s=hr(document);_r=new rr(n||[],s||""),Xe._internal.navigationManager.listenForNavigationEvents(((e,t,n)=>br.send("OnLocationChanged",e,t,n)),((e,t,n,r)=>br.send("OnLocationChanging",e,t,n,r))),Xe._internal.forceCloseConnection=()=>br.stop(),Xe._internal.sendJSDataStream=(e,t,n)=>function(e,t,n,r){setTimeout((async()=>{let o=5,i=(new Date).valueOf();try{const s=t instanceof Blob?t.size:t.byteLength;let a=0,c=0;for(;a1)await e.send("ReceiveJSDataChunk",n,c,h,null);else{if(!await e.invoke("ReceiveJSDataChunk",n,c,h,null))break;const t=(new Date).valueOf(),r=t-i;i=t,o=Math.max(1,Math.round(500/Math.max(1,r)))}a+=l,c++}}catch(t){await e.send("ReceiveJSDataChunk",n,-1,null,t.toString())}}),0)}(br,e,t,n),Er=e.attachDispatcher({beginInvokeDotNetFromJS:(e,t,n,r,o)=>{br.send("BeginInvokeDotNetFromJS",e?e.toString():null,t,n,r||0,o)},endInvokeJSFromDotNet:(e,t,n)=>{br.send("EndInvokeJSFromDotNet",e,t,n)},sendByteArray:(e,t)=>{br.send("ReceiveByteArray",e,t)}});const a=await Ir(r,i,_r);if(!await _r.startCircuit(a))return void i.log(Zn.Error,"Failed to start the circuit.");let c=!1;const l=()=>{if(!c){const e=new FormData,t=_r.circuitId;e.append("circuitId",t),c=navigator.sendBeacon("_blazor/disconnect",e)}};Xe.disconnect=l,window.addEventListener("unload",l,{capture:!1,once:!0}),i.log(Zn.Information,"Blazor server-side application started."),o.invokeAfterStartedCallbacks(Xe)}async function Ir(e,t,n){var r,o;const i=new Ln;i.name="blazorpack";const s=(new qt).withUrl("_blazor").withHubProtocol(i);e.configureSignalR(s);const a=s.build();a.on("JS.AttachComponent",((e,t)=>function(e,t,n,r){let o=pe[0];o||(o=new ae(0),pe[0]=o),o.attachRootComponentToLogicalElement(n,t,!1)}(0,n.resolveElement(t),e))),a.on("JS.BeginInvokeJS",Er.beginInvokeJSFromDotNet.bind(Er)),a.on("JS.EndInvokeDotNet",Er.endInvokeDotNetFromJS.bind(Er)),a.on("JS.ReceiveByteArray",Er.receiveByteArray.bind(Er)),a.on("JS.BeginTransmitStream",(e=>{const t=new ReadableStream({start(t){a.stream("SendDotNetStreamToJS",e).subscribe({next:e=>t.enqueue(e),complete:()=>t.close(),error:e=>t.error(e)})}});Er.supplyDotNetStream(e,t)}));const c=er.getOrCreate(t);a.on("JS.RenderBatch",((e,n)=>{t.log(Zn.Debug,`Received render batch with id ${e} and ${n.byteLength} bytes.`),c.processBatch(e,n,a)})),a.on("JS.EndLocationChanging",Xe._internal.navigationManager.endLocationChanging),a.onclose((t=>!Sr&&e.reconnectionHandler.onConnectionDown(e.reconnectionOptions,t))),a.on("JS.Error",(e=>{Sr=!0,kr(a,e,t),Bn()}));try{await a.start(),br=a}catch(e){if(kr(a,e,t),"FailedToNegotiateWithServerError"===e.errorType)throw e;Bn(),e.innerErrors&&(e.innerErrors.some((e=>"UnsupportedTransportError"===e.errorType&&e.transport===xt.WebSockets))?t.log(Zn.Error,"Unable to connect, please ensure you are using an updated browser that supports WebSockets."):e.innerErrors.some((e=>"FailedToStartTransportError"===e.errorType&&e.transport===xt.WebSockets))?t.log(Zn.Error,"Unable to connect, please ensure WebSockets are available. A VPN or proxy may be blocking the connection."):e.innerErrors.some((e=>"DisabledTransportError"===e.errorType&&e.transport===xt.LongPolling))&&t.log(Zn.Error,"Unable to initiate a SignalR connection to the server. This might be because the server is not configured to support WebSockets. For additional details, visit https://aka.ms/blazor-server-websockets-error."))}return(null===(o=null===(r=a.connection)||void 0===r?void 0:r.features)||void 0===o?void 0:o.inherentKeepAlive)&&t.log(Zn.Warning,"Failed to connect via WebSockets, using the Long Polling fallback transport. This may be due to a VPN or proxy blocking the connection. To troubleshoot this, visit https://aka.ms/blazor-server-using-fallback-long-polling."),a}function kr(e,t,n){n.log(Zn.Error,t),e&&e.stop()}let Tr=!1;function Dr(e){if(Tr)throw new Error("Blazor has already started.");return Tr=!0,Cr(e,function(e,t){return function(e){const t=dr(e,"server"),n=[];for(let e=0;ee.sequence-t.sequence))}(e)}(document))}Xe.start=Dr,window.DotNet=e,document&&document.currentScript&&"false"!==document.currentScript.getAttribute("autostart")&&Dr()})(); \ No newline at end of file +(()=>{"use strict";var e,t,n,r={};r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),function(e){const t=[],n="__jsObjectId",r="__dotNetObject",o="__byte[]",i="__dotNetStream",s="__jsStreamReferenceLength";let a,c;class l{constructor(e){this._jsObject=e,this._cachedFunctions=new Map}findFunction(e){const t=this._cachedFunctions.get(e);if(t)return t;let n,r=this._jsObject;if(e.split(".").forEach((t=>{if(!(t in r))throw new Error(`Could not find '${e}' ('${t}' was undefined).`);n=r,r=r[t]})),r instanceof Function)return r=r.bind(n),this._cachedFunctions.set(e,r),r;throw new Error(`The value '${e}' is not a function.`)}getWrappedObject(){return this._jsObject}}const h={0:new l(window)};h[0]._cachedFunctions.set("import",(e=>("string"==typeof e&&e.startsWith("./")&&(e=new URL(e.substr(2),document.baseURI).toString()),import(e))));let d,u=1;function p(e){t.push(e)}function f(e){if(e&&"object"==typeof e){h[u]=new l(e);const t={[n]:u};return u++,t}throw new Error(`Cannot create a JSObjectReference from the value '${e}'.`)}function g(e){let t=-1;if(e instanceof ArrayBuffer&&(e=new Uint8Array(e)),e instanceof Blob)t=e.size;else{if(!(e.buffer instanceof ArrayBuffer))throw new Error("Supplied value is not a typed array or blob.");if(void 0===e.byteLength)throw new Error(`Cannot create a JSStreamReference from the value '${e}' as it doesn't have a byteLength.`);t=e.byteLength}const r={[s]:t};try{const t=f(e);r[n]=t[n]}catch(t){throw new Error(`Cannot create a JSStreamReference from the value '${e}'.`)}return r}function m(e,n){c=e;const r=n?JSON.parse(n,((e,n)=>t.reduce(((t,n)=>n(e,t)),n))):null;return c=void 0,r}function y(){if(void 0===a)throw new Error("No call dispatcher has been set.");if(null===a)throw new Error("There are multiple .NET runtimes present, so a default dispatcher could not be resolved. Use DotNetObject to invoke .NET instance methods.");return a}e.attachDispatcher=function(e){const t=new v(e);return void 0===a?a=t:a&&(a=null),t},e.attachReviver=p,e.invokeMethod=function(e,t,...n){return y().invokeDotNetStaticMethod(e,t,...n)},e.invokeMethodAsync=function(e,t,...n){return y().invokeDotNetStaticMethodAsync(e,t,...n)},e.createJSObjectReference=f,e.createJSStreamReference=g,e.disposeJSObjectReference=function(e){const t=e&&e[n];"number"==typeof t&&_(t)},function(e){e[e.Default=0]="Default",e[e.JSObjectReference=1]="JSObjectReference",e[e.JSStreamReference=2]="JSStreamReference",e[e.JSVoidResult=3]="JSVoidResult"}(d=e.JSCallResultType||(e.JSCallResultType={}));class v{constructor(e){this._dotNetCallDispatcher=e,this._byteArraysToBeRevived=new Map,this._pendingDotNetToJSStreams=new Map,this._pendingAsyncCalls={},this._nextAsyncCallId=1}getDotNetCallDispatcher(){return this._dotNetCallDispatcher}invokeJSFromDotNet(e,t,n,r){const o=m(this,t),i=I(b(e,r)(...o||[]),n);return null==i?null:T(this,i)}beginInvokeJSFromDotNet(e,t,n,r,o){const i=new Promise((e=>{const r=m(this,n);e(b(t,o)(...r||[]))}));e&&i.then((t=>T(this,[e,!0,I(t,r)]))).then((t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!0,t)),(t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!1,JSON.stringify([e,!1,w(t)]))))}endInvokeDotNetFromJS(e,t,n){const r=t?m(this,n):new Error(n);this.completePendingCall(parseInt(e,10),t,r)}invokeDotNetStaticMethod(e,t,...n){return this.invokeDotNetMethod(e,t,null,n)}invokeDotNetStaticMethodAsync(e,t,...n){return this.invokeDotNetMethodAsync(e,t,null,n)}invokeDotNetMethod(e,t,n,r){if(this._dotNetCallDispatcher.invokeDotNetFromJS){const o=T(this,r),i=this._dotNetCallDispatcher.invokeDotNetFromJS(e,t,n,o);return i?m(this,i):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeDotNetMethodAsync instead.")}invokeDotNetMethodAsync(e,t,n,r){if(e&&n)throw new Error(`For instance method calls, assemblyName should be null. Received '${e}'.`);const o=this._nextAsyncCallId++,i=new Promise(((e,t)=>{this._pendingAsyncCalls[o]={resolve:e,reject:t}}));try{const i=T(this,r);this._dotNetCallDispatcher.beginInvokeDotNetFromJS(o,e,t,n,i)}catch(e){this.completePendingCall(o,!1,e)}return i}receiveByteArray(e,t){this._byteArraysToBeRevived.set(e,t)}processByteArray(e){const t=this._byteArraysToBeRevived.get(e);return t?(this._byteArraysToBeRevived.delete(e),t):null}supplyDotNetStream(e,t){if(this._pendingDotNetToJSStreams.has(e)){const n=this._pendingDotNetToJSStreams.get(e);this._pendingDotNetToJSStreams.delete(e),n.resolve(t)}else{const n=new C;n.resolve(t),this._pendingDotNetToJSStreams.set(e,n)}}getDotNetStreamPromise(e){let t;if(this._pendingDotNetToJSStreams.has(e))t=this._pendingDotNetToJSStreams.get(e).streamPromise,this._pendingDotNetToJSStreams.delete(e);else{const n=new C;this._pendingDotNetToJSStreams.set(e,n),t=n.streamPromise}return t}completePendingCall(e,t,n){if(!this._pendingAsyncCalls.hasOwnProperty(e))throw new Error(`There is no pending async call with ID ${e}.`);const r=this._pendingAsyncCalls[e];delete this._pendingAsyncCalls[e],t?r.resolve(n):r.reject(n)}}function w(e){return e instanceof Error?`${e.message}\n${e.stack}`:e?e.toString():"null"}function b(e,t){const n=h[t];if(n)return n.findFunction(e);throw new Error(`JS object instance with ID ${t} does not exist (has it been disposed?).`)}function _(e){delete h[e]}e.findJSFunction=b,e.disposeJSObjectReferenceById=_;class E{constructor(e,t){this._id=e,this._callDispatcher=t}invokeMethod(e,...t){return this._callDispatcher.invokeDotNetMethod(null,e,this._id,t)}invokeMethodAsync(e,...t){return this._callDispatcher.invokeDotNetMethodAsync(null,e,this._id,t)}dispose(){this._callDispatcher.invokeDotNetMethodAsync(null,"__Dispose",this._id,null).catch((e=>console.error(e)))}serializeAsArg(){return{[r]:this._id}}}e.DotNetObject=E,p((function(e,t){if(t&&"object"==typeof t){if(t.hasOwnProperty(r))return new E(t[r],c);if(t.hasOwnProperty(n)){const e=t[n],r=h[e];if(r)return r.getWrappedObject();throw new Error(`JS object instance with Id '${e}' does not exist. It may have been disposed.`)}if(t.hasOwnProperty(o)){const e=t[o],n=c.processByteArray(e);if(void 0===n)throw new Error(`Byte array index '${e}' does not exist.`);return n}if(t.hasOwnProperty(i)){const e=t[i],n=c.getDotNetStreamPromise(e);return new S(n)}}return t}));class S{constructor(e){this._streamPromise=e}stream(){return this._streamPromise}async arrayBuffer(){return new Response(await this.stream()).arrayBuffer()}}class C{constructor(){this.streamPromise=new Promise(((e,t)=>{this.resolve=e,this.reject=t}))}}function I(e,t){switch(t){case d.Default:return e;case d.JSObjectReference:return f(e);case d.JSStreamReference:return g(e);case d.JSVoidResult:return null;default:throw new Error(`Invalid JS call result type '${t}'.`)}}let k=0;function T(e,t){k=0,c=e;const n=JSON.stringify(t,D);return c=void 0,n}function D(e,t){if(t instanceof E)return t.serializeAsArg();if(t instanceof Uint8Array){c.getDotNetCallDispatcher().sendByteArray(k,t);const e={[o]:k};return k++,e}return t}}(e||(e={})),function(e){e[e.prependFrame=1]="prependFrame",e[e.removeFrame=2]="removeFrame",e[e.setAttribute=3]="setAttribute",e[e.removeAttribute=4]="removeAttribute",e[e.updateText=5]="updateText",e[e.stepIn=6]="stepIn",e[e.stepOut=7]="stepOut",e[e.updateMarkup=8]="updateMarkup",e[e.permutationListEntry=9]="permutationListEntry",e[e.permutationListEnd=10]="permutationListEnd"}(t||(t={})),function(e){e[e.element=1]="element",e[e.text=2]="text",e[e.attribute=3]="attribute",e[e.component=4]="component",e[e.region=5]="region",e[e.elementReferenceCapture=6]="elementReferenceCapture",e[e.markup=8]="markup"}(n||(n={}));class o{constructor(e,t){this.componentId=e,this.fieldValue=t}static fromEvent(e,t){const n=t.target;if(n instanceof Element){const t=function(e){return e instanceof HTMLInputElement?e.type&&"checkbox"===e.type.toLowerCase()?{value:e.checked}:{value:e.value}:e instanceof HTMLSelectElement||e instanceof HTMLTextAreaElement?{value:e.value}:null}(n);if(t)return new o(e,t.value)}return null}}const i=new Map,s=new Map,a=[];function c(e){return i.get(e)}function l(e){const t=i.get(e);return(null==t?void 0:t.browserEventName)||e}function h(e,t){e.forEach((e=>i.set(e,t)))}function d(e){const t=[];for(let n=0;ne.selected)).map((e=>e.value))}}{const e=function(e){return!!e&&"INPUT"===e.tagName&&"checkbox"===e.getAttribute("type")}(t);return{value:e?!!t.checked:t.value}}}}),h(["copy","cut","paste"],{createEventArgs:e=>({type:e.type})}),h(["drag","dragend","dragenter","dragleave","dragover","dragstart","drop"],{createEventArgs:e=>{return{...u(t=e),dataTransfer:t.dataTransfer?{dropEffect:t.dataTransfer.dropEffect,effectAllowed:t.dataTransfer.effectAllowed,files:Array.from(t.dataTransfer.files).map((e=>e.name)),items:Array.from(t.dataTransfer.items).map((e=>({kind:e.kind,type:e.type}))),types:t.dataTransfer.types}:null};var t}}),h(["focus","blur","focusin","focusout"],{createEventArgs:e=>({type:e.type})}),h(["keydown","keyup","keypress"],{createEventArgs:e=>{return{key:(t=e).key,code:t.code,location:t.location,repeat:t.repeat,ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),h(["contextmenu","click","mouseover","mouseout","mousemove","mousedown","mouseup","mouseleave","mouseenter","dblclick"],{createEventArgs:e=>u(e)}),h(["error"],{createEventArgs:e=>{return{message:(t=e).message,filename:t.filename,lineno:t.lineno,colno:t.colno,type:t.type};var t}}),h(["loadstart","timeout","abort","load","loadend","progress"],{createEventArgs:e=>{return{lengthComputable:(t=e).lengthComputable,loaded:t.loaded,total:t.total,type:t.type};var t}}),h(["touchcancel","touchend","touchmove","touchenter","touchleave","touchstart"],{createEventArgs:e=>{return{detail:(t=e).detail,touches:d(t.touches),targetTouches:d(t.targetTouches),changedTouches:d(t.changedTouches),ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),h(["gotpointercapture","lostpointercapture","pointercancel","pointerdown","pointerenter","pointerleave","pointermove","pointerout","pointerover","pointerup"],{createEventArgs:e=>{return{...u(t=e),pointerId:t.pointerId,width:t.width,height:t.height,pressure:t.pressure,tiltX:t.tiltX,tiltY:t.tiltY,pointerType:t.pointerType,isPrimary:t.isPrimary};var t}}),h(["wheel","mousewheel"],{createEventArgs:e=>{return{...u(t=e),deltaX:t.deltaX,deltaY:t.deltaY,deltaZ:t.deltaZ,deltaMode:t.deltaMode};var t}}),h(["toggle"],{createEventArgs:()=>({})});const p=["date","datetime-local","month","time","week"],f=new Map;let g,m,y=0;const v={async add(e,t,n){if(!n)throw new Error("initialParameters must be an object, even if empty.");const r="__bl-dynamic-root:"+(++y).toString();f.set(r,e);const o=await _().invokeMethodAsync("AddRootComponent",t,r),i=new b(o,m[t]);return await i.setParameters(n),i}};class w{invoke(e){return this._callback(e)}setCallback(t){this._selfJSObjectReference||(this._selfJSObjectReference=e.createJSObjectReference(this)),this._callback=t}getJSObjectReference(){return this._selfJSObjectReference}dispose(){this._selfJSObjectReference&&e.disposeJSObjectReference(this._selfJSObjectReference)}}class b{constructor(e,t){this._jsEventCallbackWrappers=new Map,this._componentId=e;for(const e of t)"eventcallback"===e.type&&this._jsEventCallbackWrappers.set(e.name.toLowerCase(),new w)}setParameters(e){const t={},n=Object.entries(e||{}),r=n.length;for(const[e,r]of n){const n=this._jsEventCallbackWrappers.get(e.toLowerCase());n&&r?(n.setCallback(r),t[e]=n.getJSObjectReference()):t[e]=r}return _().invokeMethodAsync("SetRootComponentParameters",this._componentId,r,t)}async dispose(){if(null!==this._componentId){await _().invokeMethodAsync("RemoveRootComponent",this._componentId),this._componentId=null;for(const e of this._jsEventCallbackWrappers.values())e.dispose()}}}function _(){if(!g)throw new Error("Dynamic root components have not been enabled in this application.");return g}const E=[];let S;const C=new Promise((e=>{S=e}));function I(e,t,n){return T(e,t.eventHandlerId,(()=>k(e).invokeMethodAsync("DispatchEventAsync",t,n)))}function k(e){const t=E[e];if(!t)throw new Error(`No interop methods are registered for renderer ${e}`);return t}let T=(e,t,n)=>n();const D=U(["abort","blur","canplay","canplaythrough","change","cuechange","durationchange","emptied","ended","error","focus","load","loadeddata","loadedmetadata","loadend","loadstart","mouseenter","mouseleave","pointerenter","pointerleave","pause","play","playing","progress","ratechange","reset","scroll","seeked","seeking","stalled","submit","suspend","timeupdate","toggle","unload","volumechange","waiting","DOMNodeInsertedIntoDocument","DOMNodeRemovedFromDocument"]),x={submit:!0},R=U(["click","dblclick","mousedown","mousemove","mouseup"]);class P{constructor(e){this.browserRendererId=e,this.afterClickCallbacks=[];const t=++P.nextEventDelegatorId;this.eventsCollectionKey=`_blazorEvents_${t}`,this.eventInfoStore=new A(this.onGlobalEvent.bind(this))}setListener(e,t,n,r){const o=this.getEventHandlerInfosForElement(e,!0),i=o.getHandler(t);if(i)this.eventInfoStore.update(i.eventHandlerId,n);else{const i={element:e,eventName:t,eventHandlerId:n,renderingComponentId:r};this.eventInfoStore.add(i),o.setHandler(t,i)}}getHandler(e){return this.eventInfoStore.get(e)}removeListener(e){const t=this.eventInfoStore.remove(e);if(t){const e=t.element,n=this.getEventHandlerInfosForElement(e,!1);n&&n.removeHandler(t.eventName)}}notifyAfterClick(e){this.afterClickCallbacks.push(e),this.eventInfoStore.addGlobalListener("click")}setStopPropagation(e,t,n){this.getEventHandlerInfosForElement(e,!0).stopPropagation(t,n)}setPreventDefault(e,t,n){this.getEventHandlerInfosForElement(e,!0).preventDefault(t,n)}onGlobalEvent(e){if(!(e.target instanceof Element))return;this.dispatchGlobalEventToAllElements(e.type,e);const t=(n=e.type,s.get(n));var n;t&&t.forEach((t=>this.dispatchGlobalEventToAllElements(t,e))),"click"===e.type&&this.afterClickCallbacks.forEach((t=>t(e)))}dispatchGlobalEventToAllElements(e,t){const n=t.composedPath();let r=n.shift(),i=null,s=!1;const a=Object.prototype.hasOwnProperty.call(D,e);let l=!1;for(;r;){const u=r,p=this.getEventHandlerInfosForElement(u,!1);if(p){const n=p.getHandler(e);if(n&&(h=u,d=t.type,!((h instanceof HTMLButtonElement||h instanceof HTMLInputElement||h instanceof HTMLTextAreaElement||h instanceof HTMLSelectElement)&&Object.prototype.hasOwnProperty.call(R,d)&&h.disabled))){if(!s){const n=c(e);i=(null==n?void 0:n.createEventArgs)?n.createEventArgs(t):{},s=!0}Object.prototype.hasOwnProperty.call(x,t.type)&&t.preventDefault(),I(this.browserRendererId,{eventHandlerId:n.eventHandlerId,eventName:e,eventFieldInfo:o.fromEvent(n.renderingComponentId,t)},i)}p.stopPropagation(e)&&(l=!0),p.preventDefault(e)&&t.preventDefault()}r=a||l?void 0:n.shift()}var h,d}getEventHandlerInfosForElement(e,t){return Object.prototype.hasOwnProperty.call(e,this.eventsCollectionKey)?e[this.eventsCollectionKey]:t?e[this.eventsCollectionKey]=new N:null}}P.nextEventDelegatorId=0;class A{constructor(e){this.globalListener=e,this.infosByEventHandlerId={},this.countByEventName={},a.push(this.handleEventNameAliasAdded.bind(this))}add(e){if(this.infosByEventHandlerId[e.eventHandlerId])throw new Error(`Event ${e.eventHandlerId} is already tracked`);this.infosByEventHandlerId[e.eventHandlerId]=e,this.addGlobalListener(e.eventName)}get(e){return this.infosByEventHandlerId[e]}addGlobalListener(e){if(e=l(e),Object.prototype.hasOwnProperty.call(this.countByEventName,e))this.countByEventName[e]++;else{this.countByEventName[e]=1;const t=Object.prototype.hasOwnProperty.call(D,e);document.addEventListener(e,this.globalListener,t)}}update(e,t){if(Object.prototype.hasOwnProperty.call(this.infosByEventHandlerId,t))throw new Error(`Event ${t} is already tracked`);const n=this.infosByEventHandlerId[e];delete this.infosByEventHandlerId[e],n.eventHandlerId=t,this.infosByEventHandlerId[t]=n}remove(e){const t=this.infosByEventHandlerId[e];if(t){delete this.infosByEventHandlerId[e];const n=l(t.eventName);0==--this.countByEventName[n]&&(delete this.countByEventName[n],document.removeEventListener(n,this.globalListener))}return t}handleEventNameAliasAdded(e,t){if(Object.prototype.hasOwnProperty.call(this.countByEventName,e)){const n=this.countByEventName[e];delete this.countByEventName[e],document.removeEventListener(e,this.globalListener),this.addGlobalListener(t),this.countByEventName[t]+=n-1}}}class N{constructor(){this.handlers={},this.preventDefaultFlags=null,this.stopPropagationFlags=null}getHandler(e){return Object.prototype.hasOwnProperty.call(this.handlers,e)?this.handlers[e]:null}setHandler(e,t){this.handlers[e]=t}removeHandler(e){delete this.handlers[e]}preventDefault(e,t){return void 0!==t&&(this.preventDefaultFlags=this.preventDefaultFlags||{},this.preventDefaultFlags[e]=t),!!this.preventDefaultFlags&&this.preventDefaultFlags[e]}stopPropagation(e,t){return void 0!==t&&(this.stopPropagationFlags=this.stopPropagationFlags||{},this.stopPropagationFlags[e]=t),!!this.stopPropagationFlags&&this.stopPropagationFlags[e]}}function U(e){const t={};return e.forEach((e=>{t[e]=!0})),t}const M=G("_blazorLogicalChildren"),L=G("_blazorLogicalParent"),B=G("_blazorLogicalEnd");function $(e,t){if(e.childNodes.length>0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return M in e||(e[M]=[]),e}function O(e,t){const n=document.createComment("!");return F(n,e,t),n}function F(e,t,n){const r=e;if(e instanceof Comment&&J(r)&&J(r).length>0)throw new Error("Not implemented: inserting non-empty logical container");if(j(r))throw new Error("Not implemented: moving existing logical children");const o=J(t);if(n0;)H(n,0)}const r=n;r.parentNode.removeChild(r)}function j(e){return e[L]||null}function W(e,t){return J(e)[t]}function z(e){const t=V(e);return"http://www.w3.org/2000/svg"===t.namespaceURI&&"foreignObject"!==t.tagName}function J(e){return e[M]}function q(e,t){const n=J(e);t.forEach((e=>{e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=Y(e.moveRangeStart)})),t.forEach((t=>{const r=document.createComment("marker");t.moveToBeforeMarker=r;const o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):X(r,e)})),t.forEach((e=>{const t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd;let i=r;for(;i;){const e=i.nextSibling;if(n.insertBefore(i,t),i===o)break;i=e}n.removeChild(t)})),t.forEach((e=>{n[e.toSiblingIndex]=e.moveRangeStart}))}function V(e){if(e instanceof Element||e instanceof DocumentFragment)return e;if(e instanceof Comment)return e.parentNode;throw new Error("Not a valid logical element")}function K(e){const t=J(j(e));return t[Array.prototype.indexOf.call(t,e)+1]||null}function X(e,t){if(t instanceof Element||t instanceof DocumentFragment)t.appendChild(e);else{if(!(t instanceof Comment))throw new Error(`Cannot append node because the parent is not a valid logical element. Parent: ${t}`);{const n=K(t);n?n.parentNode.insertBefore(e,n):X(e,j(t))}}}function Y(e){if(e instanceof Element||e instanceof DocumentFragment)return e;const t=K(e);if(t)return t.previousSibling;{const t=j(e);return t instanceof Element||t instanceof DocumentFragment?t.lastChild:Y(t)}}function G(e){return"function"==typeof Symbol?Symbol():e}function Q(e){return`_bl_${e}`}const Z="__internalId";e.attachReviver(((e,t)=>t&&"object"==typeof t&&Object.prototype.hasOwnProperty.call(t,Z)&&"string"==typeof t[Z]?function(e){const t=`[${Q(e)}]`;return document.querySelector(t)}(t[Z]):t));const ee="_blazorDeferredValue",te=document.createElement("template"),ne=document.createElementNS("http://www.w3.org/2000/svg","g"),re={},oe="__internal_",ie="preventDefault_",se="stopPropagation_";class ae{constructor(e){this.rootComponentIds=new Set,this.childComponentLocations={},this.eventDelegator=new P(e),this.eventDelegator.notifyAfterClick((e=>{if(!ge)return;if(0!==e.button||function(e){return e.ctrlKey||e.shiftKey||e.altKey||e.metaKey}(e))return;if(e.defaultPrevented)return;const t=function(e){const t=!window._blazorDisableComposedPath&&e.composedPath&&e.composedPath();if(t){for(let e=0;edocument.baseURI,getLocationHref:()=>location.href,scrollToElement:Ie};function Ie(e){const t=document.getElementById(e);return!!t&&(t.scrollIntoView(),!0)}function ke(e,t,n=!1){const r=Me(e);!t.forceLoad&&Be(r)?Te(r,!1,t.replaceHistoryEntry,t.historyEntryState,n):function(e,t){if(location.href===e){const t=e+"?";history.replaceState(null,"",t),location.replace(e)}else t?location.replace(e):location.href=e}(e,t.replaceHistoryEntry)}async function Te(e,t,n,r,o=!1){if(Re(),function(e){const t=e.indexOf("#");return t>-1&&location.href.replace(location.hash,"")===e.substring(0,t)}(e))!function(e,t,n){De(e,t,n);const r=e.indexOf("#");r!==e.length-1&&Ie(e.substring(r+1))}(e,n,r);else{if(!o&&ye&&!await Pe(e,r,t))return;fe=!0,De(e,n,r),await Ae(t)}}function De(e,t,n){t?history.replaceState({userState:n,_index:ve},"",e):(ve++,history.pushState({userState:n,_index:ve},"",e))}function xe(e){return new Promise((t=>{const n=Ee;Ee=()=>{Ee=n,t()},history.go(e)}))}function Re(){Se&&(Se(!1),Se=null)}function Pe(e,t,n){return new Promise((r=>{Re(),_e?(we++,Se=r,_e(we,e,t,n)):r(!1)}))}async function Ae(e){var t;be&&await be(location.href,null===(t=history.state)||void 0===t?void 0:t.userState,e)}async function Ne(e){var t,n;Ee&&await Ee(e),ve=null!==(n=null===(t=history.state)||void 0===t?void 0:t._index)&&void 0!==n?n:0}let Ue;function Me(e){return Ue=Ue||document.createElement("a"),Ue.href=e,Ue.href}function Le(e,t){return e?e.tagName===t?e:Le(e.parentElement,t):null}function Be(e){const t=(n=document.baseURI).substring(0,n.lastIndexOf("/"));var n;const r=e.charAt(t.length);return e.startsWith(t)&&(""===r||"/"===r||"?"===r||"#"===r)}const $e={focus:function(e,t){if(e instanceof HTMLElement)e.focus({preventScroll:t});else{if(!(e instanceof SVGElement))throw new Error("Unable to focus an invalid element.");if(!e.hasAttribute("tabindex"))throw new Error("Unable to focus an SVG element that does not have a tabindex.");e.focus({preventScroll:t})}},focusBySelector:function(e,t){const n=document.querySelector(e);n&&(n.hasAttribute("tabindex")||(n.tabIndex=-1),n.focus({preventScroll:!0}))}},Oe={init:function(e,t,n,r=50){const o=He(t);(o||document.documentElement).style.overflowAnchor="none";const i=document.createRange();h(n.parentElement)&&(t.style.display="table-row",n.style.display="table-row");const s=new IntersectionObserver((function(r){r.forEach((r=>{var o;if(!r.isIntersecting)return;i.setStartAfter(t),i.setEndBefore(n);const s=i.getBoundingClientRect().height,a=null===(o=r.rootBounds)||void 0===o?void 0:o.height;r.target===t?e.invokeMethodAsync("OnSpacerBeforeVisible",r.intersectionRect.top-r.boundingClientRect.top,s,a):r.target===n&&n.offsetHeight>0&&e.invokeMethodAsync("OnSpacerAfterVisible",r.boundingClientRect.bottom-r.intersectionRect.bottom,s,a)}))}),{root:o,rootMargin:`${r}px`});s.observe(t),s.observe(n);const a=l(t),c=l(n);function l(e){const t={attributes:!0},n=new MutationObserver(((n,r)=>{h(e.parentElement)&&(r.disconnect(),e.style.display="table-row",r.observe(e,t)),s.unobserve(e),s.observe(e)}));return n.observe(e,t),n}function h(e){return null!==e&&(e instanceof HTMLTableElement&&""===e.style.display||"table"===e.style.display||e instanceof HTMLTableSectionElement&&""===e.style.display||"table-row-group"===e.style.display)}Fe[e._id]={intersectionObserver:s,mutationObserverBefore:a,mutationObserverAfter:c}},dispose:function(e){const t=Fe[e._id];t&&(t.intersectionObserver.disconnect(),t.mutationObserverBefore.disconnect(),t.mutationObserverAfter.disconnect(),e.dispose(),delete Fe[e._id])}},Fe={};function He(e){return e&&e!==document.body&&e!==document.documentElement?"visible"!==getComputedStyle(e).overflowY?e:He(e.parentElement):null}const je={getAndRemoveExistingTitle:function(){var e;const t=document.head?document.head.getElementsByTagName("title"):[];if(0===t.length)return null;let n=null;for(let r=t.length-1;r>=0;r--){const o=t[r],i=o.previousSibling;i instanceof Comment&&null!==j(i)||(null===n&&(n=o.textContent),null===(e=o.parentNode)||void 0===e||e.removeChild(o))}return n}},We={init:function(e,t){t._blazorInputFileNextFileId=0,t.addEventListener("click",(function(){t.value=""})),t.addEventListener("change",(function(){t._blazorFilesById={};const n=Array.prototype.map.call(t.files,(function(e){const n={id:++t._blazorInputFileNextFileId,lastModified:new Date(e.lastModified).toISOString(),name:e.name,size:e.size,contentType:e.type,readPromise:void 0,arrayBuffer:void 0,blob:e};return t._blazorFilesById[n.id]=n,n}));e.invokeMethodAsync("NotifyChange",n)}))},toImageFile:async function(e,t,n,r,o){const i=ze(e,t),s=await new Promise((function(e){const t=new Image;t.onload=function(){URL.revokeObjectURL(t.src),e(t)},t.onerror=function(){t.onerror=null,URL.revokeObjectURL(t.src)},t.src=URL.createObjectURL(i.blob)})),a=await new Promise((function(e){var t;const i=Math.min(1,r/s.width),a=Math.min(1,o/s.height),c=Math.min(i,a),l=document.createElement("canvas");l.width=Math.round(s.width*c),l.height=Math.round(s.height*c),null===(t=l.getContext("2d"))||void 0===t||t.drawImage(s,0,0,l.width,l.height),l.toBlob(e,n)})),c={id:++e._blazorInputFileNextFileId,lastModified:i.lastModified,name:i.name,size:(null==a?void 0:a.size)||0,contentType:n,blob:a||i.blob};return e._blazorFilesById[c.id]=c,c},readFileData:async function(e,t){return ze(e,t).blob}};function ze(e,t){const n=e._blazorFilesById[t];if(!n)throw new Error(`There is no file with ID ${t}. The file list may have changed. See https://aka.ms/aspnet/blazor-input-file-multiple-selections.`);return n}const Je=new Set,qe={enableNavigationPrompt:function(e){0===Je.size&&window.addEventListener("beforeunload",Ve),Je.add(e)},disableNavigationPrompt:function(e){Je.delete(e),0===Je.size&&window.removeEventListener("beforeunload",Ve)}};function Ve(e){e.preventDefault(),e.returnValue=!0}async function Ke(e,t,n){return e instanceof Blob?await async function(e,t,n){const r=e.slice(t,t+n),o=await r.arrayBuffer();return new Uint8Array(o)}(e,t,n):function(e,t,n){return new Uint8Array(e.buffer,e.byteOffset+t,n)}(e,t,n)}new Map;const Xe={navigateTo:function(e,t,n=!1){ke(e,t instanceof Object?t:{forceLoad:t,replaceHistoryEntry:n})},registerCustomEventType:function(e,t){if(!t)throw new Error("The options parameter is required.");if(i.has(e))throw new Error(`The event '${e}' is already registered.`);if(t.browserEventName){const n=s.get(t.browserEventName);n?n.push(e):s.set(t.browserEventName,[e]),a.forEach((n=>n(e,t.browserEventName)))}i.set(e,t)},rootComponents:v,_internal:{navigationManager:Ce,domWrapper:$e,Virtualize:Oe,PageTitle:je,InputFile:We,NavigationLock:qe,getJSDataStreamChunk:Ke,attachWebRendererInterop:function(t,n,r){const o=E.length;return E.push(t),Object.keys(n).length>0&&function(t,n,r){if(g)throw new Error("Dynamic root components have already been enabled.");g=t,m=n;for(const[t,o]of Object.entries(r)){const r=e.findJSFunction(t,0);for(const e of o)r(e,n[e])}}(k(o),n,r),S(),o}}};window.Blazor=Xe;const Ye=[0,2e3,1e4,3e4,null];class Ge{constructor(e){this._retryDelays=void 0!==e?[...e,null]:Ye}nextRetryDelayInMilliseconds(e){return this._retryDelays[e.previousRetryCount]}}class Qe{}Qe.Authorization="Authorization",Qe.Cookie="Cookie";class Ze{constructor(e,t,n){this.statusCode=e,this.statusText=t,this.content=n}}class et{get(e,t){return this.send({...t,method:"GET",url:e})}post(e,t){return this.send({...t,method:"POST",url:e})}delete(e,t){return this.send({...t,method:"DELETE",url:e})}getCookieString(e){return""}}class tt extends et{constructor(e,t){super(),this._innerClient=e,this._accessTokenFactory=t}async send(e){let t=!0;this._accessTokenFactory&&(!this._accessToken||e.url&&e.url.indexOf("/negotiate?")>0)&&(t=!1,this._accessToken=await this._accessTokenFactory()),this._setAuthorizationHeader(e);const n=await this._innerClient.send(e);return t&&401===n.statusCode&&this._accessTokenFactory?(this._accessToken=await this._accessTokenFactory(),this._setAuthorizationHeader(e),await this._innerClient.send(e)):n}_setAuthorizationHeader(e){e.headers||(e.headers={}),this._accessToken?e.headers[Qe.Authorization]=`Bearer ${this._accessToken}`:this._accessTokenFactory&&e.headers[Qe.Authorization]&&delete e.headers[Qe.Authorization]}getCookieString(e){return this._innerClient.getCookieString(e)}}class nt extends Error{constructor(e,t){const n=new.target.prototype;super(`${e}: Status code '${t}'`),this.statusCode=t,this.__proto__=n}}class rt extends Error{constructor(e="A timeout occurred."){const t=new.target.prototype;super(e),this.__proto__=t}}class ot extends Error{constructor(e="An abort occurred."){const t=new.target.prototype;super(e),this.__proto__=t}}class it extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="UnsupportedTransportError",this.__proto__=n}}class st extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="DisabledTransportError",this.__proto__=n}}class at extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="FailedToStartTransportError",this.__proto__=n}}class ct extends Error{constructor(e){const t=new.target.prototype;super(e),this.errorType="FailedToNegotiateWithServerError",this.__proto__=t}}class lt extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.innerErrors=t,this.__proto__=n}}var ht;!function(e){e[e.Trace=0]="Trace",e[e.Debug=1]="Debug",e[e.Information=2]="Information",e[e.Warning=3]="Warning",e[e.Error=4]="Error",e[e.Critical=5]="Critical",e[e.None=6]="None"}(ht||(ht={}));class dt{constructor(){}log(e,t){}}dt.instance=new dt;const ut="0.0.0-DEV_BUILD";class pt{static isRequired(e,t){if(null==e)throw new Error(`The '${t}' argument is required.`)}static isNotEmpty(e,t){if(!e||e.match(/^\s*$/))throw new Error(`The '${t}' argument should not be empty.`)}static isIn(e,t,n){if(!(e in t))throw new Error(`Unknown ${n} value: ${e}.`)}}class ft{static get isBrowser(){return"object"==typeof window&&"object"==typeof window.document}static get isWebWorker(){return"object"==typeof self&&"importScripts"in self}static get isReactNative(){return"object"==typeof window&&void 0===window.document}static get isNode(){return!this.isBrowser&&!this.isWebWorker&&!this.isReactNative}}function gt(e,t){let n="";return mt(e)?(n=`Binary data of length ${e.byteLength}`,t&&(n+=`. Content: '${function(e){const t=new Uint8Array(e);let n="";return t.forEach((e=>{n+=`0x${e<16?"0":""}${e.toString(16)} `})),n.substr(0,n.length-1)}(e)}'`)):"string"==typeof e&&(n=`String data of length ${e.length}`,t&&(n+=`. Content: '${e}'`)),n}function mt(e){return e&&"undefined"!=typeof ArrayBuffer&&(e instanceof ArrayBuffer||e.constructor&&"ArrayBuffer"===e.constructor.name)}async function yt(e,t,n,r,o,i){const s={},[a,c]=bt();s[a]=c,e.log(ht.Trace,`(${t} transport) sending data. ${gt(o,i.logMessageContent)}.`);const l=mt(o)?"arraybuffer":"text",h=await n.post(r,{content:o,headers:{...s,...i.headers},responseType:l,timeout:i.timeout,withCredentials:i.withCredentials});e.log(ht.Trace,`(${t} transport) request complete. Response status: ${h.statusCode}.`)}class vt{constructor(e,t){this._subject=e,this._observer=t}dispose(){const e=this._subject.observers.indexOf(this._observer);e>-1&&this._subject.observers.splice(e,1),0===this._subject.observers.length&&this._subject.cancelCallback&&this._subject.cancelCallback().catch((e=>{}))}}class wt{constructor(e){this._minLevel=e,this.out=console}log(e,t){if(e>=this._minLevel){const n=`[${(new Date).toISOString()}] ${ht[e]}: ${t}`;switch(e){case ht.Critical:case ht.Error:this.out.error(n);break;case ht.Warning:this.out.warn(n);break;case ht.Information:this.out.info(n);break;default:this.out.log(n)}}}}function bt(){let e="X-SignalR-User-Agent";return ft.isNode&&(e="User-Agent"),[e,_t(ut,Et(),ft.isNode?"NodeJS":"Browser",St())]}function _t(e,t,n,r){let o="Microsoft SignalR/";const i=e.split(".");return o+=`${i[0]}.${i[1]}`,o+=` (${e}; `,o+=t&&""!==t?`${t}; `:"Unknown OS; ",o+=`${n}`,o+=r?`; ${r}`:"; Unknown Runtime Version",o+=")",o}function Et(){if(!ft.isNode)return"";switch(process.platform){case"win32":return"Windows NT";case"darwin":return"macOS";case"linux":return"Linux";default:return process.platform}}function St(){if(ft.isNode)return process.versions.node}function Ct(e){return e.stack?e.stack:e.message?e.message:`${e}`}class It extends et{constructor(e){if(super(),this._logger=e,"undefined"==typeof fetch){const e=require;this._jar=new(e("tough-cookie").CookieJar),"undefined"==typeof fetch?this._fetchType=e("node-fetch"):this._fetchType=fetch,this._fetchType=e("fetch-cookie")(this._fetchType,this._jar)}else this._fetchType=fetch.bind(function(){if("undefined"!=typeof globalThis)return globalThis;if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if(void 0!==r.g)return r.g;throw new Error("could not find global")}());if("undefined"==typeof AbortController){const e=require;this._abortControllerType=e("abort-controller")}else this._abortControllerType=AbortController}async send(e){if(e.abortSignal&&e.abortSignal.aborted)throw new ot;if(!e.method)throw new Error("No method defined.");if(!e.url)throw new Error("No url defined.");const t=new this._abortControllerType;let n;e.abortSignal&&(e.abortSignal.onabort=()=>{t.abort(),n=new ot});let r,o=null;if(e.timeout){const r=e.timeout;o=setTimeout((()=>{t.abort(),this._logger.log(ht.Warning,"Timeout from HTTP request."),n=new rt}),r)}""===e.content&&(e.content=void 0),e.content&&(e.headers=e.headers||{},mt(e.content)?e.headers["Content-Type"]="application/octet-stream":e.headers["Content-Type"]="text/plain;charset=UTF-8");try{r=await this._fetchType(e.url,{body:e.content,cache:"no-cache",credentials:!0===e.withCredentials?"include":"same-origin",headers:{"X-Requested-With":"XMLHttpRequest",...e.headers},method:e.method,mode:"cors",redirect:"follow",signal:t.signal})}catch(e){if(n)throw n;throw this._logger.log(ht.Warning,`Error from HTTP request. ${e}.`),e}finally{o&&clearTimeout(o),e.abortSignal&&(e.abortSignal.onabort=null)}if(!r.ok){const e=await kt(r,"text");throw new nt(e||r.statusText,r.status)}const i=kt(r,e.responseType),s=await i;return new Ze(r.status,r.statusText,s)}getCookieString(e){return""}}function kt(e,t){let n;switch(t){case"arraybuffer":n=e.arrayBuffer();break;case"text":default:n=e.text();break;case"blob":case"document":case"json":throw new Error(`${t} is not supported.`)}return n}class Tt extends et{constructor(e){super(),this._logger=e}send(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new ot):e.method?e.url?new Promise(((t,n)=>{const r=new XMLHttpRequest;r.open(e.method,e.url,!0),r.withCredentials=void 0===e.withCredentials||e.withCredentials,r.setRequestHeader("X-Requested-With","XMLHttpRequest"),""===e.content&&(e.content=void 0),e.content&&(mt(e.content)?r.setRequestHeader("Content-Type","application/octet-stream"):r.setRequestHeader("Content-Type","text/plain;charset=UTF-8"));const o=e.headers;o&&Object.keys(o).forEach((e=>{r.setRequestHeader(e,o[e])})),e.responseType&&(r.responseType=e.responseType),e.abortSignal&&(e.abortSignal.onabort=()=>{r.abort(),n(new ot)}),e.timeout&&(r.timeout=e.timeout),r.onload=()=>{e.abortSignal&&(e.abortSignal.onabort=null),r.status>=200&&r.status<300?t(new Ze(r.status,r.statusText,r.response||r.responseText)):n(new nt(r.response||r.responseText||r.statusText,r.status))},r.onerror=()=>{this._logger.log(ht.Warning,`Error from HTTP request. ${r.status}: ${r.statusText}.`),n(new nt(r.statusText,r.status))},r.ontimeout=()=>{this._logger.log(ht.Warning,"Timeout from HTTP request."),n(new rt)},r.send(e.content)})):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}}class Dt extends et{constructor(e){if(super(),"undefined"!=typeof fetch)this._httpClient=new It(e);else{if("undefined"==typeof XMLHttpRequest)throw new Error("No usable HttpClient found.");this._httpClient=new Tt(e)}}send(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new ot):e.method?e.url?this._httpClient.send(e):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}getCookieString(e){return this._httpClient.getCookieString(e)}}var xt,Rt,Pt,At;!function(e){e[e.None=0]="None",e[e.WebSockets=1]="WebSockets",e[e.ServerSentEvents=2]="ServerSentEvents",e[e.LongPolling=4]="LongPolling"}(xt||(xt={})),function(e){e[e.Text=1]="Text",e[e.Binary=2]="Binary"}(Rt||(Rt={}));class Nt{constructor(){this._isAborted=!1,this.onabort=null}abort(){this._isAborted||(this._isAborted=!0,this.onabort&&this.onabort())}get signal(){return this}get aborted(){return this._isAborted}}class Ut{get pollAborted(){return this._pollAbort.aborted}constructor(e,t,n){this._httpClient=e,this._logger=t,this._pollAbort=new Nt,this._options=n,this._running=!1,this.onreceive=null,this.onclose=null}async connect(e,t){if(pt.isRequired(e,"url"),pt.isRequired(t,"transferFormat"),pt.isIn(t,Rt,"transferFormat"),this._url=e,this._logger.log(ht.Trace,"(LongPolling transport) Connecting."),t===Rt.Binary&&"undefined"!=typeof XMLHttpRequest&&"string"!=typeof(new XMLHttpRequest).responseType)throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");const[n,r]=bt(),o={[n]:r,...this._options.headers},i={abortSignal:this._pollAbort.signal,headers:o,timeout:1e5,withCredentials:this._options.withCredentials};t===Rt.Binary&&(i.responseType="arraybuffer");const s=`${e}&_=${Date.now()}`;this._logger.log(ht.Trace,`(LongPolling transport) polling: ${s}.`);const a=await this._httpClient.get(s,i);200!==a.statusCode?(this._logger.log(ht.Error,`(LongPolling transport) Unexpected response code: ${a.statusCode}.`),this._closeError=new nt(a.statusText||"",a.statusCode),this._running=!1):this._running=!0,this._receiving=this._poll(this._url,i)}async _poll(e,t){try{for(;this._running;)try{const n=`${e}&_=${Date.now()}`;this._logger.log(ht.Trace,`(LongPolling transport) polling: ${n}.`);const r=await this._httpClient.get(n,t);204===r.statusCode?(this._logger.log(ht.Information,"(LongPolling transport) Poll terminated by server."),this._running=!1):200!==r.statusCode?(this._logger.log(ht.Error,`(LongPolling transport) Unexpected response code: ${r.statusCode}.`),this._closeError=new nt(r.statusText||"",r.statusCode),this._running=!1):r.content?(this._logger.log(ht.Trace,`(LongPolling transport) data received. ${gt(r.content,this._options.logMessageContent)}.`),this.onreceive&&this.onreceive(r.content)):this._logger.log(ht.Trace,"(LongPolling transport) Poll timed out, reissuing.")}catch(e){this._running?e instanceof rt?this._logger.log(ht.Trace,"(LongPolling transport) Poll timed out, reissuing."):(this._closeError=e,this._running=!1):this._logger.log(ht.Trace,`(LongPolling transport) Poll errored after shutdown: ${e.message}`)}}finally{this._logger.log(ht.Trace,"(LongPolling transport) Polling complete."),this.pollAborted||this._raiseOnClose()}}async send(e){return this._running?yt(this._logger,"LongPolling",this._httpClient,this._url,e,this._options):Promise.reject(new Error("Cannot send until the transport is connected"))}async stop(){this._logger.log(ht.Trace,"(LongPolling transport) Stopping polling."),this._running=!1,this._pollAbort.abort();try{await this._receiving,this._logger.log(ht.Trace,`(LongPolling transport) sending DELETE request to ${this._url}.`);const e={},[t,n]=bt();e[t]=n;const r={headers:{...e,...this._options.headers},timeout:this._options.timeout,withCredentials:this._options.withCredentials};await this._httpClient.delete(this._url,r),this._logger.log(ht.Trace,"(LongPolling transport) DELETE request sent.")}finally{this._logger.log(ht.Trace,"(LongPolling transport) Stop finished."),this._raiseOnClose()}}_raiseOnClose(){if(this.onclose){let e="(LongPolling transport) Firing onclose event.";this._closeError&&(e+=" Error: "+this._closeError),this._logger.log(ht.Trace,e),this.onclose(this._closeError)}}}class Mt{constructor(e,t,n,r){this._httpClient=e,this._accessToken=t,this._logger=n,this._options=r,this.onreceive=null,this.onclose=null}async connect(e,t){return pt.isRequired(e,"url"),pt.isRequired(t,"transferFormat"),pt.isIn(t,Rt,"transferFormat"),this._logger.log(ht.Trace,"(SSE transport) Connecting."),this._url=e,this._accessToken&&(e+=(e.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(this._accessToken)}`),new Promise(((n,r)=>{let o,i=!1;if(t===Rt.Text){if(ft.isBrowser||ft.isWebWorker)o=new this._options.EventSource(e,{withCredentials:this._options.withCredentials});else{const t=this._httpClient.getCookieString(e),n={};n.Cookie=t;const[r,i]=bt();n[r]=i,o=new this._options.EventSource(e,{withCredentials:this._options.withCredentials,headers:{...n,...this._options.headers}})}try{o.onmessage=e=>{if(this.onreceive)try{this._logger.log(ht.Trace,`(SSE transport) data received. ${gt(e.data,this._options.logMessageContent)}.`),this.onreceive(e.data)}catch(e){return void this._close(e)}},o.onerror=e=>{i?this._close():r(new Error("EventSource failed to connect. The connection could not be found on the server, either the connection ID is not present on the server, or a proxy is refusing/buffering the connection. If you have multiple servers check that sticky sessions are enabled."))},o.onopen=()=>{this._logger.log(ht.Information,`SSE connected to ${this._url}`),this._eventSource=o,i=!0,n()}}catch(e){return void r(e)}}else r(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"))}))}async send(e){return this._eventSource?yt(this._logger,"SSE",this._httpClient,this._url,e,this._options):Promise.reject(new Error("Cannot send until the transport is connected"))}stop(){return this._close(),Promise.resolve()}_close(e){this._eventSource&&(this._eventSource.close(),this._eventSource=void 0,this.onclose&&this.onclose(e))}}class Lt{constructor(e,t,n,r,o,i){this._logger=n,this._accessTokenFactory=t,this._logMessageContent=r,this._webSocketConstructor=o,this._httpClient=e,this.onreceive=null,this.onclose=null,this._headers=i}async connect(e,t){let n;return pt.isRequired(e,"url"),pt.isRequired(t,"transferFormat"),pt.isIn(t,Rt,"transferFormat"),this._logger.log(ht.Trace,"(WebSockets transport) Connecting."),this._accessTokenFactory&&(n=await this._accessTokenFactory()),new Promise(((r,o)=>{let i;e=e.replace(/^http/,"ws");const s=this._httpClient.getCookieString(e);let a=!1;if(ft.isReactNative){const t={},[r,o]=bt();t[r]=o,n&&(t[Qe.Authorization]=`Bearer ${n}`),s&&(t[Qe.Cookie]=s),i=new this._webSocketConstructor(e,void 0,{headers:{...t,...this._headers}})}else n&&(e+=(e.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(n)}`);i||(i=new this._webSocketConstructor(e)),t===Rt.Binary&&(i.binaryType="arraybuffer"),i.onopen=t=>{this._logger.log(ht.Information,`WebSocket connected to ${e}.`),this._webSocket=i,a=!0,r()},i.onerror=e=>{let t=null;t="undefined"!=typeof ErrorEvent&&e instanceof ErrorEvent?e.error:"There was an error with the transport",this._logger.log(ht.Information,`(WebSockets transport) ${t}.`)},i.onmessage=e=>{if(this._logger.log(ht.Trace,`(WebSockets transport) data received. ${gt(e.data,this._logMessageContent)}.`),this.onreceive)try{this.onreceive(e.data)}catch(e){return void this._close(e)}},i.onclose=e=>{if(a)this._close(e);else{let t=null;t="undefined"!=typeof ErrorEvent&&e instanceof ErrorEvent?e.error:"WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.",o(new Error(t))}}}))}send(e){return this._webSocket&&this._webSocket.readyState===this._webSocketConstructor.OPEN?(this._logger.log(ht.Trace,`(WebSockets transport) sending data. ${gt(e,this._logMessageContent)}.`),this._webSocket.send(e),Promise.resolve()):Promise.reject("WebSocket is not in the OPEN state")}stop(){return this._webSocket&&this._close(void 0),Promise.resolve()}_close(e){this._webSocket&&(this._webSocket.onclose=()=>{},this._webSocket.onmessage=()=>{},this._webSocket.onerror=()=>{},this._webSocket.close(),this._webSocket=void 0),this._logger.log(ht.Trace,"(WebSockets transport) socket closed."),this.onclose&&(!this._isCloseEvent(e)||!1!==e.wasClean&&1e3===e.code?e instanceof Error?this.onclose(e):this.onclose():this.onclose(new Error(`WebSocket closed with status code: ${e.code} (${e.reason||"no reason given"}).`)))}_isCloseEvent(e){return e&&"boolean"==typeof e.wasClean&&"number"==typeof e.code}}class Bt{constructor(e,t={}){var n;if(this._stopPromiseResolver=()=>{},this.features={},this._negotiateVersion=1,pt.isRequired(e,"url"),this._logger=void 0===(n=t.logger)?new wt(ht.Information):null===n?dt.instance:void 0!==n.log?n:new wt(n),this.baseUrl=this._resolveUrl(e),(t=t||{}).logMessageContent=void 0!==t.logMessageContent&&t.logMessageContent,"boolean"!=typeof t.withCredentials&&void 0!==t.withCredentials)throw new Error("withCredentials option was not a 'boolean' or 'undefined' value");t.withCredentials=void 0===t.withCredentials||t.withCredentials,t.timeout=void 0===t.timeout?1e5:t.timeout,"undefined"==typeof WebSocket||t.WebSocket||(t.WebSocket=WebSocket),"undefined"==typeof EventSource||t.EventSource||(t.EventSource=EventSource),this._httpClient=new tt(t.httpClient||new Dt(this._logger),t.accessTokenFactory),this._connectionState="Disconnected",this._connectionStarted=!1,this._options=t,this.onreceive=null,this.onclose=null}async start(e){if(e=e||Rt.Binary,pt.isIn(e,Rt,"transferFormat"),this._logger.log(ht.Debug,`Starting connection with transfer format '${Rt[e]}'.`),"Disconnected"!==this._connectionState)return Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state."));if(this._connectionState="Connecting",this._startInternalPromise=this._startInternal(e),await this._startInternalPromise,"Disconnecting"===this._connectionState){const e="Failed to start the HttpConnection before stop() was called.";return this._logger.log(ht.Error,e),await this._stopPromise,Promise.reject(new ot(e))}if("Connected"!==this._connectionState){const e="HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!";return this._logger.log(ht.Error,e),Promise.reject(new ot(e))}this._connectionStarted=!0}send(e){return"Connected"!==this._connectionState?Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State.")):(this._sendQueue||(this._sendQueue=new $t(this.transport)),this._sendQueue.send(e))}async stop(e){return"Disconnected"===this._connectionState?(this._logger.log(ht.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnected state.`),Promise.resolve()):"Disconnecting"===this._connectionState?(this._logger.log(ht.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnecting state.`),this._stopPromise):(this._connectionState="Disconnecting",this._stopPromise=new Promise((e=>{this._stopPromiseResolver=e})),await this._stopInternal(e),void await this._stopPromise)}async _stopInternal(e){this._stopError=e;try{await this._startInternalPromise}catch(e){}if(this.transport){try{await this.transport.stop()}catch(e){this._logger.log(ht.Error,`HttpConnection.transport.stop() threw error '${e}'.`),this._stopConnection()}this.transport=void 0}else this._logger.log(ht.Debug,"HttpConnection.transport is undefined in HttpConnection.stop() because start() failed.")}async _startInternal(e){let t=this.baseUrl;this._accessTokenFactory=this._options.accessTokenFactory,this._httpClient._accessTokenFactory=this._accessTokenFactory;try{if(this._options.skipNegotiation){if(this._options.transport!==xt.WebSockets)throw new Error("Negotiation can only be skipped when using the WebSocket transport directly.");this.transport=this._constructTransport(xt.WebSockets),await this._startTransport(t,e)}else{let n=null,r=0;do{if(n=await this._getNegotiationResponse(t),"Disconnecting"===this._connectionState||"Disconnected"===this._connectionState)throw new ot("The connection was stopped during negotiation.");if(n.error)throw new Error(n.error);if(n.ProtocolVersion)throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.");if(n.url&&(t=n.url),n.accessToken){const e=n.accessToken;this._accessTokenFactory=()=>e,this._httpClient._accessToken=e,this._httpClient._accessTokenFactory=void 0}r++}while(n.url&&r<100);if(100===r&&n.url)throw new Error("Negotiate redirection limit exceeded.");await this._createTransport(t,this._options.transport,n,e)}this.transport instanceof Ut&&(this.features.inherentKeepAlive=!0),"Connecting"===this._connectionState&&(this._logger.log(ht.Debug,"The HttpConnection connected successfully."),this._connectionState="Connected")}catch(e){return this._logger.log(ht.Error,"Failed to start the connection: "+e),this._connectionState="Disconnected",this.transport=void 0,this._stopPromiseResolver(),Promise.reject(e)}}async _getNegotiationResponse(e){const t={},[n,r]=bt();t[n]=r;const o=this._resolveNegotiateUrl(e);this._logger.log(ht.Debug,`Sending negotiation request: ${o}.`);try{const e=await this._httpClient.post(o,{content:"",headers:{...t,...this._options.headers},timeout:this._options.timeout,withCredentials:this._options.withCredentials});if(200!==e.statusCode)return Promise.reject(new Error(`Unexpected status code returned from negotiate '${e.statusCode}'`));const n=JSON.parse(e.content);return(!n.negotiateVersion||n.negotiateVersion<1)&&(n.connectionToken=n.connectionId),n}catch(e){let t="Failed to complete negotiation with the server: "+e;return e instanceof nt&&404===e.statusCode&&(t+=" Either this is not a SignalR endpoint or there is a proxy blocking the connection."),this._logger.log(ht.Error,t),Promise.reject(new ct(t))}}_createConnectUrl(e,t){return t?e+(-1===e.indexOf("?")?"?":"&")+`id=${t}`:e}async _createTransport(e,t,n,r){let o=this._createConnectUrl(e,n.connectionToken);if(this._isITransport(t))return this._logger.log(ht.Debug,"Connection was provided an instance of ITransport, using that directly."),this.transport=t,await this._startTransport(o,r),void(this.connectionId=n.connectionId);const i=[],s=n.availableTransports||[];let a=n;for(const n of s){const s=this._resolveTransportOrError(n,t,r);if(s instanceof Error)i.push(`${n.transport} failed:`),i.push(s);else if(this._isITransport(s)){if(this.transport=s,!a){try{a=await this._getNegotiationResponse(e)}catch(e){return Promise.reject(e)}o=this._createConnectUrl(e,a.connectionToken)}try{return await this._startTransport(o,r),void(this.connectionId=a.connectionId)}catch(e){if(this._logger.log(ht.Error,`Failed to start the transport '${n.transport}': ${e}`),a=void 0,i.push(new at(`${n.transport} failed: ${e}`,xt[n.transport])),"Connecting"!==this._connectionState){const e="Failed to select transport before stop() was called.";return this._logger.log(ht.Debug,e),Promise.reject(new ot(e))}}}}return i.length>0?Promise.reject(new lt(`Unable to connect to the server with any of the available transports. ${i.join(" ")}`,i)):Promise.reject(new Error("None of the transports supported by the client are supported by the server."))}_constructTransport(e){switch(e){case xt.WebSockets:if(!this._options.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new Lt(this._httpClient,this._accessTokenFactory,this._logger,this._options.logMessageContent,this._options.WebSocket,this._options.headers||{});case xt.ServerSentEvents:if(!this._options.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new Mt(this._httpClient,this._httpClient._accessToken,this._logger,this._options);case xt.LongPolling:return new Ut(this._httpClient,this._logger,this._options);default:throw new Error(`Unknown transport: ${e}.`)}}_startTransport(e,t){return this.transport.onreceive=this.onreceive,this.transport.onclose=e=>this._stopConnection(e),this.transport.connect(e,t)}_resolveTransportOrError(e,t,n){const r=xt[e.transport];if(null==r)return this._logger.log(ht.Debug,`Skipping transport '${e.transport}' because it is not supported by this client.`),new Error(`Skipping transport '${e.transport}' because it is not supported by this client.`);if(!function(e,t){return!e||0!=(t&e)}(t,r))return this._logger.log(ht.Debug,`Skipping transport '${xt[r]}' because it was disabled by the client.`),new st(`'${xt[r]}' is disabled by the client.`,r);if(!(e.transferFormats.map((e=>Rt[e])).indexOf(n)>=0))return this._logger.log(ht.Debug,`Skipping transport '${xt[r]}' because it does not support the requested transfer format '${Rt[n]}'.`),new Error(`'${xt[r]}' does not support ${Rt[n]}.`);if(r===xt.WebSockets&&!this._options.WebSocket||r===xt.ServerSentEvents&&!this._options.EventSource)return this._logger.log(ht.Debug,`Skipping transport '${xt[r]}' because it is not supported in your environment.'`),new it(`'${xt[r]}' is not supported in your environment.`,r);this._logger.log(ht.Debug,`Selecting transport '${xt[r]}'.`);try{return this._constructTransport(r)}catch(e){return e}}_isITransport(e){return e&&"object"==typeof e&&"connect"in e}_stopConnection(e){if(this._logger.log(ht.Debug,`HttpConnection.stopConnection(${e}) called while in state ${this._connectionState}.`),this.transport=void 0,e=this._stopError||e,this._stopError=void 0,"Disconnected"!==this._connectionState){if("Connecting"===this._connectionState)throw this._logger.log(ht.Warning,`Call to HttpConnection.stopConnection(${e}) was ignored because the connection is still in the connecting state.`),new Error(`HttpConnection.stopConnection(${e}) was called while the connection is still in the connecting state.`);if("Disconnecting"===this._connectionState&&this._stopPromiseResolver(),e?this._logger.log(ht.Error,`Connection disconnected with error '${e}'.`):this._logger.log(ht.Information,"Connection disconnected."),this._sendQueue&&(this._sendQueue.stop().catch((e=>{this._logger.log(ht.Error,`TransportSendQueue.stop() threw error '${e}'.`)})),this._sendQueue=void 0),this.connectionId=void 0,this._connectionState="Disconnected",this._connectionStarted){this._connectionStarted=!1;try{this.onclose&&this.onclose(e)}catch(t){this._logger.log(ht.Error,`HttpConnection.onclose(${e}) threw error '${t}'.`)}}}else this._logger.log(ht.Debug,`Call to HttpConnection.stopConnection(${e}) was ignored because the connection is already in the disconnected state.`)}_resolveUrl(e){if(0===e.lastIndexOf("https://",0)||0===e.lastIndexOf("http://",0))return e;if(!ft.isBrowser)throw new Error(`Cannot resolve '${e}'.`);const t=window.document.createElement("a");return t.href=e,this._logger.log(ht.Information,`Normalizing '${e}' to '${t.href}'.`),t.href}_resolveNegotiateUrl(e){const t=e.indexOf("?");let n=e.substring(0,-1===t?e.length:t);return"/"!==n[n.length-1]&&(n+="/"),n+="negotiate",n+=-1===t?"":e.substring(t),-1===n.indexOf("negotiateVersion")&&(n+=-1===t?"?":"&",n+="negotiateVersion="+this._negotiateVersion),n}}class $t{constructor(e){this._transport=e,this._buffer=[],this._executing=!0,this._sendBufferedData=new Ot,this._transportResult=new Ot,this._sendLoopPromise=this._sendLoop()}send(e){return this._bufferData(e),this._transportResult||(this._transportResult=new Ot),this._transportResult.promise}stop(){return this._executing=!1,this._sendBufferedData.resolve(),this._sendLoopPromise}_bufferData(e){if(this._buffer.length&&typeof this._buffer[0]!=typeof e)throw new Error(`Expected data to be of type ${typeof this._buffer} but was of type ${typeof e}`);this._buffer.push(e),this._sendBufferedData.resolve()}async _sendLoop(){for(;;){if(await this._sendBufferedData.promise,!this._executing){this._transportResult&&this._transportResult.reject("Connection stopped.");break}this._sendBufferedData=new Ot;const e=this._transportResult;this._transportResult=void 0;const t="string"==typeof this._buffer[0]?this._buffer.join(""):$t._concatBuffers(this._buffer);this._buffer.length=0;try{await this._transport.send(t),e.resolve()}catch(t){e.reject(t)}}}static _concatBuffers(e){const t=e.map((e=>e.byteLength)).reduce(((e,t)=>e+t)),n=new Uint8Array(t);let r=0;for(const t of e)n.set(new Uint8Array(t),r),r+=t.byteLength;return n.buffer}}class Ot{constructor(){this.promise=new Promise(((e,t)=>[this._resolver,this._rejecter]=[e,t]))}resolve(){this._resolver()}reject(e){this._rejecter(e)}}class Ft{static write(e){return`${e}${Ft.RecordSeparator}`}static parse(e){if(e[e.length-1]!==Ft.RecordSeparator)throw new Error("Message is incomplete.");const t=e.split(Ft.RecordSeparator);return t.pop(),t}}Ft.RecordSeparatorCode=30,Ft.RecordSeparator=String.fromCharCode(Ft.RecordSeparatorCode);class Ht{writeHandshakeRequest(e){return Ft.write(JSON.stringify(e))}parseHandshakeResponse(e){let t,n;if(mt(e)){const r=new Uint8Array(e),o=r.indexOf(Ft.RecordSeparatorCode);if(-1===o)throw new Error("Message is incomplete.");const i=o+1;t=String.fromCharCode.apply(null,Array.prototype.slice.call(r.slice(0,i))),n=r.byteLength>i?r.slice(i).buffer:null}else{const r=e,o=r.indexOf(Ft.RecordSeparator);if(-1===o)throw new Error("Message is incomplete.");const i=o+1;t=r.substring(0,i),n=r.length>i?r.substring(i):null}const r=Ft.parse(t),o=JSON.parse(r[0]);if(o.type)throw new Error("Expected a handshake response from the server.");return[n,o]}}!function(e){e[e.Invocation=1]="Invocation",e[e.StreamItem=2]="StreamItem",e[e.Completion=3]="Completion",e[e.StreamInvocation=4]="StreamInvocation",e[e.CancelInvocation=5]="CancelInvocation",e[e.Ping=6]="Ping",e[e.Close=7]="Close"}(Pt||(Pt={}));class jt{constructor(){this.observers=[]}next(e){for(const t of this.observers)t.next(e)}error(e){for(const t of this.observers)t.error&&t.error(e)}complete(){for(const e of this.observers)e.complete&&e.complete()}subscribe(e){return this.observers.push(e),new vt(this,e)}}!function(e){e.Disconnected="Disconnected",e.Connecting="Connecting",e.Connected="Connected",e.Disconnecting="Disconnecting",e.Reconnecting="Reconnecting"}(At||(At={}));class Wt{static create(e,t,n,r,o,i){return new Wt(e,t,n,r,o,i)}constructor(e,t,n,r,o,i){this._nextKeepAlive=0,this._freezeEventListener=()=>{this._logger.log(ht.Warning,"The page is being frozen, this will likely lead to the connection being closed and messages being lost. For more information see the docs at https://learn.microsoft.com/aspnet/core/signalr/javascript-client#bsleep")},pt.isRequired(e,"connection"),pt.isRequired(t,"logger"),pt.isRequired(n,"protocol"),this.serverTimeoutInMilliseconds=null!=o?o:3e4,this.keepAliveIntervalInMilliseconds=null!=i?i:15e3,this._logger=t,this._protocol=n,this.connection=e,this._reconnectPolicy=r,this._handshakeProtocol=new Ht,this.connection.onreceive=e=>this._processIncomingData(e),this.connection.onclose=e=>this._connectionClosed(e),this._callbacks={},this._methods={},this._closedCallbacks=[],this._reconnectingCallbacks=[],this._reconnectedCallbacks=[],this._invocationId=0,this._receivedHandshakeResponse=!1,this._connectionState=At.Disconnected,this._connectionStarted=!1,this._cachedPingMessage=this._protocol.writeMessage({type:Pt.Ping})}get state(){return this._connectionState}get connectionId(){return this.connection&&this.connection.connectionId||null}get baseUrl(){return this.connection.baseUrl||""}set baseUrl(e){if(this._connectionState!==At.Disconnected&&this._connectionState!==At.Reconnecting)throw new Error("The HubConnection must be in the Disconnected or Reconnecting state to change the url.");if(!e)throw new Error("The HubConnection url must be a valid url.");this.connection.baseUrl=e}start(){return this._startPromise=this._startWithStateTransitions(),this._startPromise}async _startWithStateTransitions(){if(this._connectionState!==At.Disconnected)return Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state."));this._connectionState=At.Connecting,this._logger.log(ht.Debug,"Starting HubConnection.");try{await this._startInternal(),ft.isBrowser&&window.document.addEventListener("freeze",this._freezeEventListener),this._connectionState=At.Connected,this._connectionStarted=!0,this._logger.log(ht.Debug,"HubConnection connected successfully.")}catch(e){return this._connectionState=At.Disconnected,this._logger.log(ht.Debug,`HubConnection failed to start successfully because of error '${e}'.`),Promise.reject(e)}}async _startInternal(){this._stopDuringStartError=void 0,this._receivedHandshakeResponse=!1;const e=new Promise(((e,t)=>{this._handshakeResolver=e,this._handshakeRejecter=t}));await this.connection.start(this._protocol.transferFormat);try{const t={protocol:this._protocol.name,version:this._protocol.version};if(this._logger.log(ht.Debug,"Sending handshake request."),await this._sendMessage(this._handshakeProtocol.writeHandshakeRequest(t)),this._logger.log(ht.Information,`Using HubProtocol '${this._protocol.name}'.`),this._cleanupTimeout(),this._resetTimeoutPeriod(),this._resetKeepAliveInterval(),await e,this._stopDuringStartError)throw this._stopDuringStartError;this.connection.features.inherentKeepAlive||await this._sendMessage(this._cachedPingMessage)}catch(e){throw this._logger.log(ht.Debug,`Hub handshake failed with error '${e}' during start(). Stopping HubConnection.`),this._cleanupTimeout(),this._cleanupPingTimer(),await this.connection.stop(e),e}}async stop(){const e=this._startPromise;this._stopPromise=this._stopInternal(),await this._stopPromise;try{await e}catch(e){}}_stopInternal(e){return this._connectionState===At.Disconnected?(this._logger.log(ht.Debug,`Call to HubConnection.stop(${e}) ignored because it is already in the disconnected state.`),Promise.resolve()):this._connectionState===At.Disconnecting?(this._logger.log(ht.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnecting state.`),this._stopPromise):(this._connectionState=At.Disconnecting,this._logger.log(ht.Debug,"Stopping HubConnection."),this._reconnectDelayHandle?(this._logger.log(ht.Debug,"Connection stopped during reconnect delay. Done reconnecting."),clearTimeout(this._reconnectDelayHandle),this._reconnectDelayHandle=void 0,this._completeClose(),Promise.resolve()):(this._cleanupTimeout(),this._cleanupPingTimer(),this._stopDuringStartError=e||new ot("The connection was stopped before the hub handshake could complete."),this.connection.stop(e)))}stream(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._createStreamInvocation(e,t,r);let i;const s=new jt;return s.cancelCallback=()=>{const e=this._createCancelInvocation(o.invocationId);return delete this._callbacks[o.invocationId],i.then((()=>this._sendWithProtocol(e)))},this._callbacks[o.invocationId]=(e,t)=>{t?s.error(t):e&&(e.type===Pt.Completion?e.error?s.error(new Error(e.error)):s.complete():s.next(e.item))},i=this._sendWithProtocol(o).catch((e=>{s.error(e),delete this._callbacks[o.invocationId]})),this._launchStreams(n,i),s}_sendMessage(e){return this._resetKeepAliveInterval(),this.connection.send(e)}_sendWithProtocol(e){return this._sendMessage(this._protocol.writeMessage(e))}send(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._sendWithProtocol(this._createInvocation(e,t,!0,r));return this._launchStreams(n,o),o}invoke(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._createInvocation(e,t,!1,r);return new Promise(((e,t)=>{this._callbacks[o.invocationId]=(n,r)=>{r?t(r):n&&(n.type===Pt.Completion?n.error?t(new Error(n.error)):e(n.result):t(new Error(`Unexpected message type: ${n.type}`)))};const r=this._sendWithProtocol(o).catch((e=>{t(e),delete this._callbacks[o.invocationId]}));this._launchStreams(n,r)}))}on(e,t){e&&t&&(e=e.toLowerCase(),this._methods[e]||(this._methods[e]=[]),-1===this._methods[e].indexOf(t)&&this._methods[e].push(t))}off(e,t){if(!e)return;e=e.toLowerCase();const n=this._methods[e];if(n)if(t){const r=n.indexOf(t);-1!==r&&(n.splice(r,1),0===n.length&&delete this._methods[e])}else delete this._methods[e]}onclose(e){e&&this._closedCallbacks.push(e)}onreconnecting(e){e&&this._reconnectingCallbacks.push(e)}onreconnected(e){e&&this._reconnectedCallbacks.push(e)}_processIncomingData(e){if(this._cleanupTimeout(),this._receivedHandshakeResponse||(e=this._processHandshakeResponse(e),this._receivedHandshakeResponse=!0),e){const t=this._protocol.parseMessages(e,this._logger);for(const e of t)switch(e.type){case Pt.Invocation:this._invokeClientMethod(e);break;case Pt.StreamItem:case Pt.Completion:{const t=this._callbacks[e.invocationId];if(t){e.type===Pt.Completion&&delete this._callbacks[e.invocationId];try{t(e)}catch(e){this._logger.log(ht.Error,`Stream callback threw error: ${Ct(e)}`)}}break}case Pt.Ping:break;case Pt.Close:{this._logger.log(ht.Information,"Close message received from server.");const t=e.error?new Error("Server returned an error on close: "+e.error):void 0;!0===e.allowReconnect?this.connection.stop(t):this._stopPromise=this._stopInternal(t);break}default:this._logger.log(ht.Warning,`Invalid message type: ${e.type}.`)}}this._resetTimeoutPeriod()}_processHandshakeResponse(e){let t,n;try{[n,t]=this._handshakeProtocol.parseHandshakeResponse(e)}catch(e){const t="Error parsing handshake response: "+e;this._logger.log(ht.Error,t);const n=new Error(t);throw this._handshakeRejecter(n),n}if(t.error){const e="Server returned handshake error: "+t.error;this._logger.log(ht.Error,e);const n=new Error(e);throw this._handshakeRejecter(n),n}return this._logger.log(ht.Debug,"Server handshake complete."),this._handshakeResolver(),n}_resetKeepAliveInterval(){this.connection.features.inherentKeepAlive||(this._nextKeepAlive=(new Date).getTime()+this.keepAliveIntervalInMilliseconds,this._cleanupPingTimer())}_resetTimeoutPeriod(){if(!(this.connection.features&&this.connection.features.inherentKeepAlive||(this._timeoutHandle=setTimeout((()=>this.serverTimeout()),this.serverTimeoutInMilliseconds),void 0!==this._pingServerHandle))){let e=this._nextKeepAlive-(new Date).getTime();e<0&&(e=0),this._pingServerHandle=setTimeout((async()=>{if(this._connectionState===At.Connected)try{await this._sendMessage(this._cachedPingMessage)}catch{this._cleanupPingTimer()}}),e)}}serverTimeout(){this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server."))}async _invokeClientMethod(e){const t=e.target.toLowerCase(),n=this._methods[t];if(!n)return this._logger.log(ht.Warning,`No client method with the name '${t}' found.`),void(e.invocationId&&(this._logger.log(ht.Warning,`No result given for '${t}' method and invocation ID '${e.invocationId}'.`),await this._sendWithProtocol(this._createCompletionMessage(e.invocationId,"Client didn't provide a result.",null))));const r=n.slice(),o=!!e.invocationId;let i,s,a;for(const n of r)try{const r=i;i=await n.apply(this,e.arguments),o&&i&&r&&(this._logger.log(ht.Error,`Multiple results provided for '${t}'. Sending error to server.`),a=this._createCompletionMessage(e.invocationId,"Client provided multiple results.",null)),s=void 0}catch(e){s=e,this._logger.log(ht.Error,`A callback for the method '${t}' threw error '${e}'.`)}a?await this._sendWithProtocol(a):o?(s?a=this._createCompletionMessage(e.invocationId,`${s}`,null):void 0!==i?a=this._createCompletionMessage(e.invocationId,null,i):(this._logger.log(ht.Warning,`No result given for '${t}' method and invocation ID '${e.invocationId}'.`),a=this._createCompletionMessage(e.invocationId,"Client didn't provide a result.",null)),await this._sendWithProtocol(a)):i&&this._logger.log(ht.Error,`Result given for '${t}' method but server is not expecting a result.`)}_connectionClosed(e){this._logger.log(ht.Debug,`HubConnection.connectionClosed(${e}) called while in state ${this._connectionState}.`),this._stopDuringStartError=this._stopDuringStartError||e||new ot("The underlying connection was closed before the hub handshake could complete."),this._handshakeResolver&&this._handshakeResolver(),this._cancelCallbacksWithError(e||new Error("Invocation canceled due to the underlying connection being closed.")),this._cleanupTimeout(),this._cleanupPingTimer(),this._connectionState===At.Disconnecting?this._completeClose(e):this._connectionState===At.Connected&&this._reconnectPolicy?this._reconnect(e):this._connectionState===At.Connected&&this._completeClose(e)}_completeClose(e){if(this._connectionStarted){this._connectionState=At.Disconnected,this._connectionStarted=!1,ft.isBrowser&&window.document.removeEventListener("freeze",this._freezeEventListener);try{this._closedCallbacks.forEach((t=>t.apply(this,[e])))}catch(t){this._logger.log(ht.Error,`An onclose callback called with error '${e}' threw error '${t}'.`)}}}async _reconnect(e){const t=Date.now();let n=0,r=void 0!==e?e:new Error("Attempting to reconnect due to a unknown error."),o=this._getNextRetryDelay(n++,0,r);if(null===o)return this._logger.log(ht.Debug,"Connection not reconnecting because the IRetryPolicy returned null on the first reconnect attempt."),void this._completeClose(e);if(this._connectionState=At.Reconnecting,e?this._logger.log(ht.Information,`Connection reconnecting because of error '${e}'.`):this._logger.log(ht.Information,"Connection reconnecting."),0!==this._reconnectingCallbacks.length){try{this._reconnectingCallbacks.forEach((t=>t.apply(this,[e])))}catch(t){this._logger.log(ht.Error,`An onreconnecting callback called with error '${e}' threw error '${t}'.`)}if(this._connectionState!==At.Reconnecting)return void this._logger.log(ht.Debug,"Connection left the reconnecting state in onreconnecting callback. Done reconnecting.")}for(;null!==o;){if(this._logger.log(ht.Information,`Reconnect attempt number ${n} will start in ${o} ms.`),await new Promise((e=>{this._reconnectDelayHandle=setTimeout(e,o)})),this._reconnectDelayHandle=void 0,this._connectionState!==At.Reconnecting)return void this._logger.log(ht.Debug,"Connection left the reconnecting state during reconnect delay. Done reconnecting.");try{if(await this._startInternal(),this._connectionState=At.Connected,this._logger.log(ht.Information,"HubConnection reconnected successfully."),0!==this._reconnectedCallbacks.length)try{this._reconnectedCallbacks.forEach((e=>e.apply(this,[this.connection.connectionId])))}catch(e){this._logger.log(ht.Error,`An onreconnected callback called with connectionId '${this.connection.connectionId}; threw error '${e}'.`)}return}catch(e){if(this._logger.log(ht.Information,`Reconnect attempt failed because of error '${e}'.`),this._connectionState!==At.Reconnecting)return this._logger.log(ht.Debug,`Connection moved to the '${this._connectionState}' from the reconnecting state during reconnect attempt. Done reconnecting.`),void(this._connectionState===At.Disconnecting&&this._completeClose());r=e instanceof Error?e:new Error(e.toString()),o=this._getNextRetryDelay(n++,Date.now()-t,r)}}this._logger.log(ht.Information,`Reconnect retries have been exhausted after ${Date.now()-t} ms and ${n} failed attempts. Connection disconnecting.`),this._completeClose()}_getNextRetryDelay(e,t,n){try{return this._reconnectPolicy.nextRetryDelayInMilliseconds({elapsedMilliseconds:t,previousRetryCount:e,retryReason:n})}catch(n){return this._logger.log(ht.Error,`IRetryPolicy.nextRetryDelayInMilliseconds(${e}, ${t}) threw error '${n}'.`),null}}_cancelCallbacksWithError(e){const t=this._callbacks;this._callbacks={},Object.keys(t).forEach((n=>{const r=t[n];try{r(null,e)}catch(t){this._logger.log(ht.Error,`Stream 'error' callback called with '${e}' threw error: ${Ct(t)}`)}}))}_cleanupPingTimer(){this._pingServerHandle&&(clearTimeout(this._pingServerHandle),this._pingServerHandle=void 0)}_cleanupTimeout(){this._timeoutHandle&&clearTimeout(this._timeoutHandle)}_createInvocation(e,t,n,r){if(n)return 0!==r.length?{arguments:t,streamIds:r,target:e,type:Pt.Invocation}:{arguments:t,target:e,type:Pt.Invocation};{const n=this._invocationId;return this._invocationId++,0!==r.length?{arguments:t,invocationId:n.toString(),streamIds:r,target:e,type:Pt.Invocation}:{arguments:t,invocationId:n.toString(),target:e,type:Pt.Invocation}}}_launchStreams(e,t){if(0!==e.length){t||(t=Promise.resolve());for(const n in e)e[n].subscribe({complete:()=>{t=t.then((()=>this._sendWithProtocol(this._createCompletionMessage(n))))},error:e=>{let r;r=e instanceof Error?e.message:e&&e.toString?e.toString():"Unknown error",t=t.then((()=>this._sendWithProtocol(this._createCompletionMessage(n,r))))},next:e=>{t=t.then((()=>this._sendWithProtocol(this._createStreamItemMessage(n,e))))}})}}_replaceStreamingParams(e){const t=[],n=[];for(let r=0;r=55296&&o<=56319&&r65535&&(h-=65536,i.push(h>>>10&1023|55296),h=56320|1023&h),i.push(h)}else i.push(a);i.length>=on&&(s+=String.fromCharCode.apply(String,i),i.length=0)}return i.length>0&&(s+=String.fromCharCode.apply(String,i)),s}var an,cn=Zt?new TextDecoder:null,ln=Zt?"undefined"!=typeof process&&"force"!==(null===(Xt=null===process||void 0===process?void 0:process.env)||void 0===Xt?void 0:Xt.TEXT_DECODER)?200:0:Yt,hn=function(e,t){this.type=e,this.data=t},dn=(an=function(e,t){return an=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},an(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}an(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),un=function(e){function t(n){var r=e.call(this,n)||this,o=Object.create(t.prototype);return Object.setPrototypeOf(r,o),Object.defineProperty(r,"name",{configurable:!0,enumerable:!1,value:t.name}),r}return dn(t,e),t}(Error),pn={type:-1,encode:function(e){var t,n,r,o;return e instanceof Date?function(e){var t,n=e.sec,r=e.nsec;if(n>=0&&r>=0&&n<=17179869183){if(0===r&&n<=4294967295){var o=new Uint8Array(4);return(t=new DataView(o.buffer)).setUint32(0,n),o}var i=n/4294967296,s=4294967295&n;return o=new Uint8Array(8),(t=new DataView(o.buffer)).setUint32(0,r<<2|3&i),t.setUint32(4,s),o}return o=new Uint8Array(12),(t=new DataView(o.buffer)).setUint32(0,r),Gt(t,4,n),o}((r=1e6*((t=e.getTime())-1e3*(n=Math.floor(t/1e3))),{sec:n+(o=Math.floor(r/1e9)),nsec:r-1e9*o})):null},decode:function(e){var t=function(e){var t=new DataView(e.buffer,e.byteOffset,e.byteLength);switch(e.byteLength){case 4:return{sec:t.getUint32(0),nsec:0};case 8:var n=t.getUint32(0);return{sec:4294967296*(3&n)+t.getUint32(4),nsec:n>>>2};case 12:return{sec:Qt(t,4),nsec:t.getUint32(0)};default:throw new un("Unrecognized data size for timestamp (expected 4, 8, or 12): ".concat(e.length))}}(e);return new Date(1e3*t.sec+t.nsec/1e6)}},fn=function(){function e(){this.builtInEncoders=[],this.builtInDecoders=[],this.encoders=[],this.decoders=[],this.register(pn)}return e.prototype.register=function(e){var t=e.type,n=e.encode,r=e.decode;if(t>=0)this.encoders[t]=n,this.decoders[t]=r;else{var o=1+t;this.builtInEncoders[o]=n,this.builtInDecoders[o]=r}},e.prototype.tryToEncode=function(e,t){for(var n=0;nthis.maxDepth)throw new Error("Too deep objects in depth ".concat(t));null==e?this.encodeNil():"boolean"==typeof e?this.encodeBoolean(e):"number"==typeof e?this.encodeNumber(e):"string"==typeof e?this.encodeString(e):this.encodeObject(e,t)},e.prototype.ensureBufferSizeToWrite=function(e){var t=this.pos+e;this.view.byteLength=0?e<128?this.writeU8(e):e<256?(this.writeU8(204),this.writeU8(e)):e<65536?(this.writeU8(205),this.writeU16(e)):e<4294967296?(this.writeU8(206),this.writeU32(e)):(this.writeU8(207),this.writeU64(e)):e>=-32?this.writeU8(224|e+32):e>=-128?(this.writeU8(208),this.writeI8(e)):e>=-32768?(this.writeU8(209),this.writeI16(e)):e>=-2147483648?(this.writeU8(210),this.writeI32(e)):(this.writeU8(211),this.writeI64(e)):this.forceFloat32?(this.writeU8(202),this.writeF32(e)):(this.writeU8(203),this.writeF64(e))},e.prototype.writeStringHeader=function(e){if(e<32)this.writeU8(160+e);else if(e<256)this.writeU8(217),this.writeU8(e);else if(e<65536)this.writeU8(218),this.writeU16(e);else{if(!(e<4294967296))throw new Error("Too long string: ".concat(e," bytes in UTF-8"));this.writeU8(219),this.writeU32(e)}},e.prototype.encodeString=function(e){if(e.length>nn){var t=en(e);this.ensureBufferSizeToWrite(5+t),this.writeStringHeader(t),rn(e,this.bytes,this.pos),this.pos+=t}else t=en(e),this.ensureBufferSizeToWrite(5+t),this.writeStringHeader(t),function(e,t,n){for(var r=e.length,o=n,i=0;i>6&31|192;else{if(s>=55296&&s<=56319&&i>12&15|224,t[o++]=s>>6&63|128):(t[o++]=s>>18&7|240,t[o++]=s>>12&63|128,t[o++]=s>>6&63|128)}t[o++]=63&s|128}else t[o++]=s}}(e,this.bytes,this.pos),this.pos+=t},e.prototype.encodeObject=function(e,t){var n=this.extensionCodec.tryToEncode(e,this.context);if(null!=n)this.encodeExtension(n);else if(Array.isArray(e))this.encodeArray(e,t);else if(ArrayBuffer.isView(e))this.encodeBinary(e);else{if("object"!=typeof e)throw new Error("Unrecognized object: ".concat(Object.prototype.toString.apply(e)));this.encodeMap(e,t)}},e.prototype.encodeBinary=function(e){var t=e.byteLength;if(t<256)this.writeU8(196),this.writeU8(t);else if(t<65536)this.writeU8(197),this.writeU16(t);else{if(!(t<4294967296))throw new Error("Too large binary: ".concat(t));this.writeU8(198),this.writeU32(t)}var n=gn(e);this.writeU8a(n)},e.prototype.encodeArray=function(e,t){var n=e.length;if(n<16)this.writeU8(144+n);else if(n<65536)this.writeU8(220),this.writeU16(n);else{if(!(n<4294967296))throw new Error("Too large array: ".concat(n));this.writeU8(221),this.writeU32(n)}for(var r=0,o=e;r0&&e<=this.maxKeyLength},e.prototype.find=function(e,t,n){e:for(var r=0,o=this.caches[n-1];r=this.maxLengthPerKey?n[Math.random()*n.length|0]=r:n.push(r)},e.prototype.decode=function(e,t,n){var r=this.find(e,t,n);if(null!=r)return this.hit++,r;this.miss++;var o=sn(e,t,n),i=Uint8Array.prototype.slice.call(e,t,t+n);return this.store(i,o),o},e}(),Sn=function(e,t){var n,r,o,i,s={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function a(i){return function(a){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return s.label++,{value:i[1],done:!1};case 5:s.label++,r=i[1],i=[0];continue;case 7:i=s.ops.pop(),s.trys.pop();continue;default:if(!((o=(o=s.trys).length>0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]1||a(e,t)}))})}function a(e,t){try{(n=o[e](t)).value instanceof In?Promise.resolve(n.value.v).then(c,l):h(i[0][2],n)}catch(e){h(i[0][3],e)}var n}function c(e){a("next",e)}function l(e){a("throw",e)}function h(e,t){e(t),i.shift(),i.length&&a(i[0][0],i[0][1])}},Tn=-1,Dn=new DataView(new ArrayBuffer(0)),xn=new Uint8Array(Dn.buffer),Rn=function(){try{Dn.getInt8(0)}catch(e){return e.constructor}throw new Error("never reached")}(),Pn=new Rn("Insufficient data"),An=new En,Nn=function(){function e(e,t,n,r,o,i,s,a){void 0===e&&(e=fn.defaultCodec),void 0===t&&(t=void 0),void 0===n&&(n=Yt),void 0===r&&(r=Yt),void 0===o&&(o=Yt),void 0===i&&(i=Yt),void 0===s&&(s=Yt),void 0===a&&(a=An),this.extensionCodec=e,this.context=t,this.maxStrLength=n,this.maxBinLength=r,this.maxArrayLength=o,this.maxMapLength=i,this.maxExtLength=s,this.keyDecoder=a,this.totalPos=0,this.pos=0,this.view=Dn,this.bytes=xn,this.headByte=Tn,this.stack=[]}return e.prototype.reinitializeState=function(){this.totalPos=0,this.headByte=Tn,this.stack.length=0},e.prototype.setBuffer=function(e){this.bytes=gn(e),this.view=function(e){if(e instanceof ArrayBuffer)return new DataView(e);var t=gn(e);return new DataView(t.buffer,t.byteOffset,t.byteLength)}(this.bytes),this.pos=0},e.prototype.appendBuffer=function(e){if(this.headByte!==Tn||this.hasRemaining(1)){var t=this.bytes.subarray(this.pos),n=gn(e),r=new Uint8Array(t.length+n.length);r.set(t),r.set(n,t.length),this.setBuffer(r)}else this.setBuffer(e)},e.prototype.hasRemaining=function(e){return this.view.byteLength-this.pos>=e},e.prototype.createExtraByteError=function(e){var t=this.view,n=this.pos;return new RangeError("Extra ".concat(t.byteLength-n," of ").concat(t.byteLength," byte(s) found at buffer[").concat(e,"]"))},e.prototype.decode=function(e){this.reinitializeState(),this.setBuffer(e);var t=this.doDecodeSync();if(this.hasRemaining(1))throw this.createExtraByteError(this.pos);return t},e.prototype.decodeMulti=function(e){return Sn(this,(function(t){switch(t.label){case 0:this.reinitializeState(),this.setBuffer(e),t.label=1;case 1:return this.hasRemaining(1)?[4,this.doDecodeSync()]:[3,3];case 2:return t.sent(),[3,1];case 3:return[2]}}))},e.prototype.decodeAsync=function(e){var t,n,r,o,i,s,a;return i=this,void 0,a=function(){var i,s,a,c,l,h,d,u;return Sn(this,(function(p){switch(p.label){case 0:i=!1,p.label=1;case 1:p.trys.push([1,6,7,12]),t=Cn(e),p.label=2;case 2:return[4,t.next()];case 3:if((n=p.sent()).done)return[3,5];if(a=n.value,i)throw this.createExtraByteError(this.totalPos);this.appendBuffer(a);try{s=this.doDecodeSync(),i=!0}catch(e){if(!(e instanceof Rn))throw e}this.totalPos+=this.pos,p.label=4;case 4:return[3,2];case 5:return[3,12];case 6:return c=p.sent(),r={error:c},[3,12];case 7:return p.trys.push([7,,10,11]),n&&!n.done&&(o=t.return)?[4,o.call(t)]:[3,9];case 8:p.sent(),p.label=9;case 9:return[3,11];case 10:if(r)throw r.error;return[7];case 11:return[7];case 12:if(i){if(this.hasRemaining(1))throw this.createExtraByteError(this.totalPos);return[2,s]}throw h=(l=this).headByte,d=l.pos,u=l.totalPos,new RangeError("Insufficient data in parsing ".concat(wn(h)," at ").concat(u," (").concat(d," in the current buffer)"))}}))},new((s=void 0)||(s=Promise))((function(e,t){function n(e){try{o(a.next(e))}catch(e){t(e)}}function r(e){try{o(a.throw(e))}catch(e){t(e)}}function o(t){var o;t.done?e(t.value):(o=t.value,o instanceof s?o:new s((function(e){e(o)}))).then(n,r)}o((a=a.apply(i,[])).next())}))},e.prototype.decodeArrayStream=function(e){return this.decodeMultiAsync(e,!0)},e.prototype.decodeStream=function(e){return this.decodeMultiAsync(e,!1)},e.prototype.decodeMultiAsync=function(e,t){return kn(this,arguments,(function(){var n,r,o,i,s,a,c,l,h;return Sn(this,(function(d){switch(d.label){case 0:n=t,r=-1,d.label=1;case 1:d.trys.push([1,13,14,19]),o=Cn(e),d.label=2;case 2:return[4,In(o.next())];case 3:if((i=d.sent()).done)return[3,12];if(s=i.value,t&&0===r)throw this.createExtraByteError(this.totalPos);this.appendBuffer(s),n&&(r=this.readArraySize(),n=!1,this.complete()),d.label=4;case 4:d.trys.push([4,9,,10]),d.label=5;case 5:return[4,In(this.doDecodeSync())];case 6:return[4,d.sent()];case 7:return d.sent(),0==--r?[3,8]:[3,5];case 8:return[3,10];case 9:if(!((a=d.sent())instanceof Rn))throw a;return[3,10];case 10:this.totalPos+=this.pos,d.label=11;case 11:return[3,2];case 12:return[3,19];case 13:return c=d.sent(),l={error:c},[3,19];case 14:return d.trys.push([14,,17,18]),i&&!i.done&&(h=o.return)?[4,In(h.call(o))]:[3,16];case 15:d.sent(),d.label=16;case 16:return[3,18];case 17:if(l)throw l.error;return[7];case 18:return[7];case 19:return[2]}}))}))},e.prototype.doDecodeSync=function(){e:for(;;){var e=this.readHeadByte(),t=void 0;if(e>=224)t=e-256;else if(e<192)if(e<128)t=e;else if(e<144){if(0!=(r=e-128)){this.pushMapState(r),this.complete();continue e}t={}}else if(e<160){if(0!=(r=e-144)){this.pushArrayState(r),this.complete();continue e}t=[]}else{var n=e-160;t=this.decodeUtf8String(n,0)}else if(192===e)t=null;else if(194===e)t=!1;else if(195===e)t=!0;else if(202===e)t=this.readF32();else if(203===e)t=this.readF64();else if(204===e)t=this.readU8();else if(205===e)t=this.readU16();else if(206===e)t=this.readU32();else if(207===e)t=this.readU64();else if(208===e)t=this.readI8();else if(209===e)t=this.readI16();else if(210===e)t=this.readI32();else if(211===e)t=this.readI64();else if(217===e)n=this.lookU8(),t=this.decodeUtf8String(n,1);else if(218===e)n=this.lookU16(),t=this.decodeUtf8String(n,2);else if(219===e)n=this.lookU32(),t=this.decodeUtf8String(n,4);else if(220===e){if(0!==(r=this.readU16())){this.pushArrayState(r),this.complete();continue e}t=[]}else if(221===e){if(0!==(r=this.readU32())){this.pushArrayState(r),this.complete();continue e}t=[]}else if(222===e){if(0!==(r=this.readU16())){this.pushMapState(r),this.complete();continue e}t={}}else if(223===e){if(0!==(r=this.readU32())){this.pushMapState(r),this.complete();continue e}t={}}else if(196===e){var r=this.lookU8();t=this.decodeBinary(r,1)}else if(197===e)r=this.lookU16(),t=this.decodeBinary(r,2);else if(198===e)r=this.lookU32(),t=this.decodeBinary(r,4);else if(212===e)t=this.decodeExtension(1,0);else if(213===e)t=this.decodeExtension(2,0);else if(214===e)t=this.decodeExtension(4,0);else if(215===e)t=this.decodeExtension(8,0);else if(216===e)t=this.decodeExtension(16,0);else if(199===e)r=this.lookU8(),t=this.decodeExtension(r,1);else if(200===e)r=this.lookU16(),t=this.decodeExtension(r,2);else{if(201!==e)throw new un("Unrecognized type byte: ".concat(wn(e)));r=this.lookU32(),t=this.decodeExtension(r,4)}this.complete();for(var o=this.stack;o.length>0;){var i=o[o.length-1];if(0===i.type){if(i.array[i.position]=t,i.position++,i.position!==i.size)continue e;o.pop(),t=i.array}else{if(1===i.type){if("string"!=(s=typeof t)&&"number"!==s)throw new un("The type of key must be string or number but "+typeof t);if("__proto__"===t)throw new un("The key __proto__ is not allowed");i.key=t,i.type=2;continue e}if(i.map[i.key]=t,i.readCount++,i.readCount!==i.size){i.key=null,i.type=1;continue e}o.pop(),t=i.map}}return t}var s},e.prototype.readHeadByte=function(){return this.headByte===Tn&&(this.headByte=this.readU8()),this.headByte},e.prototype.complete=function(){this.headByte=Tn},e.prototype.readArraySize=function(){var e=this.readHeadByte();switch(e){case 220:return this.readU16();case 221:return this.readU32();default:if(e<160)return e-144;throw new un("Unrecognized array type byte: ".concat(wn(e)))}},e.prototype.pushMapState=function(e){if(e>this.maxMapLength)throw new un("Max length exceeded: map length (".concat(e,") > maxMapLengthLength (").concat(this.maxMapLength,")"));this.stack.push({type:1,size:e,key:null,readCount:0,map:{}})},e.prototype.pushArrayState=function(e){if(e>this.maxArrayLength)throw new un("Max length exceeded: array length (".concat(e,") > maxArrayLength (").concat(this.maxArrayLength,")"));this.stack.push({type:0,size:e,array:new Array(e),position:0})},e.prototype.decodeUtf8String=function(e,t){var n;if(e>this.maxStrLength)throw new un("Max length exceeded: UTF-8 byte length (".concat(e,") > maxStrLength (").concat(this.maxStrLength,")"));if(this.bytes.byteLengthln?function(e,t,n){var r=e.subarray(t,t+n);return cn.decode(r)}(this.bytes,o,e):sn(this.bytes,o,e),this.pos+=t+e,r},e.prototype.stateIsMapKey=function(){return this.stack.length>0&&1===this.stack[this.stack.length-1].type},e.prototype.decodeBinary=function(e,t){if(e>this.maxBinLength)throw new un("Max length exceeded: bin length (".concat(e,") > maxBinLength (").concat(this.maxBinLength,")"));if(!this.hasRemaining(e+t))throw Pn;var n=this.pos+t,r=this.bytes.subarray(n,n+e);return this.pos+=t+e,r},e.prototype.decodeExtension=function(e,t){if(e>this.maxExtLength)throw new un("Max length exceeded: ext length (".concat(e,") > maxExtLength (").concat(this.maxExtLength,")"));var n=this.view.getInt8(this.pos+t),r=this.decodeBinary(e,t+1);return this.extensionCodec.decode(r,n,this.context)},e.prototype.lookU8=function(){return this.view.getUint8(this.pos)},e.prototype.lookU16=function(){return this.view.getUint16(this.pos)},e.prototype.lookU32=function(){return this.view.getUint32(this.pos)},e.prototype.readU8=function(){var e=this.view.getUint8(this.pos);return this.pos++,e},e.prototype.readI8=function(){var e=this.view.getInt8(this.pos);return this.pos++,e},e.prototype.readU16=function(){var e=this.view.getUint16(this.pos);return this.pos+=2,e},e.prototype.readI16=function(){var e=this.view.getInt16(this.pos);return this.pos+=2,e},e.prototype.readU32=function(){var e=this.view.getUint32(this.pos);return this.pos+=4,e},e.prototype.readI32=function(){var e=this.view.getInt32(this.pos);return this.pos+=4,e},e.prototype.readU64=function(){var e,t,n=(e=this.view,t=this.pos,4294967296*e.getUint32(t)+e.getUint32(t+4));return this.pos+=8,n},e.prototype.readI64=function(){var e=Qt(this.view,this.pos);return this.pos+=8,e},e.prototype.readF32=function(){var e=this.view.getFloat32(this.pos);return this.pos+=4,e},e.prototype.readF64=function(){var e=this.view.getFloat64(this.pos);return this.pos+=8,e},e}();class Un{static write(e){let t=e.byteLength||e.length;const n=[];do{let e=127&t;t>>=7,t>0&&(e|=128),n.push(e)}while(t>0);t=e.byteLength||e.length;const r=new Uint8Array(n.length+t);return r.set(n,0),r.set(e,n.length),r.buffer}static parse(e){const t=[],n=new Uint8Array(e),r=[0,7,14,21,28];for(let o=0;o7)throw new Error("Messages bigger than 2GB are not supported.");if(!(n.byteLength>=o+s+a))throw new Error("Incomplete message.");t.push(n.slice?n.slice(o+s,o+s+a):n.subarray(o+s,o+s+a)),o=o+s+a}return t}}const Mn=new Uint8Array([145,Pt.Ping]);class Ln{constructor(e){this.name="messagepack",this.version=1,this.transferFormat=Rt.Binary,this._errorResult=1,this._voidResult=2,this._nonVoidResult=3,e=e||{},this._encoder=new vn(e.extensionCodec,e.context,e.maxDepth,e.initialBufferSize,e.sortKeys,e.forceFloat32,e.ignoreUndefined,e.forceIntegerToFloat),this._decoder=new Nn(e.extensionCodec,e.context,e.maxStrLength,e.maxBinLength,e.maxArrayLength,e.maxMapLength,e.maxExtLength)}parseMessages(e,t){if(!(n=e)||"undefined"==typeof ArrayBuffer||!(n instanceof ArrayBuffer||n.constructor&&"ArrayBuffer"===n.constructor.name))throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer.");var n;null===t&&(t=dt.instance);const r=Un.parse(e),o=[];for(const e of r){const n=this._parseMessage(e,t);n&&o.push(n)}return o}writeMessage(e){switch(e.type){case Pt.Invocation:return this._writeInvocation(e);case Pt.StreamInvocation:return this._writeStreamInvocation(e);case Pt.StreamItem:return this._writeStreamItem(e);case Pt.Completion:return this._writeCompletion(e);case Pt.Ping:return Un.write(Mn);case Pt.CancelInvocation:return this._writeCancelInvocation(e);default:throw new Error("Invalid message type.")}}_parseMessage(e,t){if(0===e.length)throw new Error("Invalid payload.");const n=this._decoder.decode(e);if(0===n.length||!(n instanceof Array))throw new Error("Invalid payload.");const r=n[0];switch(r){case Pt.Invocation:return this._createInvocationMessage(this._readHeaders(n),n);case Pt.StreamItem:return this._createStreamItemMessage(this._readHeaders(n),n);case Pt.Completion:return this._createCompletionMessage(this._readHeaders(n),n);case Pt.Ping:return this._createPingMessage(n);case Pt.Close:return this._createCloseMessage(n);default:return t.log(ht.Information,"Unknown message type '"+r+"' ignored."),null}}_createCloseMessage(e){if(e.length<2)throw new Error("Invalid payload for Close message.");return{allowReconnect:e.length>=3?e[2]:void 0,error:e[1],type:Pt.Close}}_createPingMessage(e){if(e.length<1)throw new Error("Invalid payload for Ping message.");return{type:Pt.Ping}}_createInvocationMessage(e,t){if(t.length<5)throw new Error("Invalid payload for Invocation message.");const n=t[2];return n?{arguments:t[4],headers:e,invocationId:n,streamIds:[],target:t[3],type:Pt.Invocation}:{arguments:t[4],headers:e,streamIds:[],target:t[3],type:Pt.Invocation}}_createStreamItemMessage(e,t){if(t.length<4)throw new Error("Invalid payload for StreamItem message.");return{headers:e,invocationId:t[2],item:t[3],type:Pt.StreamItem}}_createCompletionMessage(e,t){if(t.length<4)throw new Error("Invalid payload for Completion message.");const n=t[3];if(n!==this._voidResult&&t.length<5)throw new Error("Invalid payload for Completion message.");let r,o;switch(n){case this._errorResult:r=t[4];break;case this._nonVoidResult:o=t[4]}return{error:r,headers:e,invocationId:t[2],result:o,type:Pt.Completion}}_writeInvocation(e){let t;return t=e.streamIds?this._encoder.encode([Pt.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments,e.streamIds]):this._encoder.encode([Pt.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments]),Un.write(t.slice())}_writeStreamInvocation(e){let t;return t=e.streamIds?this._encoder.encode([Pt.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments,e.streamIds]):this._encoder.encode([Pt.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments]),Un.write(t.slice())}_writeStreamItem(e){const t=this._encoder.encode([Pt.StreamItem,e.headers||{},e.invocationId,e.item]);return Un.write(t.slice())}_writeCompletion(e){const t=e.error?this._errorResult:void 0!==e.result?this._nonVoidResult:this._voidResult;let n;switch(t){case this._errorResult:n=this._encoder.encode([Pt.Completion,e.headers||{},e.invocationId,t,e.error]);break;case this._voidResult:n=this._encoder.encode([Pt.Completion,e.headers||{},e.invocationId,t]);break;case this._nonVoidResult:n=this._encoder.encode([Pt.Completion,e.headers||{},e.invocationId,t,e.result])}return Un.write(n.slice())}_writeCancelInvocation(e){const t=this._encoder.encode([Pt.CancelInvocation,e.headers||{},e.invocationId]);return Un.write(t.slice())}_readHeaders(e){const t=e[1];if("object"!=typeof t)throw new Error("Invalid headers.");return t}}let Bn=!1;function $n(){const e=document.querySelector("#blazor-error-ui");e&&(e.style.display="block"),Bn||(Bn=!0,document.querySelectorAll("#blazor-error-ui .reload").forEach((e=>{e.onclick=function(e){location.reload(),e.preventDefault()}})),document.querySelectorAll("#blazor-error-ui .dismiss").forEach((e=>{e.onclick=function(e){const t=document.querySelector("#blazor-error-ui");t&&(t.style.display="none"),e.preventDefault()}})))}const On="function"==typeof TextDecoder?new TextDecoder("utf-8"):null,Fn=On?On.decode.bind(On):function(e){let t=0;const n=e.length,r=[],o=[];for(;t65535&&(o-=65536,r.push(o>>>10&1023|55296),o=56320|1023&o),r.push(o)}r.length>1024&&(o.push(String.fromCharCode.apply(null,r)),r.length=0)}return o.push(String.fromCharCode.apply(null,r)),o.join("")},Hn=Math.pow(2,32),jn=Math.pow(2,21)-1;function Wn(e,t){return e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24}function zn(e,t){return e[t]+(e[t+1]<<8)+(e[t+2]<<16)+(e[t+3]<<24>>>0)}function Jn(e,t){const n=zn(e,t+4);if(n>jn)throw new Error(`Cannot read uint64 with high order part ${n}, because the result would exceed Number.MAX_SAFE_INTEGER.`);return n*Hn+zn(e,t)}class qn{constructor(e){this.batchData=e;const t=new Yn(e);this.arrayRangeReader=new Gn(e),this.arrayBuilderSegmentReader=new Qn(e),this.diffReader=new Vn(e),this.editReader=new Kn(e,t),this.frameReader=new Xn(e,t)}updatedComponents(){return Wn(this.batchData,this.batchData.length-20)}referenceFrames(){return Wn(this.batchData,this.batchData.length-16)}disposedComponentIds(){return Wn(this.batchData,this.batchData.length-12)}disposedEventHandlerIds(){return Wn(this.batchData,this.batchData.length-8)}updatedComponentsEntry(e,t){const n=e+4*t;return Wn(this.batchData,n)}referenceFramesEntry(e,t){return e+20*t}disposedComponentIdsEntry(e,t){const n=e+4*t;return Wn(this.batchData,n)}disposedEventHandlerIdsEntry(e,t){const n=e+8*t;return Jn(this.batchData,n)}}class Vn{constructor(e){this.batchDataUint8=e}componentId(e){return Wn(this.batchDataUint8,e)}edits(e){return e+4}editsEntry(e,t){return e+16*t}}class Kn{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}editType(e){return Wn(this.batchDataUint8,e)}siblingIndex(e){return Wn(this.batchDataUint8,e+4)}newTreeIndex(e){return Wn(this.batchDataUint8,e+8)}moveToSiblingIndex(e){return Wn(this.batchDataUint8,e+8)}removedAttributeName(e){const t=Wn(this.batchDataUint8,e+12);return this.stringReader.readString(t)}}class Xn{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}frameType(e){return Wn(this.batchDataUint8,e)}subtreeLength(e){return Wn(this.batchDataUint8,e+4)}elementReferenceCaptureId(e){const t=Wn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}componentId(e){return Wn(this.batchDataUint8,e+8)}elementName(e){const t=Wn(this.batchDataUint8,e+8);return this.stringReader.readString(t)}textContent(e){const t=Wn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}markupContent(e){const t=Wn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeName(e){const t=Wn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeValue(e){const t=Wn(this.batchDataUint8,e+8);return this.stringReader.readString(t)}attributeEventHandlerId(e){return Jn(this.batchDataUint8,e+12)}}class Yn{constructor(e){this.batchDataUint8=e,this.stringTableStartIndex=Wn(e,e.length-4)}readString(e){if(-1===e)return null;{const n=Wn(this.batchDataUint8,this.stringTableStartIndex+4*e),r=function(e,t){let n=0,r=0;for(let o=0;o<4;o++){const i=e[t+o];if(n|=(127&i)<this.nextBatchId)return this.fatalError?(this.logger.log(Zn.Debug,`Received a new batch ${e} but errored out on a previous batch ${this.nextBatchId-1}`),void await n.send("OnRenderCompleted",this.nextBatchId-1,this.fatalError.toString())):void this.logger.log(Zn.Debug,`Waiting for batch ${this.nextBatchId}. Batch ${e} not processed.`);try{this.nextBatchId++,this.logger.log(Zn.Debug,`Applying batch ${e}.`),function(e,t){const n=pe[e];if(!n)throw new Error(`There is no browser renderer with ID ${e}.`);const r=t.arrayRangeReader,o=t.updatedComponents(),i=r.values(o),s=r.count(o),a=t.referenceFrames(),c=r.values(a),l=t.diffReader;for(let e=0;e=this.minLevel){const n=`[${(new Date).toISOString()}] ${Zn[e]}: ${t}`;switch(e){case Zn.Critical:case Zn.Error:console.error(n);break;case Zn.Warning:console.warn(n);break;case Zn.Information:console.info(n);break;default:console.log(n)}}}}class rr{constructor(e,t){this.circuitId=void 0,this.components=e,this.applicationState=t}reconnect(e){if(!this.circuitId)throw new Error("Circuit host not initialized.");return e.state!==At.Connected?Promise.resolve(!1):e.invoke("ConnectCircuit",this.circuitId)}initialize(e){if(this.circuitId)throw new Error(`Circuit host '${this.circuitId}' already initialized.`);this.circuitId=e}async startCircuit(e){if(e.state!==At.Connected)return!1;const t=await e.invoke("StartCircuit",Ce.getBaseURI(),Ce.getLocationHref(),JSON.stringify(this.components.map((e=>e.toRecord()))),this.applicationState||"");return!!t&&(this.initialize(t),!0)}resolveElement(e){const t=function(e){const t=f.get(e);if(t)return f.delete(e),t}(e);if(t)return $(t,!0);const n=Number.parseInt(e);if(!Number.isNaN(n))return function(e,t){if(!e.parentNode)throw new Error(`Comment not connected to the DOM ${e.textContent}`);const n=e.parentNode,r=$(n,!0),o=J(r);return Array.from(n.childNodes).forEach((e=>o.push(e))),e[L]=r,t&&(e[B]=t,$(t)),$(e)}(this.components[n].start,this.components[n].end);throw new Error(`Invalid sequence number or identifier '${e}'.`)}}const or={configureSignalR:e=>{},logLevel:Zn.Warning,reconnectionOptions:{maxRetries:8,retryIntervalMilliseconds:2e4,dialogId:"components-reconnect-modal"}};class ir{constructor(e,t,n,r){this.maxRetries=t,this.document=n,this.logger=r,this.addedToDom=!1,this.modal=this.document.createElement("div"),this.modal.id=e,this.maxRetries=t,this.modal.style.cssText=["position: fixed","top: 0","right: 0","bottom: 0","left: 0","z-index: 1050","display: none","overflow: hidden","background-color: #fff","opacity: 0.8","text-align: center","font-weight: bold","transition: visibility 0s linear 500ms"].join(";"),this.message=this.document.createElement("h5"),this.message.style.cssText="margin-top: 20px",this.button=this.document.createElement("button"),this.button.style.cssText="margin:5px auto 5px",this.button.textContent="Retry";const o=this.document.createElement("a");o.addEventListener("click",(()=>location.reload())),o.textContent="reload",this.reloadParagraph=this.document.createElement("p"),this.reloadParagraph.textContent="Alternatively, ",this.reloadParagraph.appendChild(o),this.modal.appendChild(this.message),this.modal.appendChild(this.button),this.modal.appendChild(this.reloadParagraph),this.loader=this.getLoader(),this.message.after(this.loader),this.button.addEventListener("click",(async()=>{this.show();try{await Xe.reconnect()||this.rejected()}catch(e){this.logger.log(Zn.Error,e),this.failed()}}))}show(){this.addedToDom||(this.addedToDom=!0,this.document.body.appendChild(this.modal)),this.modal.style.display="block",this.loader.style.display="inline-block",this.button.style.display="none",this.reloadParagraph.style.display="none",this.message.textContent="Attempting to reconnect to the server...",this.modal.style.visibility="hidden",setTimeout((()=>{this.modal.style.visibility="visible"}),0)}update(e){this.message.textContent=`Attempting to reconnect to the server: ${e} of ${this.maxRetries}`}hide(){this.modal.style.display="none"}failed(){this.button.style.display="block",this.reloadParagraph.style.display="none",this.loader.style.display="none";const e=this.document.createTextNode("Reconnection failed. Try "),t=this.document.createElement("a");t.textContent="reloading",t.setAttribute("href",""),t.addEventListener("click",(()=>location.reload()));const n=this.document.createTextNode(" the page if you're unable to reconnect.");this.message.replaceChildren(e,t,n)}rejected(){this.button.style.display="none",this.reloadParagraph.style.display="none",this.loader.style.display="none";const e=this.document.createTextNode("Could not reconnect to the server. "),t=this.document.createElement("a");t.textContent="Reload",t.setAttribute("href",""),t.addEventListener("click",(()=>location.reload()));const n=this.document.createTextNode(" the page to restore functionality.");this.message.replaceChildren(e,t,n)}getLoader(){const e=this.document.createElement("div");return e.style.cssText=["border: 0.3em solid #f3f3f3","border-top: 0.3em solid #3498db","border-radius: 50%","width: 2em","height: 2em","display: inline-block"].join(";"),e.animate([{transform:"rotate(0deg)"},{transform:"rotate(360deg)"}],{duration:2e3,iterations:1/0}),e}}class sr{constructor(e,t,n){this.dialog=e,this.maxRetries=t,this.document=n,this.document=n;const r=this.document.getElementById(sr.MaxRetriesId);r&&(r.innerText=this.maxRetries.toString())}show(){this.removeClasses(),this.dialog.classList.add(sr.ShowClassName)}update(e){const t=this.document.getElementById(sr.CurrentAttemptId);t&&(t.innerText=e.toString())}hide(){this.removeClasses(),this.dialog.classList.add(sr.HideClassName)}failed(){this.removeClasses(),this.dialog.classList.add(sr.FailedClassName)}rejected(){this.removeClasses(),this.dialog.classList.add(sr.RejectedClassName)}removeClasses(){this.dialog.classList.remove(sr.ShowClassName,sr.HideClassName,sr.FailedClassName,sr.RejectedClassName)}}sr.ShowClassName="components-reconnect-show",sr.HideClassName="components-reconnect-hide",sr.FailedClassName="components-reconnect-failed",sr.RejectedClassName="components-reconnect-rejected",sr.MaxRetriesId="components-reconnect-max-retries",sr.CurrentAttemptId="components-reconnect-current-attempt";class ar{constructor(e,t,n){this._currentReconnectionProcess=null,this._logger=e,this._reconnectionDisplay=t,this._reconnectCallback=n||Xe.reconnect}onConnectionDown(e,t){if(!this._reconnectionDisplay){const t=document.getElementById(e.dialogId);this._reconnectionDisplay=t?new sr(t,e.maxRetries,document):new ir(e.dialogId,e.maxRetries,document,this._logger)}this._currentReconnectionProcess||(this._currentReconnectionProcess=new cr(e,this._logger,this._reconnectCallback,this._reconnectionDisplay))}onConnectionUp(){this._currentReconnectionProcess&&(this._currentReconnectionProcess.dispose(),this._currentReconnectionProcess=null)}}class cr{constructor(e,t,n,r){this.logger=t,this.reconnectCallback=n,this.isDisposed=!1,this.reconnectDisplay=r,this.reconnectDisplay.show(),this.attemptPeriodicReconnection(e)}dispose(){this.isDisposed=!0,this.reconnectDisplay.hide()}async attemptPeriodicReconnection(e){for(let t=0;tcr.MaximumFirstRetryInterval?cr.MaximumFirstRetryInterval:e.retryIntervalMilliseconds;if(await this.delay(n),this.isDisposed)break;try{return await this.reconnectCallback()?void 0:void this.reconnectDisplay.rejected()}catch(e){this.logger.log(Zn.Error,e)}}this.reconnectDisplay.failed()}delay(e){return new Promise((t=>setTimeout(t,e)))}}cr.MaximumFirstRetryInterval=3e3;const lr=/^\s*Blazor-Component-State:(?[a-zA-Z0-9+/=]+)$/;function hr(e){var t;if(e.nodeType===Node.COMMENT_NODE){const n=e.textContent||"",r=lr.exec(n),o=r&&r.groups&&r.groups.state;return o&&(null===(t=e.parentNode)||void 0===t||t.removeChild(e)),o}if(!e.hasChildNodes())return;const n=e.childNodes;for(let e=0;e.*)$/);function pr(e,t){const n=e.currentElement;if(n&&n.nodeType===Node.COMMENT_NODE&&n.textContent){const r=ur.exec(n.textContent),o=r&&r.groups&&r.groups.descriptor;if(!o)return;try{const r=function(e){const t=JSON.parse(e),{type:n}=t;if("server"!==n&&"webassembly"!==n)throw new Error(`Invalid component type '${n}'.`);return t}(o);switch(t){case"webassembly":return function(e,t,n){const{type:r,assembly:o,typeName:i,parameterDefinitions:s,parameterValues:a,prerenderId:c}=e,l=c?fr(c,n):void 0;if(c&&!l)throw new Error(`Could not find an end component comment for '${t}'.`);if("webassembly"===r){if(!o)throw new Error("assembly must be defined when using a descriptor.");if(!i)throw new Error("typeName must be defined when using a descriptor.");return{type:r,assembly:o,typeName:i,parameterDefinitions:s&&atob(s),parameterValues:a&&atob(a),start:t,prerenderId:c,end:l}}}(r,n,e);case"server":return function(e,t,n){const{type:r,descriptor:o,sequence:i,prerenderId:s}=e,a=s?fr(s,n):void 0;if(s&&!a)throw new Error(`Could not find an end component comment for '${t}'.`);if("server"===r){if(!o)throw new Error("descriptor must be defined when using a descriptor.");if(void 0===i)throw new Error("sequence must be defined when using a descriptor.");if(!Number.isInteger(i))throw new Error(`Error parsing the sequence '${i}' for component '${JSON.stringify(e)}'`);return{type:r,sequence:i,descriptor:o,start:t,prerenderId:s,end:a}}}(r,n,e)}}catch(e){throw new Error(`Found malformed component comment at ${n.textContent}`)}}}function fr(e,t){for(;t.next()&&t.currentElement;){const n=t.currentElement;if(n.nodeType!==Node.COMMENT_NODE)continue;if(!n.textContent)continue;const r=ur.exec(n.textContent),o=r&&r[1];if(o)return gr(o,e),n}}function gr(e,t){const n=JSON.parse(e);if(1!==Object.keys(n).length)throw new Error(`Invalid end of component comment: '${e}'`);const r=n.prerenderId;if(!r)throw new Error(`End of component comment must have a value for the prerendered property: '${e}'`);if(r!==t)throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${t}', '${r}'`)}class mr{constructor(e){this.childNodes=e,this.currentIndex=-1,this.length=e.length}next(){return this.currentIndex++,this.currentIndexasync function(e,n){const r=function(e){const t=document.baseURI;return t.endsWith("/")?`${t}${e}`:`${t}/${e}`}(n),o=await import(r);if(void 0===o)return;const{beforeStart:i,afterStarted:s}=o;return s&&e.afterStartedCallbacks.push(s),i?i(...t):void 0}(this,e))))}async invokeAfterStartedCallbacks(e){await C,await Promise.all(this.afterStartedCallbacks.map((t=>t(e))))}}let br,_r,Er,Sr=!1;async function Cr(t,n){const r=function(e){const t={...or,...e};return e&&e.reconnectionOptions&&(t.reconnectionOptions={...or.reconnectionOptions,...e.reconnectionOptions}),t}(t),o=await async function(e){const t=await fetch("_blazor/initializers",{method:"GET",credentials:"include",cache:"no-cache"}),n=await t.json(),r=new wr;return await r.importInitializersAsync(n,[e]),r}(r),i=new nr(r.logLevel);Xe.reconnect=async e=>{if(Sr)return!1;const t=e||await Ir(r,i,_r);return await _r.reconnect(t)?(r.reconnectionHandler.onConnectionUp(),!0):(i.log(Zn.Information,"Reconnection attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server."),!1)},Xe.defaultReconnectionHandler=new ar(i),r.reconnectionHandler=r.reconnectionHandler||Xe.defaultReconnectionHandler,i.log(Zn.Information,"Starting up Blazor server-side application.");const s=hr(document);_r=new rr(n||[],s||""),Xe._internal.navigationManager.listenForNavigationEvents(((e,t,n)=>br.send("OnLocationChanged",e,t,n)),((e,t,n,r)=>br.send("OnLocationChanging",e,t,n,r))),Xe._internal.forceCloseConnection=()=>br.stop(),Xe._internal.sendJSDataStream=(e,t,n)=>function(e,t,n,r){setTimeout((async()=>{let o=5,i=(new Date).valueOf();try{const s=t instanceof Blob?t.size:t.byteLength;let a=0,c=0;for(;a1)await e.send("ReceiveJSDataChunk",n,c,h,null);else{if(!await e.invoke("ReceiveJSDataChunk",n,c,h,null))break;const t=(new Date).valueOf(),r=t-i;i=t,o=Math.max(1,Math.round(500/Math.max(1,r)))}a+=l,c++}}catch(t){await e.send("ReceiveJSDataChunk",n,-1,null,t.toString())}}),0)}(br,e,t,n),Er=e.attachDispatcher({beginInvokeDotNetFromJS:(e,t,n,r,o)=>{br.send("BeginInvokeDotNetFromJS",e?e.toString():null,t,n,r||0,o)},endInvokeJSFromDotNet:(e,t,n)=>{br.send("EndInvokeJSFromDotNet",e,t,n)},sendByteArray:(e,t)=>{br.send("ReceiveByteArray",e,t)}});const a=await Ir(r,i,_r);if(!await _r.startCircuit(a))return void i.log(Zn.Error,"Failed to start the circuit.");let c=!1;const l=()=>{if(!c){const e=new FormData,t=_r.circuitId;e.append("circuitId",t),c=navigator.sendBeacon("_blazor/disconnect",e)}};Xe.disconnect=l,window.addEventListener("unload",l,{capture:!1,once:!0}),i.log(Zn.Information,"Blazor server-side application started."),o.invokeAfterStartedCallbacks(Xe)}async function Ir(e,t,n){var r,o;const i=new Ln;i.name="blazorpack";const s=(new qt).withUrl("_blazor").withHubProtocol(i);e.configureSignalR(s);const a=s.build();a.on("JS.AttachComponent",((e,t)=>function(e,t,n,r){let o=pe[0];o||(o=new ae(0),pe[0]=o),o.attachRootComponentToLogicalElement(n,t,!1)}(0,n.resolveElement(t),e))),a.on("JS.BeginInvokeJS",Er.beginInvokeJSFromDotNet.bind(Er)),a.on("JS.EndInvokeDotNet",Er.endInvokeDotNetFromJS.bind(Er)),a.on("JS.ReceiveByteArray",Er.receiveByteArray.bind(Er)),a.on("JS.BeginTransmitStream",(e=>{const t=new ReadableStream({start(t){a.stream("SendDotNetStreamToJS",e).subscribe({next:e=>t.enqueue(e),complete:()=>t.close(),error:e=>t.error(e)})}});Er.supplyDotNetStream(e,t)}));const c=er.getOrCreate(t);a.on("JS.RenderBatch",((e,n)=>{t.log(Zn.Debug,`Received render batch with id ${e} and ${n.byteLength} bytes.`),c.processBatch(e,n,a)})),a.on("JS.EndLocationChanging",Xe._internal.navigationManager.endLocationChanging),a.onclose((t=>!Sr&&e.reconnectionHandler.onConnectionDown(e.reconnectionOptions,t))),a.on("JS.Error",(e=>{Sr=!0,kr(a,e,t),$n()}));try{await a.start(),br=a}catch(e){if(kr(a,e,t),"FailedToNegotiateWithServerError"===e.errorType)throw e;$n(),e.innerErrors&&(e.innerErrors.some((e=>"UnsupportedTransportError"===e.errorType&&e.transport===xt.WebSockets))?t.log(Zn.Error,"Unable to connect, please ensure you are using an updated browser that supports WebSockets."):e.innerErrors.some((e=>"FailedToStartTransportError"===e.errorType&&e.transport===xt.WebSockets))?t.log(Zn.Error,"Unable to connect, please ensure WebSockets are available. A VPN or proxy may be blocking the connection."):e.innerErrors.some((e=>"DisabledTransportError"===e.errorType&&e.transport===xt.LongPolling))&&t.log(Zn.Error,"Unable to initiate a SignalR connection to the server. This might be because the server is not configured to support WebSockets. For additional details, visit https://aka.ms/blazor-server-websockets-error."))}return(null===(o=null===(r=a.connection)||void 0===r?void 0:r.features)||void 0===o?void 0:o.inherentKeepAlive)&&t.log(Zn.Warning,"Failed to connect via WebSockets, using the Long Polling fallback transport. This may be due to a VPN or proxy blocking the connection. To troubleshoot this, visit https://aka.ms/blazor-server-using-fallback-long-polling."),a}function kr(e,t,n){n.log(Zn.Error,t),e&&e.stop()}let Tr=!1;function Dr(e){if(Tr)throw new Error("Blazor has already started.");return Tr=!0,Cr(e,function(e,t){return function(e){const t=dr(e,"server"),n=[];for(let e=0;ee.sequence-t.sequence))}(e)}(document))}Xe.start=Dr,window.DotNet=e,document&&document.currentScript&&"false"!==document.currentScript.getAttribute("autostart")&&Dr()})(); \ No newline at end of file diff --git a/src/Components/Web.JS/dist/Release/blazor.web.js b/src/Components/Web.JS/dist/Release/blazor.web.js index 2424470d63e8..8096227b2000 100644 --- a/src/Components/Web.JS/dist/Release/blazor.web.js +++ b/src/Components/Web.JS/dist/Release/blazor.web.js @@ -1 +1 @@ -(()=>{"use strict";var e,t,n,r={};r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),function(e){const t=[],n="__jsObjectId",r="__dotNetObject",o="__byte[]",i="__dotNetStream",s="__jsStreamReferenceLength";let a,c;class l{constructor(e){this._jsObject=e,this._cachedFunctions=new Map}findFunction(e){const t=this._cachedFunctions.get(e);if(t)return t;let n,r=this._jsObject;if(e.split(".").forEach((t=>{if(!(t in r))throw new Error(`Could not find '${e}' ('${t}' was undefined).`);n=r,r=r[t]})),r instanceof Function)return r=r.bind(n),this._cachedFunctions.set(e,r),r;throw new Error(`The value '${e}' is not a function.`)}getWrappedObject(){return this._jsObject}}const h={0:new l(window)};h[0]._cachedFunctions.set("import",(e=>("string"==typeof e&&e.startsWith("./")&&(e=new URL(e.substr(2),document.baseURI).toString()),import(e))));let d,u=1;function p(e){t.push(e)}function f(e){if(e&&"object"==typeof e){h[u]=new l(e);const t={[n]:u};return u++,t}throw new Error(`Cannot create a JSObjectReference from the value '${e}'.`)}function g(e){let t=-1;if(e instanceof ArrayBuffer&&(e=new Uint8Array(e)),e instanceof Blob)t=e.size;else{if(!(e.buffer instanceof ArrayBuffer))throw new Error("Supplied value is not a typed array or blob.");if(void 0===e.byteLength)throw new Error(`Cannot create a JSStreamReference from the value '${e}' as it doesn't have a byteLength.`);t=e.byteLength}const r={[s]:t};try{const t=f(e);r[n]=t[n]}catch(t){throw new Error(`Cannot create a JSStreamReference from the value '${e}'.`)}return r}function m(e,n){c=e;const r=n?JSON.parse(n,((e,n)=>t.reduce(((t,n)=>n(e,t)),n))):null;return c=void 0,r}function y(){if(void 0===a)throw new Error("No call dispatcher has been set.");if(null===a)throw new Error("There are multiple .NET runtimes present, so a default dispatcher could not be resolved. Use DotNetObject to invoke .NET instance methods.");return a}e.attachDispatcher=function(e){const t=new w(e);return void 0===a?a=t:a&&(a=null),t},e.attachReviver=p,e.invokeMethod=function(e,t,...n){return y().invokeDotNetStaticMethod(e,t,...n)},e.invokeMethodAsync=function(e,t,...n){return y().invokeDotNetStaticMethodAsync(e,t,...n)},e.createJSObjectReference=f,e.createJSStreamReference=g,e.disposeJSObjectReference=function(e){const t=e&&e[n];"number"==typeof t&&function(e){delete h[e]}(t)},function(e){e[e.Default=0]="Default",e[e.JSObjectReference=1]="JSObjectReference",e[e.JSStreamReference=2]="JSStreamReference",e[e.JSVoidResult=3]="JSVoidResult"}(d=e.JSCallResultType||(e.JSCallResultType={}));class w{constructor(e){this._dotNetCallDispatcher=e,this._byteArraysToBeRevived=new Map,this._pendingDotNetToJSStreams=new Map,this._pendingAsyncCalls={},this._nextAsyncCallId=1}getDotNetCallDispatcher(){return this._dotNetCallDispatcher}invokeJSFromDotNet(e,t,n,r){const o=m(this,t),i=C(b(e,r)(...o||[]),n);return null==i?null:k(this,i)}beginInvokeJSFromDotNet(e,t,n,r,o){const i=new Promise((e=>{const r=m(this,n);e(b(t,o)(...r||[]))}));e&&i.then((t=>k(this,[e,!0,C(t,r)]))).then((t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!0,t)),(t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!1,JSON.stringify([e,!1,v(t)]))))}endInvokeDotNetFromJS(e,t,n){const r=t?m(this,n):new Error(n);this.completePendingCall(parseInt(e,10),t,r)}invokeDotNetStaticMethod(e,t,...n){return this.invokeDotNetMethod(e,t,null,n)}invokeDotNetStaticMethodAsync(e,t,...n){return this.invokeDotNetMethodAsync(e,t,null,n)}invokeDotNetMethod(e,t,n,r){if(this._dotNetCallDispatcher.invokeDotNetFromJS){const o=k(this,r),i=this._dotNetCallDispatcher.invokeDotNetFromJS(e,t,n,o);return i?m(this,i):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeDotNetMethodAsync instead.")}invokeDotNetMethodAsync(e,t,n,r){if(e&&n)throw new Error(`For instance method calls, assemblyName should be null. Received '${e}'.`);const o=this._nextAsyncCallId++,i=new Promise(((e,t)=>{this._pendingAsyncCalls[o]={resolve:e,reject:t}}));try{const i=k(this,r);this._dotNetCallDispatcher.beginInvokeDotNetFromJS(o,e,t,n,i)}catch(e){this.completePendingCall(o,!1,e)}return i}receiveByteArray(e,t){this._byteArraysToBeRevived.set(e,t)}processByteArray(e){const t=this._byteArraysToBeRevived.get(e);return t?(this._byteArraysToBeRevived.delete(e),t):null}supplyDotNetStream(e,t){if(this._pendingDotNetToJSStreams.has(e)){const n=this._pendingDotNetToJSStreams.get(e);this._pendingDotNetToJSStreams.delete(e),n.resolve(t)}else{const n=new S;n.resolve(t),this._pendingDotNetToJSStreams.set(e,n)}}getDotNetStreamPromise(e){let t;if(this._pendingDotNetToJSStreams.has(e))t=this._pendingDotNetToJSStreams.get(e).streamPromise,this._pendingDotNetToJSStreams.delete(e);else{const n=new S;this._pendingDotNetToJSStreams.set(e,n),t=n.streamPromise}return t}completePendingCall(e,t,n){if(!this._pendingAsyncCalls.hasOwnProperty(e))throw new Error(`There is no pending async call with ID ${e}.`);const r=this._pendingAsyncCalls[e];delete this._pendingAsyncCalls[e],t?r.resolve(n):r.reject(n)}}function v(e){return e instanceof Error?`${e.message}\n${e.stack}`:e?e.toString():"null"}function b(e,t){const n=h[t];if(n)return n.findFunction(e);throw new Error(`JS object instance with ID ${t} does not exist (has it been disposed?).`)}e.findJSFunction=b;class _{constructor(e,t){this._id=e,this._callDispatcher=t}invokeMethod(e,...t){return this._callDispatcher.invokeDotNetMethod(null,e,this._id,t)}invokeMethodAsync(e,...t){return this._callDispatcher.invokeDotNetMethodAsync(null,e,this._id,t)}dispose(){this._callDispatcher.invokeDotNetMethodAsync(null,"__Dispose",this._id,null).catch((e=>console.error(e)))}serializeAsArg(){return{[r]:this._id}}}e.DotNetObject=_,p((function(e,t){if(t&&"object"==typeof t){if(t.hasOwnProperty(r))return new _(t[r],c);if(t.hasOwnProperty(n)){const e=t[n],r=h[e];if(r)return r.getWrappedObject();throw new Error(`JS object instance with Id '${e}' does not exist. It may have been disposed.`)}if(t.hasOwnProperty(o)){const e=t[o],n=c.processByteArray(e);if(void 0===n)throw new Error(`Byte array index '${e}' does not exist.`);return n}if(t.hasOwnProperty(i)){const e=t[i],n=c.getDotNetStreamPromise(e);return new E(n)}}return t}));class E{constructor(e){this._streamPromise=e}stream(){return this._streamPromise}async arrayBuffer(){return new Response(await this.stream()).arrayBuffer()}}class S{constructor(){this.streamPromise=new Promise(((e,t)=>{this.resolve=e,this.reject=t}))}}function C(e,t){switch(t){case d.Default:return e;case d.JSObjectReference:return f(e);case d.JSStreamReference:return g(e);case d.JSVoidResult:return null;default:throw new Error(`Invalid JS call result type '${t}'.`)}}let I=0;function k(e,t){I=0,c=e;const n=JSON.stringify(t,T);return c=void 0,n}function T(e,t){if(t instanceof _)return t.serializeAsArg();if(t instanceof Uint8Array){c.getDotNetCallDispatcher().sendByteArray(I,t);const e={[o]:I};return I++,e}return t}}(e||(e={})),function(e){e[e.prependFrame=1]="prependFrame",e[e.removeFrame=2]="removeFrame",e[e.setAttribute=3]="setAttribute",e[e.removeAttribute=4]="removeAttribute",e[e.updateText=5]="updateText",e[e.stepIn=6]="stepIn",e[e.stepOut=7]="stepOut",e[e.updateMarkup=8]="updateMarkup",e[e.permutationListEntry=9]="permutationListEntry",e[e.permutationListEnd=10]="permutationListEnd"}(t||(t={})),function(e){e[e.element=1]="element",e[e.text=2]="text",e[e.attribute=3]="attribute",e[e.component=4]="component",e[e.region=5]="region",e[e.elementReferenceCapture=6]="elementReferenceCapture",e[e.markup=8]="markup"}(n||(n={}));class o{constructor(e,t){this.componentId=e,this.fieldValue=t}static fromEvent(e,t){const n=t.target;if(n instanceof Element){const t=function(e){return e instanceof HTMLInputElement?e.type&&"checkbox"===e.type.toLowerCase()?{value:e.checked}:{value:e.value}:e instanceof HTMLSelectElement||e instanceof HTMLTextAreaElement?{value:e.value}:null}(n);if(t)return new o(e,t.value)}return null}}const i=new Map,s=new Map,a=[];function c(e){return i.get(e)}function l(e){const t=i.get(e);return(null==t?void 0:t.browserEventName)||e}function h(e,t){e.forEach((e=>i.set(e,t)))}function d(e){const t=[];for(let n=0;ne.selected)).map((e=>e.value))}}{const e=function(e){return!!e&&"INPUT"===e.tagName&&"checkbox"===e.getAttribute("type")}(t);return{value:e?!!t.checked:t.value}}}}),h(["copy","cut","paste"],{createEventArgs:e=>({type:e.type})}),h(["drag","dragend","dragenter","dragleave","dragover","dragstart","drop"],{createEventArgs:e=>{return{...u(t=e),dataTransfer:t.dataTransfer?{dropEffect:t.dataTransfer.dropEffect,effectAllowed:t.dataTransfer.effectAllowed,files:Array.from(t.dataTransfer.files).map((e=>e.name)),items:Array.from(t.dataTransfer.items).map((e=>({kind:e.kind,type:e.type}))),types:t.dataTransfer.types}:null};var t}}),h(["focus","blur","focusin","focusout"],{createEventArgs:e=>({type:e.type})}),h(["keydown","keyup","keypress"],{createEventArgs:e=>{return{key:(t=e).key,code:t.code,location:t.location,repeat:t.repeat,ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),h(["contextmenu","click","mouseover","mouseout","mousemove","mousedown","mouseup","mouseleave","mouseenter","dblclick"],{createEventArgs:e=>u(e)}),h(["error"],{createEventArgs:e=>{return{message:(t=e).message,filename:t.filename,lineno:t.lineno,colno:t.colno,type:t.type};var t}}),h(["loadstart","timeout","abort","load","loadend","progress"],{createEventArgs:e=>{return{lengthComputable:(t=e).lengthComputable,loaded:t.loaded,total:t.total,type:t.type};var t}}),h(["touchcancel","touchend","touchmove","touchenter","touchleave","touchstart"],{createEventArgs:e=>{return{detail:(t=e).detail,touches:d(t.touches),targetTouches:d(t.targetTouches),changedTouches:d(t.changedTouches),ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),h(["gotpointercapture","lostpointercapture","pointercancel","pointerdown","pointerenter","pointerleave","pointermove","pointerout","pointerover","pointerup"],{createEventArgs:e=>{return{...u(t=e),pointerId:t.pointerId,width:t.width,height:t.height,pressure:t.pressure,tiltX:t.tiltX,tiltY:t.tiltY,pointerType:t.pointerType,isPrimary:t.isPrimary};var t}}),h(["wheel","mousewheel"],{createEventArgs:e=>{return{...u(t=e),deltaX:t.deltaX,deltaY:t.deltaY,deltaZ:t.deltaZ,deltaMode:t.deltaMode};var t}}),h(["toggle"],{createEventArgs:()=>({})});const p=["date","datetime-local","month","time","week"],f=new Map;let g,m,y=0;const w={async add(e,t,n){if(!n)throw new Error("initialParameters must be an object, even if empty.");const r="__bl-dynamic-root:"+(++y).toString();f.set(r,e);const o=await E().invokeMethodAsync("AddRootComponent",t,r),i=new _(o,m[t]);return await i.setParameters(n),i}};function v(e){const t=f.get(e);if(t)return f.delete(e),t}class b{invoke(e){return this._callback(e)}setCallback(t){this._selfJSObjectReference||(this._selfJSObjectReference=e.createJSObjectReference(this)),this._callback=t}getJSObjectReference(){return this._selfJSObjectReference}dispose(){this._selfJSObjectReference&&e.disposeJSObjectReference(this._selfJSObjectReference)}}class _{constructor(e,t){this._jsEventCallbackWrappers=new Map,this._componentId=e;for(const e of t)"eventcallback"===e.type&&this._jsEventCallbackWrappers.set(e.name.toLowerCase(),new b)}setParameters(e){const t={},n=Object.entries(e||{}),r=n.length;for(const[e,r]of n){const n=this._jsEventCallbackWrappers.get(e.toLowerCase());n&&r?(n.setCallback(r),t[e]=n.getJSObjectReference()):t[e]=r}return E().invokeMethodAsync("SetRootComponentParameters",this._componentId,r,t)}async dispose(){if(null!==this._componentId){await E().invokeMethodAsync("RemoveRootComponent",this._componentId),this._componentId=null;for(const e of this._jsEventCallbackWrappers.values())e.dispose()}}}function E(){if(!g)throw new Error("Dynamic root components have not been enabled in this application.");return g}const S=[];let C;const I=new Promise((e=>{C=e}));function k(e,t,n){return D(e,t.eventHandlerId,(()=>T(e).invokeMethodAsync("DispatchEventAsync",t,n)))}function T(e){const t=S[e];if(!t)throw new Error(`No interop methods are registered for renderer ${e}`);return t}let D=(e,t,n)=>n();const x=L(["abort","blur","canplay","canplaythrough","change","cuechange","durationchange","emptied","ended","error","focus","load","loadeddata","loadedmetadata","loadend","loadstart","mouseenter","mouseleave","pointerenter","pointerleave","pause","play","playing","progress","ratechange","reset","scroll","seeked","seeking","stalled","submit","suspend","timeupdate","toggle","unload","volumechange","waiting","DOMNodeInsertedIntoDocument","DOMNodeRemovedFromDocument"]),A={submit:!0},R=L(["click","dblclick","mousedown","mousemove","mouseup"]);class N{constructor(e){this.browserRendererId=e,this.afterClickCallbacks=[];const t=++N.nextEventDelegatorId;this.eventsCollectionKey=`_blazorEvents_${t}`,this.eventInfoStore=new P(this.onGlobalEvent.bind(this))}setListener(e,t,n,r){const o=this.getEventHandlerInfosForElement(e,!0),i=o.getHandler(t);if(i)this.eventInfoStore.update(i.eventHandlerId,n);else{const i={element:e,eventName:t,eventHandlerId:n,renderingComponentId:r};this.eventInfoStore.add(i),o.setHandler(t,i)}}getHandler(e){return this.eventInfoStore.get(e)}removeListener(e){const t=this.eventInfoStore.remove(e);if(t){const e=t.element,n=this.getEventHandlerInfosForElement(e,!1);n&&n.removeHandler(t.eventName)}}notifyAfterClick(e){this.afterClickCallbacks.push(e),this.eventInfoStore.addGlobalListener("click")}setStopPropagation(e,t,n){this.getEventHandlerInfosForElement(e,!0).stopPropagation(t,n)}setPreventDefault(e,t,n){this.getEventHandlerInfosForElement(e,!0).preventDefault(t,n)}onGlobalEvent(e){if(!(e.target instanceof Element))return;this.dispatchGlobalEventToAllElements(e.type,e);const t=(n=e.type,s.get(n));var n;t&&t.forEach((t=>this.dispatchGlobalEventToAllElements(t,e))),"click"===e.type&&this.afterClickCallbacks.forEach((t=>t(e)))}dispatchGlobalEventToAllElements(e,t){const n=t.composedPath();let r=n.shift(),i=null,s=!1;const a=Object.prototype.hasOwnProperty.call(x,e);let l=!1;for(;r;){const u=r,p=this.getEventHandlerInfosForElement(u,!1);if(p){const n=p.getHandler(e);if(n&&(h=u,d=t.type,!((h instanceof HTMLButtonElement||h instanceof HTMLInputElement||h instanceof HTMLTextAreaElement||h instanceof HTMLSelectElement)&&Object.prototype.hasOwnProperty.call(R,d)&&h.disabled))){if(!s){const n=c(e);i=(null==n?void 0:n.createEventArgs)?n.createEventArgs(t):{},s=!0}Object.prototype.hasOwnProperty.call(A,t.type)&&t.preventDefault(),k(this.browserRendererId,{eventHandlerId:n.eventHandlerId,eventName:e,eventFieldInfo:o.fromEvent(n.renderingComponentId,t)},i)}p.stopPropagation(e)&&(l=!0),p.preventDefault(e)&&t.preventDefault()}r=a||l?void 0:n.shift()}var h,d}getEventHandlerInfosForElement(e,t){return Object.prototype.hasOwnProperty.call(e,this.eventsCollectionKey)?e[this.eventsCollectionKey]:t?e[this.eventsCollectionKey]=new U:null}}N.nextEventDelegatorId=0;class P{constructor(e){this.globalListener=e,this.infosByEventHandlerId={},this.countByEventName={},a.push(this.handleEventNameAliasAdded.bind(this))}add(e){if(this.infosByEventHandlerId[e.eventHandlerId])throw new Error(`Event ${e.eventHandlerId} is already tracked`);this.infosByEventHandlerId[e.eventHandlerId]=e,this.addGlobalListener(e.eventName)}get(e){return this.infosByEventHandlerId[e]}addGlobalListener(e){if(e=l(e),Object.prototype.hasOwnProperty.call(this.countByEventName,e))this.countByEventName[e]++;else{this.countByEventName[e]=1;const t=Object.prototype.hasOwnProperty.call(x,e);document.addEventListener(e,this.globalListener,t)}}update(e,t){if(Object.prototype.hasOwnProperty.call(this.infosByEventHandlerId,t))throw new Error(`Event ${t} is already tracked`);const n=this.infosByEventHandlerId[e];delete this.infosByEventHandlerId[e],n.eventHandlerId=t,this.infosByEventHandlerId[t]=n}remove(e){const t=this.infosByEventHandlerId[e];if(t){delete this.infosByEventHandlerId[e];const n=l(t.eventName);0==--this.countByEventName[n]&&(delete this.countByEventName[n],document.removeEventListener(n,this.globalListener))}return t}handleEventNameAliasAdded(e,t){if(Object.prototype.hasOwnProperty.call(this.countByEventName,e)){const n=this.countByEventName[e];delete this.countByEventName[e],document.removeEventListener(e,this.globalListener),this.addGlobalListener(t),this.countByEventName[t]+=n-1}}}class U{constructor(){this.handlers={},this.preventDefaultFlags=null,this.stopPropagationFlags=null}getHandler(e){return Object.prototype.hasOwnProperty.call(this.handlers,e)?this.handlers[e]:null}setHandler(e,t){this.handlers[e]=t}removeHandler(e){delete this.handlers[e]}preventDefault(e,t){return void 0!==t&&(this.preventDefaultFlags=this.preventDefaultFlags||{},this.preventDefaultFlags[e]=t),!!this.preventDefaultFlags&&this.preventDefaultFlags[e]}stopPropagation(e,t){return void 0!==t&&(this.stopPropagationFlags=this.stopPropagationFlags||{},this.stopPropagationFlags[e]=t),!!this.stopPropagationFlags&&this.stopPropagationFlags[e]}}function L(e){const t={};return e.forEach((e=>{t[e]=!0})),t}const M=Z("_blazorLogicalChildren"),B=Z("_blazorLogicalParent"),F=Z("_blazorLogicalEnd");function $(e,t){if(!e.parentNode)throw new Error(`Comment not connected to the DOM ${e.textContent}`);const n=e.parentNode,r=O(n,!0),o=V(r);return Array.from(n.childNodes).forEach((e=>o.push(e))),e[B]=r,t&&(e[F]=t,O(t)),O(e)}function O(e,t){if(e.childNodes.length>0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return M in e||(e[M]=[]),e}function H(e,t){const n=document.createComment("!");return j(n,e,t),n}function j(e,t,n){const r=e;if(e instanceof Comment&&V(r)&&V(r).length>0)throw new Error("Not implemented: inserting non-empty logical container");if(z(r))throw new Error("Not implemented: moving existing logical children");const o=V(t);if(n0;)W(n,0)}const r=n;r.parentNode.removeChild(r)}function z(e){return e[B]||null}function J(e,t){return V(e)[t]}function q(e){const t=X(e);return"http://www.w3.org/2000/svg"===t.namespaceURI&&"foreignObject"!==t.tagName}function V(e){return e[M]}function K(e,t){const n=V(e);t.forEach((e=>{e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=Q(e.moveRangeStart)})),t.forEach((t=>{const r=document.createComment("marker");t.moveToBeforeMarker=r;const o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):Y(r,e)})),t.forEach((e=>{const t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd;let i=r;for(;i;){const e=i.nextSibling;if(n.insertBefore(i,t),i===o)break;i=e}n.removeChild(t)})),t.forEach((e=>{n[e.toSiblingIndex]=e.moveRangeStart}))}function X(e){if(e instanceof Element||e instanceof DocumentFragment)return e;if(e instanceof Comment)return e.parentNode;throw new Error("Not a valid logical element")}function G(e){const t=V(z(e));return t[Array.prototype.indexOf.call(t,e)+1]||null}function Y(e,t){if(t instanceof Element||t instanceof DocumentFragment)t.appendChild(e);else{if(!(t instanceof Comment))throw new Error(`Cannot append node because the parent is not a valid logical element. Parent: ${t}`);{const n=G(t);n?n.parentNode.insertBefore(e,n):Y(e,z(t))}}}function Q(e){if(e instanceof Element||e instanceof DocumentFragment)return e;const t=G(e);if(t)return t.previousSibling;{const t=z(e);return t instanceof Element||t instanceof DocumentFragment?t.lastChild:Q(t)}}function Z(e){return"function"==typeof Symbol?Symbol():e}function ee(e){return`_bl_${e}`}const te="__internalId";e.attachReviver(((e,t)=>t&&"object"==typeof t&&Object.prototype.hasOwnProperty.call(t,te)&&"string"==typeof t[te]?function(e){const t=`[${ee(e)}]`;return document.querySelector(t)}(t[te]):t));const ne="_blazorDeferredValue",re=document.createElement("template"),oe=document.createElementNS("http://www.w3.org/2000/svg","g"),ie={},se="__internal_",ae="preventDefault_",ce="stopPropagation_";class le{constructor(e){this.rootComponentIds=new Set,this.childComponentLocations={},this.eventDelegator=new N(e),this.eventDelegator.notifyAfterClick((e=>{if(!ve)return;if(0!==e.button||function(e){return e.ctrlKey||e.shiftKey||e.altKey||e.metaKey}(e))return;if(e.defaultPrevented)return;const t=function(e){const t=!window._blazorDisableComposedPath&&e.composedPath&&e.composedPath();if(t){for(let e=0;edocument.baseURI,getLocationHref:()=>location.href,scrollToElement:xe};function xe(e){const t=document.getElementById(e);return!!t&&(t.scrollIntoView(),!0)}function Ae(e,t,n=!1){const r=$e(e);!t.forceLoad&&He(r)?Re(r,!1,t.replaceHistoryEntry,t.historyEntryState,n):function(e,t){if(location.href===e){const t=e+"?";history.replaceState(null,"",t),location.replace(e)}else t?location.replace(e):location.href=e}(e,t.replaceHistoryEntry)}async function Re(e,t,n,r,o=!1){if(Ue(),function(e){const t=e.indexOf("#");return t>-1&&location.href.replace(location.hash,"")===e.substring(0,t)}(e))!function(e,t,n){Ne(e,t,n);const r=e.indexOf("#");r!==e.length-1&&xe(e.substring(r+1))}(e,n,r);else{if(!o&&_e&&!await Le(e,r,t))return;me=!0,Ne(e,n,r),await Me(t)}}function Ne(e,t,n){t?history.replaceState({userState:n,_index:Ee},"",e):(Ee++,history.pushState({userState:n,_index:Ee},"",e))}function Pe(e){return new Promise((t=>{const n=ke;ke=()=>{ke=n,t()},history.go(e)}))}function Ue(){Te&&(Te(!1),Te=null)}function Le(e,t,n){return new Promise((r=>{Ue(),Ie?(Se++,Te=r,Ie(Se,e,t,n)):r(!1)}))}async function Me(e){var t;Ce&&await Ce(location.href,null===(t=history.state)||void 0===t?void 0:t.userState,e)}async function Be(e){var t,n;ke&&await ke(e),Ee=null!==(n=null===(t=history.state)||void 0===t?void 0:t._index)&&void 0!==n?n:0}let Fe;function $e(e){return Fe=Fe||document.createElement("a"),Fe.href=e,Fe.href}function Oe(e,t){return e?e.tagName===t?e:Oe(e.parentElement,t):null}function He(e){const t=(n=document.baseURI).substring(0,n.lastIndexOf("/"));var n;const r=e.charAt(t.length);return e.startsWith(t)&&(""===r||"/"===r||"?"===r||"#"===r)}const je={focus:function(e,t){if(e instanceof HTMLElement)e.focus({preventScroll:t});else{if(!(e instanceof SVGElement))throw new Error("Unable to focus an invalid element.");if(!e.hasAttribute("tabindex"))throw new Error("Unable to focus an SVG element that does not have a tabindex.");e.focus({preventScroll:t})}},focusBySelector:function(e,t){const n=document.querySelector(e);n&&(n.hasAttribute("tabindex")||(n.tabIndex=-1),n.focus({preventScroll:!0}))}},We={init:function(e,t,n,r=50){const o=Je(t);(o||document.documentElement).style.overflowAnchor="none";const i=document.createRange();h(n.parentElement)&&(t.style.display="table-row",n.style.display="table-row");const s=new IntersectionObserver((function(r){r.forEach((r=>{var o;if(!r.isIntersecting)return;i.setStartAfter(t),i.setEndBefore(n);const s=i.getBoundingClientRect().height,a=null===(o=r.rootBounds)||void 0===o?void 0:o.height;r.target===t?e.invokeMethodAsync("OnSpacerBeforeVisible",r.intersectionRect.top-r.boundingClientRect.top,s,a):r.target===n&&n.offsetHeight>0&&e.invokeMethodAsync("OnSpacerAfterVisible",r.boundingClientRect.bottom-r.intersectionRect.bottom,s,a)}))}),{root:o,rootMargin:`${r}px`});s.observe(t),s.observe(n);const a=l(t),c=l(n);function l(e){const t={attributes:!0},n=new MutationObserver(((n,r)=>{h(e.parentElement)&&(r.disconnect(),e.style.display="table-row",r.observe(e,t)),s.unobserve(e),s.observe(e)}));return n.observe(e,t),n}function h(e){return null!==e&&(e instanceof HTMLTableElement&&""===e.style.display||"table"===e.style.display||e instanceof HTMLTableSectionElement&&""===e.style.display||"table-row-group"===e.style.display)}ze[e._id]={intersectionObserver:s,mutationObserverBefore:a,mutationObserverAfter:c}},dispose:function(e){const t=ze[e._id];t&&(t.intersectionObserver.disconnect(),t.mutationObserverBefore.disconnect(),t.mutationObserverAfter.disconnect(),e.dispose(),delete ze[e._id])}},ze={};function Je(e){return e&&e!==document.body&&e!==document.documentElement?"visible"!==getComputedStyle(e).overflowY?e:Je(e.parentElement):null}const qe={getAndRemoveExistingTitle:function(){var e;const t=document.head?document.head.getElementsByTagName("title"):[];if(0===t.length)return null;let n=null;for(let r=t.length-1;r>=0;r--){const o=t[r],i=o.previousSibling;i instanceof Comment&&null!==z(i)||(null===n&&(n=o.textContent),null===(e=o.parentNode)||void 0===e||e.removeChild(o))}return n}},Ve={init:function(e,t){t._blazorInputFileNextFileId=0,t.addEventListener("click",(function(){t.value=""})),t.addEventListener("change",(function(){t._blazorFilesById={};const n=Array.prototype.map.call(t.files,(function(e){const n={id:++t._blazorInputFileNextFileId,lastModified:new Date(e.lastModified).toISOString(),name:e.name,size:e.size,contentType:e.type,readPromise:void 0,arrayBuffer:void 0,blob:e};return t._blazorFilesById[n.id]=n,n}));e.invokeMethodAsync("NotifyChange",n)}))},toImageFile:async function(e,t,n,r,o){const i=Ke(e,t),s=await new Promise((function(e){const t=new Image;t.onload=function(){URL.revokeObjectURL(t.src),e(t)},t.onerror=function(){t.onerror=null,URL.revokeObjectURL(t.src)},t.src=URL.createObjectURL(i.blob)})),a=await new Promise((function(e){var t;const i=Math.min(1,r/s.width),a=Math.min(1,o/s.height),c=Math.min(i,a),l=document.createElement("canvas");l.width=Math.round(s.width*c),l.height=Math.round(s.height*c),null===(t=l.getContext("2d"))||void 0===t||t.drawImage(s,0,0,l.width,l.height),l.toBlob(e,n)})),c={id:++e._blazorInputFileNextFileId,lastModified:i.lastModified,name:i.name,size:(null==a?void 0:a.size)||0,contentType:n,blob:a||i.blob};return e._blazorFilesById[c.id]=c,c},readFileData:async function(e,t){return Ke(e,t).blob}};function Ke(e,t){const n=e._blazorFilesById[t];if(!n)throw new Error(`There is no file with ID ${t}. The file list may have changed. See https://aka.ms/aspnet/blazor-input-file-multiple-selections.`);return n}const Xe=new Set,Ge={enableNavigationPrompt:function(e){0===Xe.size&&window.addEventListener("beforeunload",Ye),Xe.add(e)},disableNavigationPrompt:function(e){Xe.delete(e),0===Xe.size&&window.removeEventListener("beforeunload",Ye)}};function Ye(e){e.preventDefault(),e.returnValue=!0}async function Qe(e,t,n){return e instanceof Blob?await async function(e,t,n){const r=e.slice(t,t+n),o=await r.arrayBuffer();return new Uint8Array(o)}(e,t,n):function(e,t,n){return new Uint8Array(e.buffer,e.byteOffset+t,n)}(e,t,n)}const Ze=new Map,et={navigateTo:function(e,t,n=!1){Ae(e,t instanceof Object?t:{forceLoad:t,replaceHistoryEntry:n})},registerCustomEventType:function(e,t){if(!t)throw new Error("The options parameter is required.");if(i.has(e))throw new Error(`The event '${e}' is already registered.`);if(t.browserEventName){const n=s.get(t.browserEventName);n?n.push(e):s.set(t.browserEventName,[e]),a.forEach((n=>n(e,t.browserEventName)))}i.set(e,t)},rootComponents:w,_internal:{navigationManager:De,domWrapper:je,Virtualize:We,PageTitle:qe,InputFile:Ve,NavigationLock:Ge,getJSDataStreamChunk:Qe,attachWebRendererInterop:function(t,n,r){const o=S.length;return S.push(t),Object.keys(n).length>0&&function(t,n,r){if(g)throw new Error("Dynamic root components have already been enabled.");g=t,m=n;for(const[t,o]of Object.entries(r)){const r=e.findJSFunction(t,0);for(const e of o)r(e,n[e])}}(T(o),n,r),C(),o}}};window.Blazor=et;const tt=[0,2e3,1e4,3e4,null];class nt{constructor(e){this._retryDelays=void 0!==e?[...e,null]:tt}nextRetryDelayInMilliseconds(e){return this._retryDelays[e.previousRetryCount]}}class rt{}rt.Authorization="Authorization",rt.Cookie="Cookie";class ot{constructor(e,t,n){this.statusCode=e,this.statusText=t,this.content=n}}class it{get(e,t){return this.send({...t,method:"GET",url:e})}post(e,t){return this.send({...t,method:"POST",url:e})}delete(e,t){return this.send({...t,method:"DELETE",url:e})}getCookieString(e){return""}}class st extends it{constructor(e,t){super(),this._innerClient=e,this._accessTokenFactory=t}async send(e){let t=!0;this._accessTokenFactory&&(!this._accessToken||e.url&&e.url.indexOf("/negotiate?")>0)&&(t=!1,this._accessToken=await this._accessTokenFactory()),this._setAuthorizationHeader(e);const n=await this._innerClient.send(e);return t&&401===n.statusCode&&this._accessTokenFactory?(this._accessToken=await this._accessTokenFactory(),this._setAuthorizationHeader(e),await this._innerClient.send(e)):n}_setAuthorizationHeader(e){e.headers||(e.headers={}),this._accessToken?e.headers[rt.Authorization]=`Bearer ${this._accessToken}`:this._accessTokenFactory&&e.headers[rt.Authorization]&&delete e.headers[rt.Authorization]}getCookieString(e){return this._innerClient.getCookieString(e)}}class at extends Error{constructor(e,t){const n=new.target.prototype;super(`${e}: Status code '${t}'`),this.statusCode=t,this.__proto__=n}}class ct extends Error{constructor(e="A timeout occurred."){const t=new.target.prototype;super(e),this.__proto__=t}}class lt extends Error{constructor(e="An abort occurred."){const t=new.target.prototype;super(e),this.__proto__=t}}class ht extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="UnsupportedTransportError",this.__proto__=n}}class dt extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="DisabledTransportError",this.__proto__=n}}class ut extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="FailedToStartTransportError",this.__proto__=n}}class pt extends Error{constructor(e){const t=new.target.prototype;super(e),this.errorType="FailedToNegotiateWithServerError",this.__proto__=t}}class ft extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.innerErrors=t,this.__proto__=n}}var gt;!function(e){e[e.Trace=0]="Trace",e[e.Debug=1]="Debug",e[e.Information=2]="Information",e[e.Warning=3]="Warning",e[e.Error=4]="Error",e[e.Critical=5]="Critical",e[e.None=6]="None"}(gt||(gt={}));class mt{constructor(){}log(e,t){}}mt.instance=new mt;const yt="0.0.0-DEV_BUILD";class wt{static isRequired(e,t){if(null==e)throw new Error(`The '${t}' argument is required.`)}static isNotEmpty(e,t){if(!e||e.match(/^\s*$/))throw new Error(`The '${t}' argument should not be empty.`)}static isIn(e,t,n){if(!(e in t))throw new Error(`Unknown ${n} value: ${e}.`)}}class vt{static get isBrowser(){return"object"==typeof window&&"object"==typeof window.document}static get isWebWorker(){return"object"==typeof self&&"importScripts"in self}static get isReactNative(){return"object"==typeof window&&void 0===window.document}static get isNode(){return!this.isBrowser&&!this.isWebWorker&&!this.isReactNative}}function bt(e,t){let n="";return _t(e)?(n=`Binary data of length ${e.byteLength}`,t&&(n+=`. Content: '${function(e){const t=new Uint8Array(e);let n="";return t.forEach((e=>{n+=`0x${e<16?"0":""}${e.toString(16)} `})),n.substr(0,n.length-1)}(e)}'`)):"string"==typeof e&&(n=`String data of length ${e.length}`,t&&(n+=`. Content: '${e}'`)),n}function _t(e){return e&&"undefined"!=typeof ArrayBuffer&&(e instanceof ArrayBuffer||e.constructor&&"ArrayBuffer"===e.constructor.name)}async function Et(e,t,n,r,o,i){const s={},[a,c]=It();s[a]=c,e.log(gt.Trace,`(${t} transport) sending data. ${bt(o,i.logMessageContent)}.`);const l=_t(o)?"arraybuffer":"text",h=await n.post(r,{content:o,headers:{...s,...i.headers},responseType:l,timeout:i.timeout,withCredentials:i.withCredentials});e.log(gt.Trace,`(${t} transport) request complete. Response status: ${h.statusCode}.`)}class St{constructor(e,t){this._subject=e,this._observer=t}dispose(){const e=this._subject.observers.indexOf(this._observer);e>-1&&this._subject.observers.splice(e,1),0===this._subject.observers.length&&this._subject.cancelCallback&&this._subject.cancelCallback().catch((e=>{}))}}class Ct{constructor(e){this._minLevel=e,this.out=console}log(e,t){if(e>=this._minLevel){const n=`[${(new Date).toISOString()}] ${gt[e]}: ${t}`;switch(e){case gt.Critical:case gt.Error:this.out.error(n);break;case gt.Warning:this.out.warn(n);break;case gt.Information:this.out.info(n);break;default:this.out.log(n)}}}}function It(){let e="X-SignalR-User-Agent";return vt.isNode&&(e="User-Agent"),[e,kt(yt,Tt(),vt.isNode?"NodeJS":"Browser",Dt())]}function kt(e,t,n,r){let o="Microsoft SignalR/";const i=e.split(".");return o+=`${i[0]}.${i[1]}`,o+=` (${e}; `,o+=t&&""!==t?`${t}; `:"Unknown OS; ",o+=`${n}`,o+=r?`; ${r}`:"; Unknown Runtime Version",o+=")",o}function Tt(){if(!vt.isNode)return"";switch(process.platform){case"win32":return"Windows NT";case"darwin":return"macOS";case"linux":return"Linux";default:return process.platform}}function Dt(){if(vt.isNode)return process.versions.node}function xt(e){return e.stack?e.stack:e.message?e.message:`${e}`}class At extends it{constructor(e){if(super(),this._logger=e,"undefined"==typeof fetch){const e=require;this._jar=new(e("tough-cookie").CookieJar),"undefined"==typeof fetch?this._fetchType=e("node-fetch"):this._fetchType=fetch,this._fetchType=e("fetch-cookie")(this._fetchType,this._jar)}else this._fetchType=fetch.bind(function(){if("undefined"!=typeof globalThis)return globalThis;if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if(void 0!==r.g)return r.g;throw new Error("could not find global")}());if("undefined"==typeof AbortController){const e=require;this._abortControllerType=e("abort-controller")}else this._abortControllerType=AbortController}async send(e){if(e.abortSignal&&e.abortSignal.aborted)throw new lt;if(!e.method)throw new Error("No method defined.");if(!e.url)throw new Error("No url defined.");const t=new this._abortControllerType;let n;e.abortSignal&&(e.abortSignal.onabort=()=>{t.abort(),n=new lt});let r,o=null;if(e.timeout){const r=e.timeout;o=setTimeout((()=>{t.abort(),this._logger.log(gt.Warning,"Timeout from HTTP request."),n=new ct}),r)}""===e.content&&(e.content=void 0),e.content&&(e.headers=e.headers||{},_t(e.content)?e.headers["Content-Type"]="application/octet-stream":e.headers["Content-Type"]="text/plain;charset=UTF-8");try{r=await this._fetchType(e.url,{body:e.content,cache:"no-cache",credentials:!0===e.withCredentials?"include":"same-origin",headers:{"X-Requested-With":"XMLHttpRequest",...e.headers},method:e.method,mode:"cors",redirect:"follow",signal:t.signal})}catch(e){if(n)throw n;throw this._logger.log(gt.Warning,`Error from HTTP request. ${e}.`),e}finally{o&&clearTimeout(o),e.abortSignal&&(e.abortSignal.onabort=null)}if(!r.ok){const e=await Rt(r,"text");throw new at(e||r.statusText,r.status)}const i=Rt(r,e.responseType),s=await i;return new ot(r.status,r.statusText,s)}getCookieString(e){return""}}function Rt(e,t){let n;switch(t){case"arraybuffer":n=e.arrayBuffer();break;case"text":default:n=e.text();break;case"blob":case"document":case"json":throw new Error(`${t} is not supported.`)}return n}class Nt extends it{constructor(e){super(),this._logger=e}send(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new lt):e.method?e.url?new Promise(((t,n)=>{const r=new XMLHttpRequest;r.open(e.method,e.url,!0),r.withCredentials=void 0===e.withCredentials||e.withCredentials,r.setRequestHeader("X-Requested-With","XMLHttpRequest"),""===e.content&&(e.content=void 0),e.content&&(_t(e.content)?r.setRequestHeader("Content-Type","application/octet-stream"):r.setRequestHeader("Content-Type","text/plain;charset=UTF-8"));const o=e.headers;o&&Object.keys(o).forEach((e=>{r.setRequestHeader(e,o[e])})),e.responseType&&(r.responseType=e.responseType),e.abortSignal&&(e.abortSignal.onabort=()=>{r.abort(),n(new lt)}),e.timeout&&(r.timeout=e.timeout),r.onload=()=>{e.abortSignal&&(e.abortSignal.onabort=null),r.status>=200&&r.status<300?t(new ot(r.status,r.statusText,r.response||r.responseText)):n(new at(r.response||r.responseText||r.statusText,r.status))},r.onerror=()=>{this._logger.log(gt.Warning,`Error from HTTP request. ${r.status}: ${r.statusText}.`),n(new at(r.statusText,r.status))},r.ontimeout=()=>{this._logger.log(gt.Warning,"Timeout from HTTP request."),n(new ct)},r.send(e.content)})):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}}class Pt extends it{constructor(e){if(super(),"undefined"!=typeof fetch)this._httpClient=new At(e);else{if("undefined"==typeof XMLHttpRequest)throw new Error("No usable HttpClient found.");this._httpClient=new Nt(e)}}send(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new lt):e.method?e.url?this._httpClient.send(e):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}getCookieString(e){return this._httpClient.getCookieString(e)}}var Ut,Lt,Mt,Bt;!function(e){e[e.None=0]="None",e[e.WebSockets=1]="WebSockets",e[e.ServerSentEvents=2]="ServerSentEvents",e[e.LongPolling=4]="LongPolling"}(Ut||(Ut={})),function(e){e[e.Text=1]="Text",e[e.Binary=2]="Binary"}(Lt||(Lt={}));class Ft{constructor(){this._isAborted=!1,this.onabort=null}abort(){this._isAborted||(this._isAborted=!0,this.onabort&&this.onabort())}get signal(){return this}get aborted(){return this._isAborted}}class $t{get pollAborted(){return this._pollAbort.aborted}constructor(e,t,n){this._httpClient=e,this._logger=t,this._pollAbort=new Ft,this._options=n,this._running=!1,this.onreceive=null,this.onclose=null}async connect(e,t){if(wt.isRequired(e,"url"),wt.isRequired(t,"transferFormat"),wt.isIn(t,Lt,"transferFormat"),this._url=e,this._logger.log(gt.Trace,"(LongPolling transport) Connecting."),t===Lt.Binary&&"undefined"!=typeof XMLHttpRequest&&"string"!=typeof(new XMLHttpRequest).responseType)throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");const[n,r]=It(),o={[n]:r,...this._options.headers},i={abortSignal:this._pollAbort.signal,headers:o,timeout:1e5,withCredentials:this._options.withCredentials};t===Lt.Binary&&(i.responseType="arraybuffer");const s=`${e}&_=${Date.now()}`;this._logger.log(gt.Trace,`(LongPolling transport) polling: ${s}.`);const a=await this._httpClient.get(s,i);200!==a.statusCode?(this._logger.log(gt.Error,`(LongPolling transport) Unexpected response code: ${a.statusCode}.`),this._closeError=new at(a.statusText||"",a.statusCode),this._running=!1):this._running=!0,this._receiving=this._poll(this._url,i)}async _poll(e,t){try{for(;this._running;)try{const n=`${e}&_=${Date.now()}`;this._logger.log(gt.Trace,`(LongPolling transport) polling: ${n}.`);const r=await this._httpClient.get(n,t);204===r.statusCode?(this._logger.log(gt.Information,"(LongPolling transport) Poll terminated by server."),this._running=!1):200!==r.statusCode?(this._logger.log(gt.Error,`(LongPolling transport) Unexpected response code: ${r.statusCode}.`),this._closeError=new at(r.statusText||"",r.statusCode),this._running=!1):r.content?(this._logger.log(gt.Trace,`(LongPolling transport) data received. ${bt(r.content,this._options.logMessageContent)}.`),this.onreceive&&this.onreceive(r.content)):this._logger.log(gt.Trace,"(LongPolling transport) Poll timed out, reissuing.")}catch(e){this._running?e instanceof ct?this._logger.log(gt.Trace,"(LongPolling transport) Poll timed out, reissuing."):(this._closeError=e,this._running=!1):this._logger.log(gt.Trace,`(LongPolling transport) Poll errored after shutdown: ${e.message}`)}}finally{this._logger.log(gt.Trace,"(LongPolling transport) Polling complete."),this.pollAborted||this._raiseOnClose()}}async send(e){return this._running?Et(this._logger,"LongPolling",this._httpClient,this._url,e,this._options):Promise.reject(new Error("Cannot send until the transport is connected"))}async stop(){this._logger.log(gt.Trace,"(LongPolling transport) Stopping polling."),this._running=!1,this._pollAbort.abort();try{await this._receiving,this._logger.log(gt.Trace,`(LongPolling transport) sending DELETE request to ${this._url}.`);const e={},[t,n]=It();e[t]=n;const r={headers:{...e,...this._options.headers},timeout:this._options.timeout,withCredentials:this._options.withCredentials};await this._httpClient.delete(this._url,r),this._logger.log(gt.Trace,"(LongPolling transport) DELETE request sent.")}finally{this._logger.log(gt.Trace,"(LongPolling transport) Stop finished."),this._raiseOnClose()}}_raiseOnClose(){if(this.onclose){let e="(LongPolling transport) Firing onclose event.";this._closeError&&(e+=" Error: "+this._closeError),this._logger.log(gt.Trace,e),this.onclose(this._closeError)}}}class Ot{constructor(e,t,n,r){this._httpClient=e,this._accessToken=t,this._logger=n,this._options=r,this.onreceive=null,this.onclose=null}async connect(e,t){return wt.isRequired(e,"url"),wt.isRequired(t,"transferFormat"),wt.isIn(t,Lt,"transferFormat"),this._logger.log(gt.Trace,"(SSE transport) Connecting."),this._url=e,this._accessToken&&(e+=(e.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(this._accessToken)}`),new Promise(((n,r)=>{let o,i=!1;if(t===Lt.Text){if(vt.isBrowser||vt.isWebWorker)o=new this._options.EventSource(e,{withCredentials:this._options.withCredentials});else{const t=this._httpClient.getCookieString(e),n={};n.Cookie=t;const[r,i]=It();n[r]=i,o=new this._options.EventSource(e,{withCredentials:this._options.withCredentials,headers:{...n,...this._options.headers}})}try{o.onmessage=e=>{if(this.onreceive)try{this._logger.log(gt.Trace,`(SSE transport) data received. ${bt(e.data,this._options.logMessageContent)}.`),this.onreceive(e.data)}catch(e){return void this._close(e)}},o.onerror=e=>{i?this._close():r(new Error("EventSource failed to connect. The connection could not be found on the server, either the connection ID is not present on the server, or a proxy is refusing/buffering the connection. If you have multiple servers check that sticky sessions are enabled."))},o.onopen=()=>{this._logger.log(gt.Information,`SSE connected to ${this._url}`),this._eventSource=o,i=!0,n()}}catch(e){return void r(e)}}else r(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"))}))}async send(e){return this._eventSource?Et(this._logger,"SSE",this._httpClient,this._url,e,this._options):Promise.reject(new Error("Cannot send until the transport is connected"))}stop(){return this._close(),Promise.resolve()}_close(e){this._eventSource&&(this._eventSource.close(),this._eventSource=void 0,this.onclose&&this.onclose(e))}}class Ht{constructor(e,t,n,r,o,i){this._logger=n,this._accessTokenFactory=t,this._logMessageContent=r,this._webSocketConstructor=o,this._httpClient=e,this.onreceive=null,this.onclose=null,this._headers=i}async connect(e,t){let n;return wt.isRequired(e,"url"),wt.isRequired(t,"transferFormat"),wt.isIn(t,Lt,"transferFormat"),this._logger.log(gt.Trace,"(WebSockets transport) Connecting."),this._accessTokenFactory&&(n=await this._accessTokenFactory()),new Promise(((r,o)=>{let i;e=e.replace(/^http/,"ws");const s=this._httpClient.getCookieString(e);let a=!1;if(vt.isReactNative){const t={},[r,o]=It();t[r]=o,n&&(t[rt.Authorization]=`Bearer ${n}`),s&&(t[rt.Cookie]=s),i=new this._webSocketConstructor(e,void 0,{headers:{...t,...this._headers}})}else n&&(e+=(e.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(n)}`);i||(i=new this._webSocketConstructor(e)),t===Lt.Binary&&(i.binaryType="arraybuffer"),i.onopen=t=>{this._logger.log(gt.Information,`WebSocket connected to ${e}.`),this._webSocket=i,a=!0,r()},i.onerror=e=>{let t=null;t="undefined"!=typeof ErrorEvent&&e instanceof ErrorEvent?e.error:"There was an error with the transport",this._logger.log(gt.Information,`(WebSockets transport) ${t}.`)},i.onmessage=e=>{if(this._logger.log(gt.Trace,`(WebSockets transport) data received. ${bt(e.data,this._logMessageContent)}.`),this.onreceive)try{this.onreceive(e.data)}catch(e){return void this._close(e)}},i.onclose=e=>{if(a)this._close(e);else{let t=null;t="undefined"!=typeof ErrorEvent&&e instanceof ErrorEvent?e.error:"WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.",o(new Error(t))}}}))}send(e){return this._webSocket&&this._webSocket.readyState===this._webSocketConstructor.OPEN?(this._logger.log(gt.Trace,`(WebSockets transport) sending data. ${bt(e,this._logMessageContent)}.`),this._webSocket.send(e),Promise.resolve()):Promise.reject("WebSocket is not in the OPEN state")}stop(){return this._webSocket&&this._close(void 0),Promise.resolve()}_close(e){this._webSocket&&(this._webSocket.onclose=()=>{},this._webSocket.onmessage=()=>{},this._webSocket.onerror=()=>{},this._webSocket.close(),this._webSocket=void 0),this._logger.log(gt.Trace,"(WebSockets transport) socket closed."),this.onclose&&(!this._isCloseEvent(e)||!1!==e.wasClean&&1e3===e.code?e instanceof Error?this.onclose(e):this.onclose():this.onclose(new Error(`WebSocket closed with status code: ${e.code} (${e.reason||"no reason given"}).`)))}_isCloseEvent(e){return e&&"boolean"==typeof e.wasClean&&"number"==typeof e.code}}class jt{constructor(e,t={}){var n;if(this._stopPromiseResolver=()=>{},this.features={},this._negotiateVersion=1,wt.isRequired(e,"url"),this._logger=void 0===(n=t.logger)?new Ct(gt.Information):null===n?mt.instance:void 0!==n.log?n:new Ct(n),this.baseUrl=this._resolveUrl(e),(t=t||{}).logMessageContent=void 0!==t.logMessageContent&&t.logMessageContent,"boolean"!=typeof t.withCredentials&&void 0!==t.withCredentials)throw new Error("withCredentials option was not a 'boolean' or 'undefined' value");t.withCredentials=void 0===t.withCredentials||t.withCredentials,t.timeout=void 0===t.timeout?1e5:t.timeout,"undefined"==typeof WebSocket||t.WebSocket||(t.WebSocket=WebSocket),"undefined"==typeof EventSource||t.EventSource||(t.EventSource=EventSource),this._httpClient=new st(t.httpClient||new Pt(this._logger),t.accessTokenFactory),this._connectionState="Disconnected",this._connectionStarted=!1,this._options=t,this.onreceive=null,this.onclose=null}async start(e){if(e=e||Lt.Binary,wt.isIn(e,Lt,"transferFormat"),this._logger.log(gt.Debug,`Starting connection with transfer format '${Lt[e]}'.`),"Disconnected"!==this._connectionState)return Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state."));if(this._connectionState="Connecting",this._startInternalPromise=this._startInternal(e),await this._startInternalPromise,"Disconnecting"===this._connectionState){const e="Failed to start the HttpConnection before stop() was called.";return this._logger.log(gt.Error,e),await this._stopPromise,Promise.reject(new lt(e))}if("Connected"!==this._connectionState){const e="HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!";return this._logger.log(gt.Error,e),Promise.reject(new lt(e))}this._connectionStarted=!0}send(e){return"Connected"!==this._connectionState?Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State.")):(this._sendQueue||(this._sendQueue=new Wt(this.transport)),this._sendQueue.send(e))}async stop(e){return"Disconnected"===this._connectionState?(this._logger.log(gt.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnected state.`),Promise.resolve()):"Disconnecting"===this._connectionState?(this._logger.log(gt.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnecting state.`),this._stopPromise):(this._connectionState="Disconnecting",this._stopPromise=new Promise((e=>{this._stopPromiseResolver=e})),await this._stopInternal(e),void await this._stopPromise)}async _stopInternal(e){this._stopError=e;try{await this._startInternalPromise}catch(e){}if(this.transport){try{await this.transport.stop()}catch(e){this._logger.log(gt.Error,`HttpConnection.transport.stop() threw error '${e}'.`),this._stopConnection()}this.transport=void 0}else this._logger.log(gt.Debug,"HttpConnection.transport is undefined in HttpConnection.stop() because start() failed.")}async _startInternal(e){let t=this.baseUrl;this._accessTokenFactory=this._options.accessTokenFactory,this._httpClient._accessTokenFactory=this._accessTokenFactory;try{if(this._options.skipNegotiation){if(this._options.transport!==Ut.WebSockets)throw new Error("Negotiation can only be skipped when using the WebSocket transport directly.");this.transport=this._constructTransport(Ut.WebSockets),await this._startTransport(t,e)}else{let n=null,r=0;do{if(n=await this._getNegotiationResponse(t),"Disconnecting"===this._connectionState||"Disconnected"===this._connectionState)throw new lt("The connection was stopped during negotiation.");if(n.error)throw new Error(n.error);if(n.ProtocolVersion)throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.");if(n.url&&(t=n.url),n.accessToken){const e=n.accessToken;this._accessTokenFactory=()=>e,this._httpClient._accessToken=e,this._httpClient._accessTokenFactory=void 0}r++}while(n.url&&r<100);if(100===r&&n.url)throw new Error("Negotiate redirection limit exceeded.");await this._createTransport(t,this._options.transport,n,e)}this.transport instanceof $t&&(this.features.inherentKeepAlive=!0),"Connecting"===this._connectionState&&(this._logger.log(gt.Debug,"The HttpConnection connected successfully."),this._connectionState="Connected")}catch(e){return this._logger.log(gt.Error,"Failed to start the connection: "+e),this._connectionState="Disconnected",this.transport=void 0,this._stopPromiseResolver(),Promise.reject(e)}}async _getNegotiationResponse(e){const t={},[n,r]=It();t[n]=r;const o=this._resolveNegotiateUrl(e);this._logger.log(gt.Debug,`Sending negotiation request: ${o}.`);try{const e=await this._httpClient.post(o,{content:"",headers:{...t,...this._options.headers},timeout:this._options.timeout,withCredentials:this._options.withCredentials});if(200!==e.statusCode)return Promise.reject(new Error(`Unexpected status code returned from negotiate '${e.statusCode}'`));const n=JSON.parse(e.content);return(!n.negotiateVersion||n.negotiateVersion<1)&&(n.connectionToken=n.connectionId),n}catch(e){let t="Failed to complete negotiation with the server: "+e;return e instanceof at&&404===e.statusCode&&(t+=" Either this is not a SignalR endpoint or there is a proxy blocking the connection."),this._logger.log(gt.Error,t),Promise.reject(new pt(t))}}_createConnectUrl(e,t){return t?e+(-1===e.indexOf("?")?"?":"&")+`id=${t}`:e}async _createTransport(e,t,n,r){let o=this._createConnectUrl(e,n.connectionToken);if(this._isITransport(t))return this._logger.log(gt.Debug,"Connection was provided an instance of ITransport, using that directly."),this.transport=t,await this._startTransport(o,r),void(this.connectionId=n.connectionId);const i=[],s=n.availableTransports||[];let a=n;for(const n of s){const s=this._resolveTransportOrError(n,t,r);if(s instanceof Error)i.push(`${n.transport} failed:`),i.push(s);else if(this._isITransport(s)){if(this.transport=s,!a){try{a=await this._getNegotiationResponse(e)}catch(e){return Promise.reject(e)}o=this._createConnectUrl(e,a.connectionToken)}try{return await this._startTransport(o,r),void(this.connectionId=a.connectionId)}catch(e){if(this._logger.log(gt.Error,`Failed to start the transport '${n.transport}': ${e}`),a=void 0,i.push(new ut(`${n.transport} failed: ${e}`,Ut[n.transport])),"Connecting"!==this._connectionState){const e="Failed to select transport before stop() was called.";return this._logger.log(gt.Debug,e),Promise.reject(new lt(e))}}}}return i.length>0?Promise.reject(new ft(`Unable to connect to the server with any of the available transports. ${i.join(" ")}`,i)):Promise.reject(new Error("None of the transports supported by the client are supported by the server."))}_constructTransport(e){switch(e){case Ut.WebSockets:if(!this._options.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new Ht(this._httpClient,this._accessTokenFactory,this._logger,this._options.logMessageContent,this._options.WebSocket,this._options.headers||{});case Ut.ServerSentEvents:if(!this._options.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new Ot(this._httpClient,this._httpClient._accessToken,this._logger,this._options);case Ut.LongPolling:return new $t(this._httpClient,this._logger,this._options);default:throw new Error(`Unknown transport: ${e}.`)}}_startTransport(e,t){return this.transport.onreceive=this.onreceive,this.transport.onclose=e=>this._stopConnection(e),this.transport.connect(e,t)}_resolveTransportOrError(e,t,n){const r=Ut[e.transport];if(null==r)return this._logger.log(gt.Debug,`Skipping transport '${e.transport}' because it is not supported by this client.`),new Error(`Skipping transport '${e.transport}' because it is not supported by this client.`);if(!function(e,t){return!e||0!=(t&e)}(t,r))return this._logger.log(gt.Debug,`Skipping transport '${Ut[r]}' because it was disabled by the client.`),new dt(`'${Ut[r]}' is disabled by the client.`,r);if(!(e.transferFormats.map((e=>Lt[e])).indexOf(n)>=0))return this._logger.log(gt.Debug,`Skipping transport '${Ut[r]}' because it does not support the requested transfer format '${Lt[n]}'.`),new Error(`'${Ut[r]}' does not support ${Lt[n]}.`);if(r===Ut.WebSockets&&!this._options.WebSocket||r===Ut.ServerSentEvents&&!this._options.EventSource)return this._logger.log(gt.Debug,`Skipping transport '${Ut[r]}' because it is not supported in your environment.'`),new ht(`'${Ut[r]}' is not supported in your environment.`,r);this._logger.log(gt.Debug,`Selecting transport '${Ut[r]}'.`);try{return this._constructTransport(r)}catch(e){return e}}_isITransport(e){return e&&"object"==typeof e&&"connect"in e}_stopConnection(e){if(this._logger.log(gt.Debug,`HttpConnection.stopConnection(${e}) called while in state ${this._connectionState}.`),this.transport=void 0,e=this._stopError||e,this._stopError=void 0,"Disconnected"!==this._connectionState){if("Connecting"===this._connectionState)throw this._logger.log(gt.Warning,`Call to HttpConnection.stopConnection(${e}) was ignored because the connection is still in the connecting state.`),new Error(`HttpConnection.stopConnection(${e}) was called while the connection is still in the connecting state.`);if("Disconnecting"===this._connectionState&&this._stopPromiseResolver(),e?this._logger.log(gt.Error,`Connection disconnected with error '${e}'.`):this._logger.log(gt.Information,"Connection disconnected."),this._sendQueue&&(this._sendQueue.stop().catch((e=>{this._logger.log(gt.Error,`TransportSendQueue.stop() threw error '${e}'.`)})),this._sendQueue=void 0),this.connectionId=void 0,this._connectionState="Disconnected",this._connectionStarted){this._connectionStarted=!1;try{this.onclose&&this.onclose(e)}catch(t){this._logger.log(gt.Error,`HttpConnection.onclose(${e}) threw error '${t}'.`)}}}else this._logger.log(gt.Debug,`Call to HttpConnection.stopConnection(${e}) was ignored because the connection is already in the disconnected state.`)}_resolveUrl(e){if(0===e.lastIndexOf("https://",0)||0===e.lastIndexOf("http://",0))return e;if(!vt.isBrowser)throw new Error(`Cannot resolve '${e}'.`);const t=window.document.createElement("a");return t.href=e,this._logger.log(gt.Information,`Normalizing '${e}' to '${t.href}'.`),t.href}_resolveNegotiateUrl(e){const t=e.indexOf("?");let n=e.substring(0,-1===t?e.length:t);return"/"!==n[n.length-1]&&(n+="/"),n+="negotiate",n+=-1===t?"":e.substring(t),-1===n.indexOf("negotiateVersion")&&(n+=-1===t?"?":"&",n+="negotiateVersion="+this._negotiateVersion),n}}class Wt{constructor(e){this._transport=e,this._buffer=[],this._executing=!0,this._sendBufferedData=new zt,this._transportResult=new zt,this._sendLoopPromise=this._sendLoop()}send(e){return this._bufferData(e),this._transportResult||(this._transportResult=new zt),this._transportResult.promise}stop(){return this._executing=!1,this._sendBufferedData.resolve(),this._sendLoopPromise}_bufferData(e){if(this._buffer.length&&typeof this._buffer[0]!=typeof e)throw new Error(`Expected data to be of type ${typeof this._buffer} but was of type ${typeof e}`);this._buffer.push(e),this._sendBufferedData.resolve()}async _sendLoop(){for(;;){if(await this._sendBufferedData.promise,!this._executing){this._transportResult&&this._transportResult.reject("Connection stopped.");break}this._sendBufferedData=new zt;const e=this._transportResult;this._transportResult=void 0;const t="string"==typeof this._buffer[0]?this._buffer.join(""):Wt._concatBuffers(this._buffer);this._buffer.length=0;try{await this._transport.send(t),e.resolve()}catch(t){e.reject(t)}}}static _concatBuffers(e){const t=e.map((e=>e.byteLength)).reduce(((e,t)=>e+t)),n=new Uint8Array(t);let r=0;for(const t of e)n.set(new Uint8Array(t),r),r+=t.byteLength;return n.buffer}}class zt{constructor(){this.promise=new Promise(((e,t)=>[this._resolver,this._rejecter]=[e,t]))}resolve(){this._resolver()}reject(e){this._rejecter(e)}}class Jt{static write(e){return`${e}${Jt.RecordSeparator}`}static parse(e){if(e[e.length-1]!==Jt.RecordSeparator)throw new Error("Message is incomplete.");const t=e.split(Jt.RecordSeparator);return t.pop(),t}}Jt.RecordSeparatorCode=30,Jt.RecordSeparator=String.fromCharCode(Jt.RecordSeparatorCode);class qt{writeHandshakeRequest(e){return Jt.write(JSON.stringify(e))}parseHandshakeResponse(e){let t,n;if(_t(e)){const r=new Uint8Array(e),o=r.indexOf(Jt.RecordSeparatorCode);if(-1===o)throw new Error("Message is incomplete.");const i=o+1;t=String.fromCharCode.apply(null,Array.prototype.slice.call(r.slice(0,i))),n=r.byteLength>i?r.slice(i).buffer:null}else{const r=e,o=r.indexOf(Jt.RecordSeparator);if(-1===o)throw new Error("Message is incomplete.");const i=o+1;t=r.substring(0,i),n=r.length>i?r.substring(i):null}const r=Jt.parse(t),o=JSON.parse(r[0]);if(o.type)throw new Error("Expected a handshake response from the server.");return[n,o]}}!function(e){e[e.Invocation=1]="Invocation",e[e.StreamItem=2]="StreamItem",e[e.Completion=3]="Completion",e[e.StreamInvocation=4]="StreamInvocation",e[e.CancelInvocation=5]="CancelInvocation",e[e.Ping=6]="Ping",e[e.Close=7]="Close"}(Mt||(Mt={}));class Vt{constructor(){this.observers=[]}next(e){for(const t of this.observers)t.next(e)}error(e){for(const t of this.observers)t.error&&t.error(e)}complete(){for(const e of this.observers)e.complete&&e.complete()}subscribe(e){return this.observers.push(e),new St(this,e)}}!function(e){e.Disconnected="Disconnected",e.Connecting="Connecting",e.Connected="Connected",e.Disconnecting="Disconnecting",e.Reconnecting="Reconnecting"}(Bt||(Bt={}));class Kt{static create(e,t,n,r,o,i){return new Kt(e,t,n,r,o,i)}constructor(e,t,n,r,o,i){this._nextKeepAlive=0,this._freezeEventListener=()=>{this._logger.log(gt.Warning,"The page is being frozen, this will likely lead to the connection being closed and messages being lost. For more information see the docs at https://learn.microsoft.com/aspnet/core/signalr/javascript-client#bsleep")},wt.isRequired(e,"connection"),wt.isRequired(t,"logger"),wt.isRequired(n,"protocol"),this.serverTimeoutInMilliseconds=null!=o?o:3e4,this.keepAliveIntervalInMilliseconds=null!=i?i:15e3,this._logger=t,this._protocol=n,this.connection=e,this._reconnectPolicy=r,this._handshakeProtocol=new qt,this.connection.onreceive=e=>this._processIncomingData(e),this.connection.onclose=e=>this._connectionClosed(e),this._callbacks={},this._methods={},this._closedCallbacks=[],this._reconnectingCallbacks=[],this._reconnectedCallbacks=[],this._invocationId=0,this._receivedHandshakeResponse=!1,this._connectionState=Bt.Disconnected,this._connectionStarted=!1,this._cachedPingMessage=this._protocol.writeMessage({type:Mt.Ping})}get state(){return this._connectionState}get connectionId(){return this.connection&&this.connection.connectionId||null}get baseUrl(){return this.connection.baseUrl||""}set baseUrl(e){if(this._connectionState!==Bt.Disconnected&&this._connectionState!==Bt.Reconnecting)throw new Error("The HubConnection must be in the Disconnected or Reconnecting state to change the url.");if(!e)throw new Error("The HubConnection url must be a valid url.");this.connection.baseUrl=e}start(){return this._startPromise=this._startWithStateTransitions(),this._startPromise}async _startWithStateTransitions(){if(this._connectionState!==Bt.Disconnected)return Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state."));this._connectionState=Bt.Connecting,this._logger.log(gt.Debug,"Starting HubConnection.");try{await this._startInternal(),vt.isBrowser&&window.document.addEventListener("freeze",this._freezeEventListener),this._connectionState=Bt.Connected,this._connectionStarted=!0,this._logger.log(gt.Debug,"HubConnection connected successfully.")}catch(e){return this._connectionState=Bt.Disconnected,this._logger.log(gt.Debug,`HubConnection failed to start successfully because of error '${e}'.`),Promise.reject(e)}}async _startInternal(){this._stopDuringStartError=void 0,this._receivedHandshakeResponse=!1;const e=new Promise(((e,t)=>{this._handshakeResolver=e,this._handshakeRejecter=t}));await this.connection.start(this._protocol.transferFormat);try{const t={protocol:this._protocol.name,version:this._protocol.version};if(this._logger.log(gt.Debug,"Sending handshake request."),await this._sendMessage(this._handshakeProtocol.writeHandshakeRequest(t)),this._logger.log(gt.Information,`Using HubProtocol '${this._protocol.name}'.`),this._cleanupTimeout(),this._resetTimeoutPeriod(),this._resetKeepAliveInterval(),await e,this._stopDuringStartError)throw this._stopDuringStartError;this.connection.features.inherentKeepAlive||await this._sendMessage(this._cachedPingMessage)}catch(e){throw this._logger.log(gt.Debug,`Hub handshake failed with error '${e}' during start(). Stopping HubConnection.`),this._cleanupTimeout(),this._cleanupPingTimer(),await this.connection.stop(e),e}}async stop(){const e=this._startPromise;this._stopPromise=this._stopInternal(),await this._stopPromise;try{await e}catch(e){}}_stopInternal(e){return this._connectionState===Bt.Disconnected?(this._logger.log(gt.Debug,`Call to HubConnection.stop(${e}) ignored because it is already in the disconnected state.`),Promise.resolve()):this._connectionState===Bt.Disconnecting?(this._logger.log(gt.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnecting state.`),this._stopPromise):(this._connectionState=Bt.Disconnecting,this._logger.log(gt.Debug,"Stopping HubConnection."),this._reconnectDelayHandle?(this._logger.log(gt.Debug,"Connection stopped during reconnect delay. Done reconnecting."),clearTimeout(this._reconnectDelayHandle),this._reconnectDelayHandle=void 0,this._completeClose(),Promise.resolve()):(this._cleanupTimeout(),this._cleanupPingTimer(),this._stopDuringStartError=e||new lt("The connection was stopped before the hub handshake could complete."),this.connection.stop(e)))}stream(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._createStreamInvocation(e,t,r);let i;const s=new Vt;return s.cancelCallback=()=>{const e=this._createCancelInvocation(o.invocationId);return delete this._callbacks[o.invocationId],i.then((()=>this._sendWithProtocol(e)))},this._callbacks[o.invocationId]=(e,t)=>{t?s.error(t):e&&(e.type===Mt.Completion?e.error?s.error(new Error(e.error)):s.complete():s.next(e.item))},i=this._sendWithProtocol(o).catch((e=>{s.error(e),delete this._callbacks[o.invocationId]})),this._launchStreams(n,i),s}_sendMessage(e){return this._resetKeepAliveInterval(),this.connection.send(e)}_sendWithProtocol(e){return this._sendMessage(this._protocol.writeMessage(e))}send(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._sendWithProtocol(this._createInvocation(e,t,!0,r));return this._launchStreams(n,o),o}invoke(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._createInvocation(e,t,!1,r);return new Promise(((e,t)=>{this._callbacks[o.invocationId]=(n,r)=>{r?t(r):n&&(n.type===Mt.Completion?n.error?t(new Error(n.error)):e(n.result):t(new Error(`Unexpected message type: ${n.type}`)))};const r=this._sendWithProtocol(o).catch((e=>{t(e),delete this._callbacks[o.invocationId]}));this._launchStreams(n,r)}))}on(e,t){e&&t&&(e=e.toLowerCase(),this._methods[e]||(this._methods[e]=[]),-1===this._methods[e].indexOf(t)&&this._methods[e].push(t))}off(e,t){if(!e)return;e=e.toLowerCase();const n=this._methods[e];if(n)if(t){const r=n.indexOf(t);-1!==r&&(n.splice(r,1),0===n.length&&delete this._methods[e])}else delete this._methods[e]}onclose(e){e&&this._closedCallbacks.push(e)}onreconnecting(e){e&&this._reconnectingCallbacks.push(e)}onreconnected(e){e&&this._reconnectedCallbacks.push(e)}_processIncomingData(e){if(this._cleanupTimeout(),this._receivedHandshakeResponse||(e=this._processHandshakeResponse(e),this._receivedHandshakeResponse=!0),e){const t=this._protocol.parseMessages(e,this._logger);for(const e of t)switch(e.type){case Mt.Invocation:this._invokeClientMethod(e);break;case Mt.StreamItem:case Mt.Completion:{const t=this._callbacks[e.invocationId];if(t){e.type===Mt.Completion&&delete this._callbacks[e.invocationId];try{t(e)}catch(e){this._logger.log(gt.Error,`Stream callback threw error: ${xt(e)}`)}}break}case Mt.Ping:break;case Mt.Close:{this._logger.log(gt.Information,"Close message received from server.");const t=e.error?new Error("Server returned an error on close: "+e.error):void 0;!0===e.allowReconnect?this.connection.stop(t):this._stopPromise=this._stopInternal(t);break}default:this._logger.log(gt.Warning,`Invalid message type: ${e.type}.`)}}this._resetTimeoutPeriod()}_processHandshakeResponse(e){let t,n;try{[n,t]=this._handshakeProtocol.parseHandshakeResponse(e)}catch(e){const t="Error parsing handshake response: "+e;this._logger.log(gt.Error,t);const n=new Error(t);throw this._handshakeRejecter(n),n}if(t.error){const e="Server returned handshake error: "+t.error;this._logger.log(gt.Error,e);const n=new Error(e);throw this._handshakeRejecter(n),n}return this._logger.log(gt.Debug,"Server handshake complete."),this._handshakeResolver(),n}_resetKeepAliveInterval(){this.connection.features.inherentKeepAlive||(this._nextKeepAlive=(new Date).getTime()+this.keepAliveIntervalInMilliseconds,this._cleanupPingTimer())}_resetTimeoutPeriod(){if(!(this.connection.features&&this.connection.features.inherentKeepAlive||(this._timeoutHandle=setTimeout((()=>this.serverTimeout()),this.serverTimeoutInMilliseconds),void 0!==this._pingServerHandle))){let e=this._nextKeepAlive-(new Date).getTime();e<0&&(e=0),this._pingServerHandle=setTimeout((async()=>{if(this._connectionState===Bt.Connected)try{await this._sendMessage(this._cachedPingMessage)}catch{this._cleanupPingTimer()}}),e)}}serverTimeout(){this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server."))}async _invokeClientMethod(e){const t=e.target.toLowerCase(),n=this._methods[t];if(!n)return this._logger.log(gt.Warning,`No client method with the name '${t}' found.`),void(e.invocationId&&(this._logger.log(gt.Warning,`No result given for '${t}' method and invocation ID '${e.invocationId}'.`),await this._sendWithProtocol(this._createCompletionMessage(e.invocationId,"Client didn't provide a result.",null))));const r=n.slice(),o=!!e.invocationId;let i,s,a;for(const n of r)try{const r=i;i=await n.apply(this,e.arguments),o&&i&&r&&(this._logger.log(gt.Error,`Multiple results provided for '${t}'. Sending error to server.`),a=this._createCompletionMessage(e.invocationId,"Client provided multiple results.",null)),s=void 0}catch(e){s=e,this._logger.log(gt.Error,`A callback for the method '${t}' threw error '${e}'.`)}a?await this._sendWithProtocol(a):o?(s?a=this._createCompletionMessage(e.invocationId,`${s}`,null):void 0!==i?a=this._createCompletionMessage(e.invocationId,null,i):(this._logger.log(gt.Warning,`No result given for '${t}' method and invocation ID '${e.invocationId}'.`),a=this._createCompletionMessage(e.invocationId,"Client didn't provide a result.",null)),await this._sendWithProtocol(a)):i&&this._logger.log(gt.Error,`Result given for '${t}' method but server is not expecting a result.`)}_connectionClosed(e){this._logger.log(gt.Debug,`HubConnection.connectionClosed(${e}) called while in state ${this._connectionState}.`),this._stopDuringStartError=this._stopDuringStartError||e||new lt("The underlying connection was closed before the hub handshake could complete."),this._handshakeResolver&&this._handshakeResolver(),this._cancelCallbacksWithError(e||new Error("Invocation canceled due to the underlying connection being closed.")),this._cleanupTimeout(),this._cleanupPingTimer(),this._connectionState===Bt.Disconnecting?this._completeClose(e):this._connectionState===Bt.Connected&&this._reconnectPolicy?this._reconnect(e):this._connectionState===Bt.Connected&&this._completeClose(e)}_completeClose(e){if(this._connectionStarted){this._connectionState=Bt.Disconnected,this._connectionStarted=!1,vt.isBrowser&&window.document.removeEventListener("freeze",this._freezeEventListener);try{this._closedCallbacks.forEach((t=>t.apply(this,[e])))}catch(t){this._logger.log(gt.Error,`An onclose callback called with error '${e}' threw error '${t}'.`)}}}async _reconnect(e){const t=Date.now();let n=0,r=void 0!==e?e:new Error("Attempting to reconnect due to a unknown error."),o=this._getNextRetryDelay(n++,0,r);if(null===o)return this._logger.log(gt.Debug,"Connection not reconnecting because the IRetryPolicy returned null on the first reconnect attempt."),void this._completeClose(e);if(this._connectionState=Bt.Reconnecting,e?this._logger.log(gt.Information,`Connection reconnecting because of error '${e}'.`):this._logger.log(gt.Information,"Connection reconnecting."),0!==this._reconnectingCallbacks.length){try{this._reconnectingCallbacks.forEach((t=>t.apply(this,[e])))}catch(t){this._logger.log(gt.Error,`An onreconnecting callback called with error '${e}' threw error '${t}'.`)}if(this._connectionState!==Bt.Reconnecting)return void this._logger.log(gt.Debug,"Connection left the reconnecting state in onreconnecting callback. Done reconnecting.")}for(;null!==o;){if(this._logger.log(gt.Information,`Reconnect attempt number ${n} will start in ${o} ms.`),await new Promise((e=>{this._reconnectDelayHandle=setTimeout(e,o)})),this._reconnectDelayHandle=void 0,this._connectionState!==Bt.Reconnecting)return void this._logger.log(gt.Debug,"Connection left the reconnecting state during reconnect delay. Done reconnecting.");try{if(await this._startInternal(),this._connectionState=Bt.Connected,this._logger.log(gt.Information,"HubConnection reconnected successfully."),0!==this._reconnectedCallbacks.length)try{this._reconnectedCallbacks.forEach((e=>e.apply(this,[this.connection.connectionId])))}catch(e){this._logger.log(gt.Error,`An onreconnected callback called with connectionId '${this.connection.connectionId}; threw error '${e}'.`)}return}catch(e){if(this._logger.log(gt.Information,`Reconnect attempt failed because of error '${e}'.`),this._connectionState!==Bt.Reconnecting)return this._logger.log(gt.Debug,`Connection moved to the '${this._connectionState}' from the reconnecting state during reconnect attempt. Done reconnecting.`),void(this._connectionState===Bt.Disconnecting&&this._completeClose());r=e instanceof Error?e:new Error(e.toString()),o=this._getNextRetryDelay(n++,Date.now()-t,r)}}this._logger.log(gt.Information,`Reconnect retries have been exhausted after ${Date.now()-t} ms and ${n} failed attempts. Connection disconnecting.`),this._completeClose()}_getNextRetryDelay(e,t,n){try{return this._reconnectPolicy.nextRetryDelayInMilliseconds({elapsedMilliseconds:t,previousRetryCount:e,retryReason:n})}catch(n){return this._logger.log(gt.Error,`IRetryPolicy.nextRetryDelayInMilliseconds(${e}, ${t}) threw error '${n}'.`),null}}_cancelCallbacksWithError(e){const t=this._callbacks;this._callbacks={},Object.keys(t).forEach((n=>{const r=t[n];try{r(null,e)}catch(t){this._logger.log(gt.Error,`Stream 'error' callback called with '${e}' threw error: ${xt(t)}`)}}))}_cleanupPingTimer(){this._pingServerHandle&&(clearTimeout(this._pingServerHandle),this._pingServerHandle=void 0)}_cleanupTimeout(){this._timeoutHandle&&clearTimeout(this._timeoutHandle)}_createInvocation(e,t,n,r){if(n)return 0!==r.length?{arguments:t,streamIds:r,target:e,type:Mt.Invocation}:{arguments:t,target:e,type:Mt.Invocation};{const n=this._invocationId;return this._invocationId++,0!==r.length?{arguments:t,invocationId:n.toString(),streamIds:r,target:e,type:Mt.Invocation}:{arguments:t,invocationId:n.toString(),target:e,type:Mt.Invocation}}}_launchStreams(e,t){if(0!==e.length){t||(t=Promise.resolve());for(const n in e)e[n].subscribe({complete:()=>{t=t.then((()=>this._sendWithProtocol(this._createCompletionMessage(n))))},error:e=>{let r;r=e instanceof Error?e.message:e&&e.toString?e.toString():"Unknown error",t=t.then((()=>this._sendWithProtocol(this._createCompletionMessage(n,r))))},next:e=>{t=t.then((()=>this._sendWithProtocol(this._createStreamItemMessage(n,e))))}})}}_replaceStreamingParams(e){const t=[],n=[];for(let r=0;r=55296&&o<=56319&&r65535&&(h-=65536,i.push(h>>>10&1023|55296),h=56320|1023&h),i.push(h)}else i.push(a);i.length>=hn&&(s+=String.fromCharCode.apply(String,i),i.length=0)}return i.length>0&&(s+=String.fromCharCode.apply(String,i)),s}var un,pn=on?new TextDecoder:null,fn=on?"undefined"!=typeof process&&"force"!==(null===(en=null===process||void 0===process?void 0:process.env)||void 0===en?void 0:en.TEXT_DECODER)?200:0:tn,gn=function(e,t){this.type=e,this.data=t},mn=(un=function(e,t){return un=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},un(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}un(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),yn=function(e){function t(n){var r=e.call(this,n)||this,o=Object.create(t.prototype);return Object.setPrototypeOf(r,o),Object.defineProperty(r,"name",{configurable:!0,enumerable:!1,value:t.name}),r}return mn(t,e),t}(Error),wn={type:-1,encode:function(e){var t,n,r,o;return e instanceof Date?function(e){var t,n=e.sec,r=e.nsec;if(n>=0&&r>=0&&n<=17179869183){if(0===r&&n<=4294967295){var o=new Uint8Array(4);return(t=new DataView(o.buffer)).setUint32(0,n),o}var i=n/4294967296,s=4294967295&n;return o=new Uint8Array(8),(t=new DataView(o.buffer)).setUint32(0,r<<2|3&i),t.setUint32(4,s),o}return o=new Uint8Array(12),(t=new DataView(o.buffer)).setUint32(0,r),nn(t,4,n),o}((r=1e6*((t=e.getTime())-1e3*(n=Math.floor(t/1e3))),{sec:n+(o=Math.floor(r/1e9)),nsec:r-1e9*o})):null},decode:function(e){var t=function(e){var t=new DataView(e.buffer,e.byteOffset,e.byteLength);switch(e.byteLength){case 4:return{sec:t.getUint32(0),nsec:0};case 8:var n=t.getUint32(0);return{sec:4294967296*(3&n)+t.getUint32(4),nsec:n>>>2};case 12:return{sec:rn(t,4),nsec:t.getUint32(0)};default:throw new yn("Unrecognized data size for timestamp (expected 4, 8, or 12): ".concat(e.length))}}(e);return new Date(1e3*t.sec+t.nsec/1e6)}},vn=function(){function e(){this.builtInEncoders=[],this.builtInDecoders=[],this.encoders=[],this.decoders=[],this.register(wn)}return e.prototype.register=function(e){var t=e.type,n=e.encode,r=e.decode;if(t>=0)this.encoders[t]=n,this.decoders[t]=r;else{var o=1+t;this.builtInEncoders[o]=n,this.builtInDecoders[o]=r}},e.prototype.tryToEncode=function(e,t){for(var n=0;nthis.maxDepth)throw new Error("Too deep objects in depth ".concat(t));null==e?this.encodeNil():"boolean"==typeof e?this.encodeBoolean(e):"number"==typeof e?this.encodeNumber(e):"string"==typeof e?this.encodeString(e):this.encodeObject(e,t)},e.prototype.ensureBufferSizeToWrite=function(e){var t=this.pos+e;this.view.byteLength=0?e<128?this.writeU8(e):e<256?(this.writeU8(204),this.writeU8(e)):e<65536?(this.writeU8(205),this.writeU16(e)):e<4294967296?(this.writeU8(206),this.writeU32(e)):(this.writeU8(207),this.writeU64(e)):e>=-32?this.writeU8(224|e+32):e>=-128?(this.writeU8(208),this.writeI8(e)):e>=-32768?(this.writeU8(209),this.writeI16(e)):e>=-2147483648?(this.writeU8(210),this.writeI32(e)):(this.writeU8(211),this.writeI64(e)):this.forceFloat32?(this.writeU8(202),this.writeF32(e)):(this.writeU8(203),this.writeF64(e))},e.prototype.writeStringHeader=function(e){if(e<32)this.writeU8(160+e);else if(e<256)this.writeU8(217),this.writeU8(e);else if(e<65536)this.writeU8(218),this.writeU16(e);else{if(!(e<4294967296))throw new Error("Too long string: ".concat(e," bytes in UTF-8"));this.writeU8(219),this.writeU32(e)}},e.prototype.encodeString=function(e){if(e.length>cn){var t=sn(e);this.ensureBufferSizeToWrite(5+t),this.writeStringHeader(t),ln(e,this.bytes,this.pos),this.pos+=t}else t=sn(e),this.ensureBufferSizeToWrite(5+t),this.writeStringHeader(t),function(e,t,n){for(var r=e.length,o=n,i=0;i>6&31|192;else{if(s>=55296&&s<=56319&&i>12&15|224,t[o++]=s>>6&63|128):(t[o++]=s>>18&7|240,t[o++]=s>>12&63|128,t[o++]=s>>6&63|128)}t[o++]=63&s|128}else t[o++]=s}}(e,this.bytes,this.pos),this.pos+=t},e.prototype.encodeObject=function(e,t){var n=this.extensionCodec.tryToEncode(e,this.context);if(null!=n)this.encodeExtension(n);else if(Array.isArray(e))this.encodeArray(e,t);else if(ArrayBuffer.isView(e))this.encodeBinary(e);else{if("object"!=typeof e)throw new Error("Unrecognized object: ".concat(Object.prototype.toString.apply(e)));this.encodeMap(e,t)}},e.prototype.encodeBinary=function(e){var t=e.byteLength;if(t<256)this.writeU8(196),this.writeU8(t);else if(t<65536)this.writeU8(197),this.writeU16(t);else{if(!(t<4294967296))throw new Error("Too large binary: ".concat(t));this.writeU8(198),this.writeU32(t)}var n=bn(e);this.writeU8a(n)},e.prototype.encodeArray=function(e,t){var n=e.length;if(n<16)this.writeU8(144+n);else if(n<65536)this.writeU8(220),this.writeU16(n);else{if(!(n<4294967296))throw new Error("Too large array: ".concat(n));this.writeU8(221),this.writeU32(n)}for(var r=0,o=e;r0&&e<=this.maxKeyLength},e.prototype.find=function(e,t,n){e:for(var r=0,o=this.caches[n-1];r=this.maxLengthPerKey?n[Math.random()*n.length|0]=r:n.push(r)},e.prototype.decode=function(e,t,n){var r=this.find(e,t,n);if(null!=r)return this.hit++,r;this.miss++;var o=dn(e,t,n),i=Uint8Array.prototype.slice.call(e,t,t+n);return this.store(i,o),o},e}(),Dn=function(e,t){var n,r,o,i,s={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function a(i){return function(a){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return s.label++,{value:i[1],done:!1};case 5:s.label++,r=i[1],i=[0];continue;case 7:i=s.ops.pop(),s.trys.pop();continue;default:if(!((o=(o=s.trys).length>0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]1||a(e,t)}))})}function a(e,t){try{(n=o[e](t)).value instanceof An?Promise.resolve(n.value.v).then(c,l):h(i[0][2],n)}catch(e){h(i[0][3],e)}var n}function c(e){a("next",e)}function l(e){a("throw",e)}function h(e,t){e(t),i.shift(),i.length&&a(i[0][0],i[0][1])}},Nn=-1,Pn=new DataView(new ArrayBuffer(0)),Un=new Uint8Array(Pn.buffer),Ln=function(){try{Pn.getInt8(0)}catch(e){return e.constructor}throw new Error("never reached")}(),Mn=new Ln("Insufficient data"),Bn=new Tn,Fn=function(){function e(e,t,n,r,o,i,s,a){void 0===e&&(e=vn.defaultCodec),void 0===t&&(t=void 0),void 0===n&&(n=tn),void 0===r&&(r=tn),void 0===o&&(o=tn),void 0===i&&(i=tn),void 0===s&&(s=tn),void 0===a&&(a=Bn),this.extensionCodec=e,this.context=t,this.maxStrLength=n,this.maxBinLength=r,this.maxArrayLength=o,this.maxMapLength=i,this.maxExtLength=s,this.keyDecoder=a,this.totalPos=0,this.pos=0,this.view=Pn,this.bytes=Un,this.headByte=Nn,this.stack=[]}return e.prototype.reinitializeState=function(){this.totalPos=0,this.headByte=Nn,this.stack.length=0},e.prototype.setBuffer=function(e){this.bytes=bn(e),this.view=function(e){if(e instanceof ArrayBuffer)return new DataView(e);var t=bn(e);return new DataView(t.buffer,t.byteOffset,t.byteLength)}(this.bytes),this.pos=0},e.prototype.appendBuffer=function(e){if(this.headByte!==Nn||this.hasRemaining(1)){var t=this.bytes.subarray(this.pos),n=bn(e),r=new Uint8Array(t.length+n.length);r.set(t),r.set(n,t.length),this.setBuffer(r)}else this.setBuffer(e)},e.prototype.hasRemaining=function(e){return this.view.byteLength-this.pos>=e},e.prototype.createExtraByteError=function(e){var t=this.view,n=this.pos;return new RangeError("Extra ".concat(t.byteLength-n," of ").concat(t.byteLength," byte(s) found at buffer[").concat(e,"]"))},e.prototype.decode=function(e){this.reinitializeState(),this.setBuffer(e);var t=this.doDecodeSync();if(this.hasRemaining(1))throw this.createExtraByteError(this.pos);return t},e.prototype.decodeMulti=function(e){return Dn(this,(function(t){switch(t.label){case 0:this.reinitializeState(),this.setBuffer(e),t.label=1;case 1:return this.hasRemaining(1)?[4,this.doDecodeSync()]:[3,3];case 2:return t.sent(),[3,1];case 3:return[2]}}))},e.prototype.decodeAsync=function(e){var t,n,r,o,i,s,a;return i=this,void 0,a=function(){var i,s,a,c,l,h,d,u;return Dn(this,(function(p){switch(p.label){case 0:i=!1,p.label=1;case 1:p.trys.push([1,6,7,12]),t=xn(e),p.label=2;case 2:return[4,t.next()];case 3:if((n=p.sent()).done)return[3,5];if(a=n.value,i)throw this.createExtraByteError(this.totalPos);this.appendBuffer(a);try{s=this.doDecodeSync(),i=!0}catch(e){if(!(e instanceof Ln))throw e}this.totalPos+=this.pos,p.label=4;case 4:return[3,2];case 5:return[3,12];case 6:return c=p.sent(),r={error:c},[3,12];case 7:return p.trys.push([7,,10,11]),n&&!n.done&&(o=t.return)?[4,o.call(t)]:[3,9];case 8:p.sent(),p.label=9;case 9:return[3,11];case 10:if(r)throw r.error;return[7];case 11:return[7];case 12:if(i){if(this.hasRemaining(1))throw this.createExtraByteError(this.totalPos);return[2,s]}throw h=(l=this).headByte,d=l.pos,u=l.totalPos,new RangeError("Insufficient data in parsing ".concat(Cn(h)," at ").concat(u," (").concat(d," in the current buffer)"))}}))},new((s=void 0)||(s=Promise))((function(e,t){function n(e){try{o(a.next(e))}catch(e){t(e)}}function r(e){try{o(a.throw(e))}catch(e){t(e)}}function o(t){var o;t.done?e(t.value):(o=t.value,o instanceof s?o:new s((function(e){e(o)}))).then(n,r)}o((a=a.apply(i,[])).next())}))},e.prototype.decodeArrayStream=function(e){return this.decodeMultiAsync(e,!0)},e.prototype.decodeStream=function(e){return this.decodeMultiAsync(e,!1)},e.prototype.decodeMultiAsync=function(e,t){return Rn(this,arguments,(function(){var n,r,o,i,s,a,c,l,h;return Dn(this,(function(d){switch(d.label){case 0:n=t,r=-1,d.label=1;case 1:d.trys.push([1,13,14,19]),o=xn(e),d.label=2;case 2:return[4,An(o.next())];case 3:if((i=d.sent()).done)return[3,12];if(s=i.value,t&&0===r)throw this.createExtraByteError(this.totalPos);this.appendBuffer(s),n&&(r=this.readArraySize(),n=!1,this.complete()),d.label=4;case 4:d.trys.push([4,9,,10]),d.label=5;case 5:return[4,An(this.doDecodeSync())];case 6:return[4,d.sent()];case 7:return d.sent(),0==--r?[3,8]:[3,5];case 8:return[3,10];case 9:if(!((a=d.sent())instanceof Ln))throw a;return[3,10];case 10:this.totalPos+=this.pos,d.label=11;case 11:return[3,2];case 12:return[3,19];case 13:return c=d.sent(),l={error:c},[3,19];case 14:return d.trys.push([14,,17,18]),i&&!i.done&&(h=o.return)?[4,An(h.call(o))]:[3,16];case 15:d.sent(),d.label=16;case 16:return[3,18];case 17:if(l)throw l.error;return[7];case 18:return[7];case 19:return[2]}}))}))},e.prototype.doDecodeSync=function(){e:for(;;){var e=this.readHeadByte(),t=void 0;if(e>=224)t=e-256;else if(e<192)if(e<128)t=e;else if(e<144){if(0!=(r=e-128)){this.pushMapState(r),this.complete();continue e}t={}}else if(e<160){if(0!=(r=e-144)){this.pushArrayState(r),this.complete();continue e}t=[]}else{var n=e-160;t=this.decodeUtf8String(n,0)}else if(192===e)t=null;else if(194===e)t=!1;else if(195===e)t=!0;else if(202===e)t=this.readF32();else if(203===e)t=this.readF64();else if(204===e)t=this.readU8();else if(205===e)t=this.readU16();else if(206===e)t=this.readU32();else if(207===e)t=this.readU64();else if(208===e)t=this.readI8();else if(209===e)t=this.readI16();else if(210===e)t=this.readI32();else if(211===e)t=this.readI64();else if(217===e)n=this.lookU8(),t=this.decodeUtf8String(n,1);else if(218===e)n=this.lookU16(),t=this.decodeUtf8String(n,2);else if(219===e)n=this.lookU32(),t=this.decodeUtf8String(n,4);else if(220===e){if(0!==(r=this.readU16())){this.pushArrayState(r),this.complete();continue e}t=[]}else if(221===e){if(0!==(r=this.readU32())){this.pushArrayState(r),this.complete();continue e}t=[]}else if(222===e){if(0!==(r=this.readU16())){this.pushMapState(r),this.complete();continue e}t={}}else if(223===e){if(0!==(r=this.readU32())){this.pushMapState(r),this.complete();continue e}t={}}else if(196===e){var r=this.lookU8();t=this.decodeBinary(r,1)}else if(197===e)r=this.lookU16(),t=this.decodeBinary(r,2);else if(198===e)r=this.lookU32(),t=this.decodeBinary(r,4);else if(212===e)t=this.decodeExtension(1,0);else if(213===e)t=this.decodeExtension(2,0);else if(214===e)t=this.decodeExtension(4,0);else if(215===e)t=this.decodeExtension(8,0);else if(216===e)t=this.decodeExtension(16,0);else if(199===e)r=this.lookU8(),t=this.decodeExtension(r,1);else if(200===e)r=this.lookU16(),t=this.decodeExtension(r,2);else{if(201!==e)throw new yn("Unrecognized type byte: ".concat(Cn(e)));r=this.lookU32(),t=this.decodeExtension(r,4)}this.complete();for(var o=this.stack;o.length>0;){var i=o[o.length-1];if(0===i.type){if(i.array[i.position]=t,i.position++,i.position!==i.size)continue e;o.pop(),t=i.array}else{if(1===i.type){if("string"!=(s=typeof t)&&"number"!==s)throw new yn("The type of key must be string or number but "+typeof t);if("__proto__"===t)throw new yn("The key __proto__ is not allowed");i.key=t,i.type=2;continue e}if(i.map[i.key]=t,i.readCount++,i.readCount!==i.size){i.key=null,i.type=1;continue e}o.pop(),t=i.map}}return t}var s},e.prototype.readHeadByte=function(){return this.headByte===Nn&&(this.headByte=this.readU8()),this.headByte},e.prototype.complete=function(){this.headByte=Nn},e.prototype.readArraySize=function(){var e=this.readHeadByte();switch(e){case 220:return this.readU16();case 221:return this.readU32();default:if(e<160)return e-144;throw new yn("Unrecognized array type byte: ".concat(Cn(e)))}},e.prototype.pushMapState=function(e){if(e>this.maxMapLength)throw new yn("Max length exceeded: map length (".concat(e,") > maxMapLengthLength (").concat(this.maxMapLength,")"));this.stack.push({type:1,size:e,key:null,readCount:0,map:{}})},e.prototype.pushArrayState=function(e){if(e>this.maxArrayLength)throw new yn("Max length exceeded: array length (".concat(e,") > maxArrayLength (").concat(this.maxArrayLength,")"));this.stack.push({type:0,size:e,array:new Array(e),position:0})},e.prototype.decodeUtf8String=function(e,t){var n;if(e>this.maxStrLength)throw new yn("Max length exceeded: UTF-8 byte length (".concat(e,") > maxStrLength (").concat(this.maxStrLength,")"));if(this.bytes.byteLengthfn?function(e,t,n){var r=e.subarray(t,t+n);return pn.decode(r)}(this.bytes,o,e):dn(this.bytes,o,e),this.pos+=t+e,r},e.prototype.stateIsMapKey=function(){return this.stack.length>0&&1===this.stack[this.stack.length-1].type},e.prototype.decodeBinary=function(e,t){if(e>this.maxBinLength)throw new yn("Max length exceeded: bin length (".concat(e,") > maxBinLength (").concat(this.maxBinLength,")"));if(!this.hasRemaining(e+t))throw Mn;var n=this.pos+t,r=this.bytes.subarray(n,n+e);return this.pos+=t+e,r},e.prototype.decodeExtension=function(e,t){if(e>this.maxExtLength)throw new yn("Max length exceeded: ext length (".concat(e,") > maxExtLength (").concat(this.maxExtLength,")"));var n=this.view.getInt8(this.pos+t),r=this.decodeBinary(e,t+1);return this.extensionCodec.decode(r,n,this.context)},e.prototype.lookU8=function(){return this.view.getUint8(this.pos)},e.prototype.lookU16=function(){return this.view.getUint16(this.pos)},e.prototype.lookU32=function(){return this.view.getUint32(this.pos)},e.prototype.readU8=function(){var e=this.view.getUint8(this.pos);return this.pos++,e},e.prototype.readI8=function(){var e=this.view.getInt8(this.pos);return this.pos++,e},e.prototype.readU16=function(){var e=this.view.getUint16(this.pos);return this.pos+=2,e},e.prototype.readI16=function(){var e=this.view.getInt16(this.pos);return this.pos+=2,e},e.prototype.readU32=function(){var e=this.view.getUint32(this.pos);return this.pos+=4,e},e.prototype.readI32=function(){var e=this.view.getInt32(this.pos);return this.pos+=4,e},e.prototype.readU64=function(){var e,t,n=(e=this.view,t=this.pos,4294967296*e.getUint32(t)+e.getUint32(t+4));return this.pos+=8,n},e.prototype.readI64=function(){var e=rn(this.view,this.pos);return this.pos+=8,e},e.prototype.readF32=function(){var e=this.view.getFloat32(this.pos);return this.pos+=4,e},e.prototype.readF64=function(){var e=this.view.getFloat64(this.pos);return this.pos+=8,e},e}();class $n{static write(e){let t=e.byteLength||e.length;const n=[];do{let e=127&t;t>>=7,t>0&&(e|=128),n.push(e)}while(t>0);t=e.byteLength||e.length;const r=new Uint8Array(n.length+t);return r.set(n,0),r.set(e,n.length),r.buffer}static parse(e){const t=[],n=new Uint8Array(e),r=[0,7,14,21,28];for(let o=0;o7)throw new Error("Messages bigger than 2GB are not supported.");if(!(n.byteLength>=o+s+a))throw new Error("Incomplete message.");t.push(n.slice?n.slice(o+s,o+s+a):n.subarray(o+s,o+s+a)),o=o+s+a}return t}}const On=new Uint8Array([145,Mt.Ping]);class Hn{constructor(e){this.name="messagepack",this.version=1,this.transferFormat=Lt.Binary,this._errorResult=1,this._voidResult=2,this._nonVoidResult=3,e=e||{},this._encoder=new Sn(e.extensionCodec,e.context,e.maxDepth,e.initialBufferSize,e.sortKeys,e.forceFloat32,e.ignoreUndefined,e.forceIntegerToFloat),this._decoder=new Fn(e.extensionCodec,e.context,e.maxStrLength,e.maxBinLength,e.maxArrayLength,e.maxMapLength,e.maxExtLength)}parseMessages(e,t){if(!(n=e)||"undefined"==typeof ArrayBuffer||!(n instanceof ArrayBuffer||n.constructor&&"ArrayBuffer"===n.constructor.name))throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer.");var n;null===t&&(t=mt.instance);const r=$n.parse(e),o=[];for(const e of r){const n=this._parseMessage(e,t);n&&o.push(n)}return o}writeMessage(e){switch(e.type){case Mt.Invocation:return this._writeInvocation(e);case Mt.StreamInvocation:return this._writeStreamInvocation(e);case Mt.StreamItem:return this._writeStreamItem(e);case Mt.Completion:return this._writeCompletion(e);case Mt.Ping:return $n.write(On);case Mt.CancelInvocation:return this._writeCancelInvocation(e);default:throw new Error("Invalid message type.")}}_parseMessage(e,t){if(0===e.length)throw new Error("Invalid payload.");const n=this._decoder.decode(e);if(0===n.length||!(n instanceof Array))throw new Error("Invalid payload.");const r=n[0];switch(r){case Mt.Invocation:return this._createInvocationMessage(this._readHeaders(n),n);case Mt.StreamItem:return this._createStreamItemMessage(this._readHeaders(n),n);case Mt.Completion:return this._createCompletionMessage(this._readHeaders(n),n);case Mt.Ping:return this._createPingMessage(n);case Mt.Close:return this._createCloseMessage(n);default:return t.log(gt.Information,"Unknown message type '"+r+"' ignored."),null}}_createCloseMessage(e){if(e.length<2)throw new Error("Invalid payload for Close message.");return{allowReconnect:e.length>=3?e[2]:void 0,error:e[1],type:Mt.Close}}_createPingMessage(e){if(e.length<1)throw new Error("Invalid payload for Ping message.");return{type:Mt.Ping}}_createInvocationMessage(e,t){if(t.length<5)throw new Error("Invalid payload for Invocation message.");const n=t[2];return n?{arguments:t[4],headers:e,invocationId:n,streamIds:[],target:t[3],type:Mt.Invocation}:{arguments:t[4],headers:e,streamIds:[],target:t[3],type:Mt.Invocation}}_createStreamItemMessage(e,t){if(t.length<4)throw new Error("Invalid payload for StreamItem message.");return{headers:e,invocationId:t[2],item:t[3],type:Mt.StreamItem}}_createCompletionMessage(e,t){if(t.length<4)throw new Error("Invalid payload for Completion message.");const n=t[3];if(n!==this._voidResult&&t.length<5)throw new Error("Invalid payload for Completion message.");let r,o;switch(n){case this._errorResult:r=t[4];break;case this._nonVoidResult:o=t[4]}return{error:r,headers:e,invocationId:t[2],result:o,type:Mt.Completion}}_writeInvocation(e){let t;return t=e.streamIds?this._encoder.encode([Mt.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments,e.streamIds]):this._encoder.encode([Mt.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments]),$n.write(t.slice())}_writeStreamInvocation(e){let t;return t=e.streamIds?this._encoder.encode([Mt.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments,e.streamIds]):this._encoder.encode([Mt.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments]),$n.write(t.slice())}_writeStreamItem(e){const t=this._encoder.encode([Mt.StreamItem,e.headers||{},e.invocationId,e.item]);return $n.write(t.slice())}_writeCompletion(e){const t=e.error?this._errorResult:void 0!==e.result?this._nonVoidResult:this._voidResult;let n;switch(t){case this._errorResult:n=this._encoder.encode([Mt.Completion,e.headers||{},e.invocationId,t,e.error]);break;case this._voidResult:n=this._encoder.encode([Mt.Completion,e.headers||{},e.invocationId,t]);break;case this._nonVoidResult:n=this._encoder.encode([Mt.Completion,e.headers||{},e.invocationId,t,e.result])}return $n.write(n.slice())}_writeCancelInvocation(e){const t=this._encoder.encode([Mt.CancelInvocation,e.headers||{},e.invocationId]);return $n.write(t.slice())}_readHeaders(e){const t=e[1];if("object"!=typeof t)throw new Error("Invalid headers.");return t}}let jn=!1;function Wn(){const e=document.querySelector("#blazor-error-ui");e&&(e.style.display="block"),jn||(jn=!0,document.querySelectorAll("#blazor-error-ui .reload").forEach((e=>{e.onclick=function(e){location.reload(),e.preventDefault()}})),document.querySelectorAll("#blazor-error-ui .dismiss").forEach((e=>{e.onclick=function(e){const t=document.querySelector("#blazor-error-ui");t&&(t.style.display="none"),e.preventDefault()}})))}const zn="function"==typeof TextDecoder?new TextDecoder("utf-8"):null,Jn=zn?zn.decode.bind(zn):function(e){let t=0;const n=e.length,r=[],o=[];for(;t65535&&(o-=65536,r.push(o>>>10&1023|55296),o=56320|1023&o),r.push(o)}r.length>1024&&(o.push(String.fromCharCode.apply(null,r)),r.length=0)}return o.push(String.fromCharCode.apply(null,r)),o.join("")},qn=Math.pow(2,32),Vn=Math.pow(2,21)-1;function Kn(e,t){return e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24}function Xn(e,t){return e[t]+(e[t+1]<<8)+(e[t+2]<<16)+(e[t+3]<<24>>>0)}function Gn(e,t){const n=Xn(e,t+4);if(n>Vn)throw new Error(`Cannot read uint64 with high order part ${n}, because the result would exceed Number.MAX_SAFE_INTEGER.`);return n*qn+Xn(e,t)}class Yn{constructor(e){this.batchData=e;const t=new tr(e);this.arrayRangeReader=new nr(e),this.arrayBuilderSegmentReader=new rr(e),this.diffReader=new Qn(e),this.editReader=new Zn(e,t),this.frameReader=new er(e,t)}updatedComponents(){return Kn(this.batchData,this.batchData.length-20)}referenceFrames(){return Kn(this.batchData,this.batchData.length-16)}disposedComponentIds(){return Kn(this.batchData,this.batchData.length-12)}disposedEventHandlerIds(){return Kn(this.batchData,this.batchData.length-8)}updatedComponentsEntry(e,t){const n=e+4*t;return Kn(this.batchData,n)}referenceFramesEntry(e,t){return e+20*t}disposedComponentIdsEntry(e,t){const n=e+4*t;return Kn(this.batchData,n)}disposedEventHandlerIdsEntry(e,t){const n=e+8*t;return Gn(this.batchData,n)}}class Qn{constructor(e){this.batchDataUint8=e}componentId(e){return Kn(this.batchDataUint8,e)}edits(e){return e+4}editsEntry(e,t){return e+16*t}}class Zn{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}editType(e){return Kn(this.batchDataUint8,e)}siblingIndex(e){return Kn(this.batchDataUint8,e+4)}newTreeIndex(e){return Kn(this.batchDataUint8,e+8)}moveToSiblingIndex(e){return Kn(this.batchDataUint8,e+8)}removedAttributeName(e){const t=Kn(this.batchDataUint8,e+12);return this.stringReader.readString(t)}}class er{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}frameType(e){return Kn(this.batchDataUint8,e)}subtreeLength(e){return Kn(this.batchDataUint8,e+4)}elementReferenceCaptureId(e){const t=Kn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}componentId(e){return Kn(this.batchDataUint8,e+8)}elementName(e){const t=Kn(this.batchDataUint8,e+8);return this.stringReader.readString(t)}textContent(e){const t=Kn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}markupContent(e){const t=Kn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeName(e){const t=Kn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeValue(e){const t=Kn(this.batchDataUint8,e+8);return this.stringReader.readString(t)}attributeEventHandlerId(e){return Gn(this.batchDataUint8,e+12)}}class tr{constructor(e){this.batchDataUint8=e,this.stringTableStartIndex=Kn(e,e.length-4)}readString(e){if(-1===e)return null;{const n=Kn(this.batchDataUint8,this.stringTableStartIndex+4*e),r=function(e,t){let n=0,r=0;for(let o=0;o<4;o++){const i=e[t+o];if(n|=(127&i)<this.nextBatchId)return this.fatalError?(this.logger.log(or.Debug,`Received a new batch ${e} but errored out on a previous batch ${this.nextBatchId-1}`),void await n.send("OnRenderCompleted",this.nextBatchId-1,this.fatalError.toString())):void this.logger.log(or.Debug,`Waiting for batch ${this.nextBatchId}. Batch ${e} not processed.`);try{this.nextBatchId++,this.logger.log(or.Debug,`Applying batch ${e}.`),we(this.browserRendererId,new Yn(t)),await this.completeBatch(n,e)}catch(t){throw this.fatalError=t.toString(),this.logger.log(or.Error,`There was an error applying batch ${e}.`),n.send("OnRenderCompleted",e,t.toString()),t}}getLastBatchid(){return this.nextBatchId-1}async completeBatch(e,t){try{await e.send("OnRenderCompleted",t,null)}catch{this.logger.log(or.Warning,`Failed to deliver completion notification for render '${t}'.`)}}}class sr{log(e,t){}}sr.instance=new sr;class ar{constructor(e){this.minLevel=e}log(e,t){if(e>=this.minLevel){const n=`[${(new Date).toISOString()}] ${or[e]}: ${t}`;switch(e){case or.Critical:case or.Error:console.error(n);break;case or.Warning:console.warn(n);break;case or.Information:console.info(n);break;default:console.log(n)}}}}class cr{constructor(e,t){this.circuitId=void 0,this.components=e,this.applicationState=t}reconnect(e){if(!this.circuitId)throw new Error("Circuit host not initialized.");return e.state!==Bt.Connected?Promise.resolve(!1):e.invoke("ConnectCircuit",this.circuitId)}initialize(e){if(this.circuitId)throw new Error(`Circuit host '${this.circuitId}' already initialized.`);this.circuitId=e}async startCircuit(e){if(e.state!==Bt.Connected)return!1;const t=await e.invoke("StartCircuit",De.getBaseURI(),De.getLocationHref(),JSON.stringify(this.components.map((e=>e.toRecord()))),this.applicationState||"");return!!t&&(this.initialize(t),!0)}resolveElement(e){const t=v(e);if(t)return O(t,!0);const n=Number.parseInt(e);if(!Number.isNaN(n))return $(this.components[n].start,this.components[n].end);throw new Error(`Invalid sequence number or identifier '${e}'.`)}}const lr={configureSignalR:e=>{},logLevel:or.Warning,reconnectionOptions:{maxRetries:8,retryIntervalMilliseconds:2e4,dialogId:"components-reconnect-modal"}};class hr{constructor(e,t,n,r){this.maxRetries=t,this.document=n,this.logger=r,this.addedToDom=!1,this.modal=this.document.createElement("div"),this.modal.id=e,this.maxRetries=t,this.modal.style.cssText=["position: fixed","top: 0","right: 0","bottom: 0","left: 0","z-index: 1050","display: none","overflow: hidden","background-color: #fff","opacity: 0.8","text-align: center","font-weight: bold","transition: visibility 0s linear 500ms"].join(";"),this.message=this.document.createElement("h5"),this.message.style.cssText="margin-top: 20px",this.button=this.document.createElement("button"),this.button.style.cssText="margin:5px auto 5px",this.button.textContent="Retry";const o=this.document.createElement("a");o.addEventListener("click",(()=>location.reload())),o.textContent="reload",this.reloadParagraph=this.document.createElement("p"),this.reloadParagraph.textContent="Alternatively, ",this.reloadParagraph.appendChild(o),this.modal.appendChild(this.message),this.modal.appendChild(this.button),this.modal.appendChild(this.reloadParagraph),this.loader=this.getLoader(),this.message.after(this.loader),this.button.addEventListener("click",(async()=>{this.show();try{await et.reconnect()||this.rejected()}catch(e){this.logger.log(or.Error,e),this.failed()}}))}show(){this.addedToDom||(this.addedToDom=!0,this.document.body.appendChild(this.modal)),this.modal.style.display="block",this.loader.style.display="inline-block",this.button.style.display="none",this.reloadParagraph.style.display="none",this.message.textContent="Attempting to reconnect to the server...",this.modal.style.visibility="hidden",setTimeout((()=>{this.modal.style.visibility="visible"}),0)}update(e){this.message.textContent=`Attempting to reconnect to the server: ${e} of ${this.maxRetries}`}hide(){this.modal.style.display="none"}failed(){this.button.style.display="block",this.reloadParagraph.style.display="none",this.loader.style.display="none";const e=this.document.createTextNode("Reconnection failed. Try "),t=this.document.createElement("a");t.textContent="reloading",t.setAttribute("href",""),t.addEventListener("click",(()=>location.reload()));const n=this.document.createTextNode(" the page if you're unable to reconnect.");this.message.replaceChildren(e,t,n)}rejected(){this.button.style.display="none",this.reloadParagraph.style.display="none",this.loader.style.display="none";const e=this.document.createTextNode("Could not reconnect to the server. "),t=this.document.createElement("a");t.textContent="Reload",t.setAttribute("href",""),t.addEventListener("click",(()=>location.reload()));const n=this.document.createTextNode(" the page to restore functionality.");this.message.replaceChildren(e,t,n)}getLoader(){const e=this.document.createElement("div");return e.style.cssText=["border: 0.3em solid #f3f3f3","border-top: 0.3em solid #3498db","border-radius: 50%","width: 2em","height: 2em","display: inline-block"].join(";"),e.animate([{transform:"rotate(0deg)"},{transform:"rotate(360deg)"}],{duration:2e3,iterations:1/0}),e}}class dr{constructor(e,t,n){this.dialog=e,this.maxRetries=t,this.document=n,this.document=n;const r=this.document.getElementById(dr.MaxRetriesId);r&&(r.innerText=this.maxRetries.toString())}show(){this.removeClasses(),this.dialog.classList.add(dr.ShowClassName)}update(e){const t=this.document.getElementById(dr.CurrentAttemptId);t&&(t.innerText=e.toString())}hide(){this.removeClasses(),this.dialog.classList.add(dr.HideClassName)}failed(){this.removeClasses(),this.dialog.classList.add(dr.FailedClassName)}rejected(){this.removeClasses(),this.dialog.classList.add(dr.RejectedClassName)}removeClasses(){this.dialog.classList.remove(dr.ShowClassName,dr.HideClassName,dr.FailedClassName,dr.RejectedClassName)}}dr.ShowClassName="components-reconnect-show",dr.HideClassName="components-reconnect-hide",dr.FailedClassName="components-reconnect-failed",dr.RejectedClassName="components-reconnect-rejected",dr.MaxRetriesId="components-reconnect-max-retries",dr.CurrentAttemptId="components-reconnect-current-attempt";class ur{constructor(e,t,n){this._currentReconnectionProcess=null,this._logger=e,this._reconnectionDisplay=t,this._reconnectCallback=n||et.reconnect}onConnectionDown(e,t){if(!this._reconnectionDisplay){const t=document.getElementById(e.dialogId);this._reconnectionDisplay=t?new dr(t,e.maxRetries,document):new hr(e.dialogId,e.maxRetries,document,this._logger)}this._currentReconnectionProcess||(this._currentReconnectionProcess=new pr(e,this._logger,this._reconnectCallback,this._reconnectionDisplay))}onConnectionUp(){this._currentReconnectionProcess&&(this._currentReconnectionProcess.dispose(),this._currentReconnectionProcess=null)}}class pr{constructor(e,t,n,r){this.logger=t,this.reconnectCallback=n,this.isDisposed=!1,this.reconnectDisplay=r,this.reconnectDisplay.show(),this.attemptPeriodicReconnection(e)}dispose(){this.isDisposed=!0,this.reconnectDisplay.hide()}async attemptPeriodicReconnection(e){for(let t=0;tpr.MaximumFirstRetryInterval?pr.MaximumFirstRetryInterval:e.retryIntervalMilliseconds;if(await this.delay(n),this.isDisposed)break;try{return await this.reconnectCallback()?void 0:void this.reconnectDisplay.rejected()}catch(e){this.logger.log(or.Error,e)}}this.reconnectDisplay.failed()}delay(e){return new Promise((t=>setTimeout(t,e)))}}function fr(e,t){switch(t){case"webassembly":return function(e){const t=yr(e,"webassembly"),n=[];for(let e=0;ee.id-t.id))}(e);case"server":return function(e){const t=yr(e,"server"),n=[];for(let e=0;ee.sequence-t.sequence))}(e)}}pr.MaximumFirstRetryInterval=3e3;const gr=/^\s*Blazor-Component-State:(?[a-zA-Z0-9+/=]+)$/;function mr(e){var t;if(e.nodeType===Node.COMMENT_NODE){const n=e.textContent||"",r=gr.exec(n),o=r&&r.groups&&r.groups.state;return o&&(null===(t=e.parentNode)||void 0===t||t.removeChild(e)),o}if(!e.hasChildNodes())return;const n=e.childNodes;for(let e=0;e.*)$/);function vr(e,t){const n=e.currentElement;if(n&&n.nodeType===Node.COMMENT_NODE&&n.textContent){const r=wr.exec(n.textContent),o=r&&r.groups&&r.groups.descriptor;if(!o)return;try{const r=function(e){const t=JSON.parse(e),{type:n}=t;if("server"!==n&&"webassembly"!==n)throw new Error(`Invalid component type '${n}'.`);return t}(o);switch(t){case"webassembly":return function(e,t,n){const{type:r,assembly:o,typeName:i,parameterDefinitions:s,parameterValues:a,prerenderId:c}=e,l=c?br(c,n):void 0;if(c&&!l)throw new Error(`Could not find an end component comment for '${t}'.`);if("webassembly"===r){if(!o)throw new Error("assembly must be defined when using a descriptor.");if(!i)throw new Error("typeName must be defined when using a descriptor.");return{type:r,assembly:o,typeName:i,parameterDefinitions:s&&atob(s),parameterValues:a&&atob(a),start:t,prerenderId:c,end:l}}}(r,n,e);case"server":return function(e,t,n){const{type:r,descriptor:o,sequence:i,prerenderId:s}=e,a=s?br(s,n):void 0;if(s&&!a)throw new Error(`Could not find an end component comment for '${t}'.`);if("server"===r){if(!o)throw new Error("descriptor must be defined when using a descriptor.");if(void 0===i)throw new Error("sequence must be defined when using a descriptor.");if(!Number.isInteger(i))throw new Error(`Error parsing the sequence '${i}' for component '${JSON.stringify(e)}'`);return{type:r,sequence:i,descriptor:o,start:t,prerenderId:s,end:a}}}(r,n,e)}}catch(e){throw new Error(`Found malformed component comment at ${n.textContent}`)}}}function br(e,t){for(;t.next()&&t.currentElement;){const n=t.currentElement;if(n.nodeType!==Node.COMMENT_NODE)continue;if(!n.textContent)continue;const r=wr.exec(n.textContent),o=r&&r[1];if(o)return _r(o,e),n}}function _r(e,t){const n=JSON.parse(e);if(1!==Object.keys(n).length)throw new Error(`Invalid end of component comment: '${e}'`);const r=n.prerenderId;if(!r)throw new Error(`End of component comment must have a value for the prerendered property: '${e}'`);if(r!==t)throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${t}', '${r}'`)}class Er{constructor(e){this.childNodes=e,this.currentIndex=-1,this.length=e.length}next(){return this.currentIndex++,this.currentIndexasync function(e,n){const r=function(e){const t=document.baseURI;return t.endsWith("/")?`${t}${e}`:`${t}/${e}`}(n),o=await import(r);if(void 0===o)return;const{beforeStart:i,afterStarted:s}=o;return s&&e.afterStartedCallbacks.push(s),i?i(...t):void 0}(this,e))))}async invokeAfterStartedCallbacks(e){await I,await Promise.all(this.afterStartedCallbacks.map((t=>t(e))))}}let kr,Tr,Dr,xr,Ar=!1;async function Rr(e,t,n){var r,o;const i=new Hn;i.name="blazorpack";const s=(new Yt).withUrl("_blazor").withHubProtocol(i);e.configureSignalR(s);const a=s.build();a.on("JS.AttachComponent",((e,t)=>ye(0,n.resolveElement(t),e,!1))),a.on("JS.BeginInvokeJS",Dr.beginInvokeJSFromDotNet.bind(Dr)),a.on("JS.EndInvokeDotNet",Dr.endInvokeDotNetFromJS.bind(Dr)),a.on("JS.ReceiveByteArray",Dr.receiveByteArray.bind(Dr)),a.on("JS.BeginTransmitStream",(e=>{const t=new ReadableStream({start(t){a.stream("SendDotNetStreamToJS",e).subscribe({next:e=>t.enqueue(e),complete:()=>t.close(),error:e=>t.error(e)})}});Dr.supplyDotNetStream(e,t)}));const c=ir.getOrCreate(t);a.on("JS.RenderBatch",((e,n)=>{t.log(or.Debug,`Received render batch with id ${e} and ${n.byteLength} bytes.`),c.processBatch(e,n,a)})),a.on("JS.EndLocationChanging",et._internal.navigationManager.endLocationChanging),a.onclose((t=>!Ar&&e.reconnectionHandler.onConnectionDown(e.reconnectionOptions,t))),a.on("JS.Error",(e=>{Ar=!0,Nr(a,e,t),Wn()}));try{await a.start(),kr=a}catch(e){if(Nr(a,e,t),"FailedToNegotiateWithServerError"===e.errorType)throw e;Wn(),e.innerErrors&&(e.innerErrors.some((e=>"UnsupportedTransportError"===e.errorType&&e.transport===Ut.WebSockets))?t.log(or.Error,"Unable to connect, please ensure you are using an updated browser that supports WebSockets."):e.innerErrors.some((e=>"FailedToStartTransportError"===e.errorType&&e.transport===Ut.WebSockets))?t.log(or.Error,"Unable to connect, please ensure WebSockets are available. A VPN or proxy may be blocking the connection."):e.innerErrors.some((e=>"DisabledTransportError"===e.errorType&&e.transport===Ut.LongPolling))&&t.log(or.Error,"Unable to initiate a SignalR connection to the server. This might be because the server is not configured to support WebSockets. For additional details, visit https://aka.ms/blazor-server-websockets-error."))}return(null===(o=null===(r=a.connection)||void 0===r?void 0:r.features)||void 0===o?void 0:o.inherentKeepAlive)&&t.log(or.Warning,"Failed to connect via WebSockets, using the Long Polling fallback transport. This may be due to a VPN or proxy blocking the connection. To troubleshoot this, visit https://aka.ms/blazor-server-using-fallback-long-polling."),a}function Nr(e,t,n){n.log(or.Error,t),e&&e.stop()}function Pr(e){return xr=e,xr}var Ur,Lr;const Mr=navigator,Br=Mr.userAgentData&&Mr.userAgentData.brands,Fr=Br?Br.some((e=>"Google Chrome"===e.brand||"Microsoft Edge"===e.brand||"Chromium"===e.brand)):window.chrome,$r=null!==(Lr=null===(Ur=Mr.userAgentData)||void 0===Ur?void 0:Ur.platform)&&void 0!==Lr?Lr:navigator.platform;let Or,Hr,jr,Wr,zr,Jr,qr=!1,Vr=!1;function Kr(){return(qr||Vr)&&(Fr||navigator.userAgent.includes("Firefox"))}const Xr=Math.pow(2,32),Gr=Math.pow(2,21)-1;let Yr=null,Qr="Production";function Zr(e){return Hr.getI32(e)}const eo={start:function(t){return async function(t){const n={},{dotnet:r}=await async function(e){if("undefined"==typeof WebAssembly||!WebAssembly.validate)throw new Error("This browser does not support WebAssembly.");const t=await async function(e,t){const n=void 0!==e?e("manifest","blazor.boot.json","_framework/blazor.boot.json",""):i("_framework/blazor.boot.json");let r;r=n?"string"==typeof n?await i(n):await n:await i("_framework/blazor.boot.json"),Qr=t||r.headers.get("Blazor-Environment")||"Production";const o=await r.json();return o.modifiableAssemblies=r.headers.get("DOTNET-MODIFIABLE-ASSEMBLIES"),o.aspnetCoreBrowserTools=r.headers.get("ASPNETCORE-BROWSER-TOOLS"),o;function i(e){return fetch(e,{method:"GET",credentials:"include",cache:"no-cache"})}}(e.loadBootResource,e.environment),n=Object.keys(t.resources.runtime).filter((e=>e.startsWith("dotnet.")&&e.endsWith(".js")))[0],r=t.resources.runtime[n];let o=`_framework/${n}`;if(e.loadBootResource){const t="dotnetjs",i=e.loadBootResource(t,n,o,r);if("string"==typeof i)o=i;else if(i)throw new Error(`For a ${t} resource, custom loaders must supply a URI string.`)}if(t.cacheBootResources){const e=document.createElement("link");e.rel="modulepreload",e.href=o,e.crossOrigin="anonymous",e.integrity=r,document.head.appendChild(e)}const i=new URL(o,document.baseURI).toString();return await import(i)}(t),o=function(e,t){const n={maxParallelDownloads:1e6,enableDownloadRetry:!1,applicationEnvironment:Qr},r={...window.Module||{},onConfigLoaded:async n=>{n.environmentVariables||(n.environmentVariables={}),0===n.icuDataMode&&(n.environmentVariables.__BLAZOR_SHARDED_ICU="1"),n.aspnetCoreBrowserTools&&(n.environmentVariables.__ASPNETCORE_BROWSER_TOOLS=n.aspnetCoreBrowserTools),et._internal.getApplicationEnvironment=()=>n.applicationEnvironment,t.jsInitializer=await async function(e,t){const n=e.resources.libraryInitializers,r=new Ir;return n&&await r.importInitializersAsync(Object.keys(n),[t,e.resources.extensions]),r}(n,e)},onDownloadResourceProgress:to,config:n,disableDotnet6Compatibility:!1,print:ro,printErr:oo};return r}(t,n);r.withStartupOptions(t).withModuleConfig(o),Jr=await r.create();const{MONO:i,BINDING:s,Module:a,setModuleImports:c,INTERNAL:l}=Jr;jr=a,Or=s,Hr=i,zr=l;const h=zr.resourceLoader;n.resourceLoader=h,function(e){qr=!!e.bootConfig.resources.pdb,Vr=e.bootConfig.debugBuild;const t=$r.match(/^Mac/i)?"Cmd":"Alt";Kr()&&console.info(`Debugging hotkey: Shift+${t}+D (when application has focus)`),document.addEventListener("keydown",(e=>{e.shiftKey&&(e.metaKey||e.altKey)&&"KeyD"===e.code&&(Vr||qr?navigator.userAgent.includes("Firefox")?async function(){const e=await fetch(`_framework/debug?url=${encodeURIComponent(location.href)}&isFirefox=true`);200!==e.status&&console.warn(await e.text())}():Fr?function(){const e=document.createElement("a");e.href=`_framework/debug?url=${encodeURIComponent(location.href)}`,e.target="_blank",e.rel="noopener noreferrer",e.click()}():console.error("Currently, only Microsoft Edge (80+), Google Chrome, or Chromium, are supported for debugging."):console.error("Cannot start debugging, because the application was not compiled with debugging enabled."))}))}(h),et._internal.dotNetCriticalError=oo,et._internal.loadLazyAssembly=e=>async function(e,t){const n=e.bootConfig.resources,r=n.lazyAssembly;if(!r)throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly.");if(!r.hasOwnProperty(t))throw new Error(`${t} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);const o=t,i=function(e,t){const n=e.lastIndexOf(".");if(n<0)throw new Error(`No extension to replace in '${e}'`);return e.substr(0,n)+".pdb"}(t),s=Kr()&&n.pdb&&r.hasOwnProperty(i),a=e.loadResource(o,`_framework/${o}`,r[o],"assembly").response.then((e=>e.arrayBuffer()));if(s){const t=await e.loadResource(i,`_framework/${i}`,r[i],"pdb").response.then((e=>e.arrayBuffer())),[n,o]=await Promise.all([a,t]);return{dll:new Uint8Array(n),pdb:new Uint8Array(o)}}{const e=await a;return{dll:new Uint8Array(e),pdb:null}}}(zr.resourceLoader,e),et._internal.loadSatelliteAssemblies=(e,t)=>async function(e,t,n){const r=e.bootConfig.resources.satelliteResources;r&&await Promise.all(t.filter((e=>r.hasOwnProperty(e))).map((t=>e.loadResources(r[t],(e=>`_framework/${e}`),"assembly"))).reduce(((e,t)=>e.concat(t)),new Array).map((async e=>{const t=await e.response,r=await t.arrayBuffer(),o={dll:new Uint8Array(r)};n(o)})))}(h,e,t),c("blazor-internal",{Blazor:{_internal:et._internal}});const d=await Jr.getAssemblyExports("Microsoft.AspNetCore.Components.WebAssembly");return Object.assign(et._internal,{dotNetExports:{...d.Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime}}),Wr=e.attachDispatcher({beginInvokeDotNetFromJS:(e,t,n,r,o)=>{if(so(),!r&&!t)throw new Error("Either assemblyName or dotNetObjectId must have a non null value.");const i=r?r.toString():t;et._internal.dotNetExports.BeginInvokeDotNet(e?e.toString():null,i,n,o)},endInvokeJSFromDotNet:(e,t,n)=>{et._internal.dotNetExports.EndInvokeJS(n)},sendByteArray:(e,t)=>{et._internal.dotNetExports.ReceiveByteArrayFromJS(e,t)},invokeDotNetFromJS:(e,t,n,r)=>(so(),et._internal.dotNetExports.InvokeDotNet(e||null,t,null!=n?n:0,r))}),n}(t)},callEntryPoint:async function(e){try{await Jr.runMain(e,[])}catch(e){console.error(e),Wn()}},toUint8Array:function(e){const t=io(e),n=Zr(t),r=new Uint8Array(n);return r.set(jr.HEAPU8.subarray(t+4,t+4+n)),r},getArrayLength:function(e){return Zr(io(e))},getArrayEntryPtr:function(e,t,n){return io(e)+4+t*n},getObjectFieldsBaseAddress:function(e){return e+8},readInt16Field:function(e,t){return n=e+(t||0),Hr.getI16(n);var n},readInt32Field:function(e,t){return Zr(e+(t||0))},readUint64Field:function(e,t){return function(e){const t=e>>2,n=jr.HEAPU32[t+1];if(n>Gr)throw new Error(`Cannot read uint64 with high order part ${n}, because the result would exceed Number.MAX_SAFE_INTEGER.`);return n*Xr+jr.HEAPU32[t]}(e+(t||0))},readFloatField:function(e,t){return n=e+(t||0),Hr.getF32(n);var n},readObjectField:function(e,t){return Zr(e+(t||0))},readStringField:function(e,t,n){const r=Zr(e+(t||0));if(0===r)return null;if(n){const e=Or.unbox_mono_obj(r);return"boolean"==typeof e?e?"":null:e}return Or.conv_string(r)},readStructField:function(e,t){return e+(t||0)},beginHeapLock:function(){return so(),Yr=ao.create(),Yr},invokeWhenHeapUnlocked:function(e){Yr?Yr.enqueuePostReleaseAction(e):e()}};function to(e,t){const n=e/t*100;document.documentElement.style.setProperty("--blazor-load-percentage",`${n}%`),document.documentElement.style.setProperty("--blazor-load-percentage-text",`"${Math.floor(n)}%"`)}const no=["DEBUGGING ENABLED"],ro=e=>no.indexOf(e)<0&&console.log(e),oo=e=>{console.error(e||"(null)"),Wn()};function io(e){return e+12}function so(){if(Yr)throw new Error("Assertion failed - heap is currently locked")}class ao{enqueuePostReleaseAction(e){this.postReleaseActions||(this.postReleaseActions=[]),this.postReleaseActions.push(e)}release(){var e;if(Yr!==this)throw new Error("Trying to release a lock which isn't current");for(zr.mono_wasm_gc_unlock(),Yr=null;null===(e=this.postReleaseActions)||void 0===e?void 0:e.length;)this.postReleaseActions.shift()(),so()}static create(){return zr.mono_wasm_gc_lock(),new ao}}class co{constructor(e){this.batchAddress=e,this.arrayRangeReader=lo,this.arrayBuilderSegmentReader=ho,this.diffReader=uo,this.editReader=po,this.frameReader=fo}updatedComponents(){return xr.readStructField(this.batchAddress,0)}referenceFrames(){return xr.readStructField(this.batchAddress,lo.structLength)}disposedComponentIds(){return xr.readStructField(this.batchAddress,2*lo.structLength)}disposedEventHandlerIds(){return xr.readStructField(this.batchAddress,3*lo.structLength)}updatedComponentsEntry(e,t){return go(e,t,uo.structLength)}referenceFramesEntry(e,t){return go(e,t,fo.structLength)}disposedComponentIdsEntry(e,t){const n=go(e,t,4);return xr.readInt32Field(n)}disposedEventHandlerIdsEntry(e,t){const n=go(e,t,8);return xr.readUint64Field(n)}}const lo={structLength:8,values:e=>xr.readObjectField(e,0),count:e=>xr.readInt32Field(e,4)},ho={structLength:12,values:e=>{const t=xr.readObjectField(e,0),n=xr.getObjectFieldsBaseAddress(t);return xr.readObjectField(n,0)},offset:e=>xr.readInt32Field(e,4),count:e=>xr.readInt32Field(e,8)},uo={structLength:4+ho.structLength,componentId:e=>xr.readInt32Field(e,0),edits:e=>xr.readStructField(e,4),editsEntry:(e,t)=>go(e,t,po.structLength)},po={structLength:20,editType:e=>xr.readInt32Field(e,0),siblingIndex:e=>xr.readInt32Field(e,4),newTreeIndex:e=>xr.readInt32Field(e,8),moveToSiblingIndex:e=>xr.readInt32Field(e,8),removedAttributeName:e=>xr.readStringField(e,16)},fo={structLength:36,frameType:e=>xr.readInt16Field(e,4),subtreeLength:e=>xr.readInt32Field(e,8),elementReferenceCaptureId:e=>xr.readStringField(e,16),componentId:e=>xr.readInt32Field(e,12),elementName:e=>xr.readStringField(e,16),textContent:e=>xr.readStringField(e,16),markupContent:e=>xr.readStringField(e,16),attributeName:e=>xr.readStringField(e,16),attributeValue:e=>xr.readStringField(e,24,!0),attributeEventHandlerId:e=>xr.readUint64Field(e,8)};function go(e,t,n){return xr.getArrayEntryPtr(e,t,n)}class mo{constructor(e){this.preregisteredComponents=e;const t={};for(let n=0;n{this.childNodes.forEach((e=>{if(e instanceof HTMLTemplateElement){const t=e.getAttribute("blazor-component-id");t&&function(e,t){const n=function(e){const t=`bl:${e}`,n=document.createNodeIterator(document,NodeFilter.SHOW_COMMENT);let r=null;for(;(r=n.nextNode())&&r.textContent!==t;);if(!r)return null;const o=`/bl:${e}`;let i=null;for(;(i=n.nextNode())&&i.textContent!==o;);return i?{startMarker:r,endMarker:i}:null}(e);if(n){const{startMarker:e,endMarker:r}=n,o=new Range;for(o.setStart(e,e.textContent.length),o.setEnd(r,0),o.deleteContents();t.childNodes[0];)r.parentNode.insertBefore(t.childNodes[0],r)}}(t,e.content)}}))}))}}let So=!1;async function Co(t){if(So)throw new Error("Blazor has already started.");So=!0,await async function(t){const n=fr(document,"server"),r=fr(document,"webassembly");n.length&&await async function(t,n){const r=function(e){const t={...lr,...e};return e&&e.reconnectionOptions&&(t.reconnectionOptions={...lr.reconnectionOptions,...e.reconnectionOptions}),t}(t),o=await async function(e){const t=await fetch("_blazor/initializers",{method:"GET",credentials:"include",cache:"no-cache"}),n=await t.json(),r=new Ir;return await r.importInitializersAsync(n,[e]),r}(r),i=new ar(r.logLevel);et.reconnect=async e=>{if(Ar)return!1;const t=e||await Rr(r,i,Tr);return await Tr.reconnect(t)?(r.reconnectionHandler.onConnectionUp(),!0):(i.log(or.Information,"Reconnection attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server."),!1)},et.defaultReconnectionHandler=new ur(i),r.reconnectionHandler=r.reconnectionHandler||et.defaultReconnectionHandler,i.log(or.Information,"Starting up Blazor server-side application.");const s=mr(document);Tr=new cr(n||[],s||""),et._internal.navigationManager.listenForNavigationEvents(((e,t,n)=>kr.send("OnLocationChanged",e,t,n)),((e,t,n,r)=>kr.send("OnLocationChanging",e,t,n,r))),et._internal.forceCloseConnection=()=>kr.stop(),et._internal.sendJSDataStream=(e,t,n)=>function(e,t,n,r){setTimeout((async()=>{let o=5,i=(new Date).valueOf();try{const s=t instanceof Blob?t.size:t.byteLength;let a=0,c=0;for(;a1)await e.send("ReceiveJSDataChunk",n,c,h,null);else{if(!await e.invoke("ReceiveJSDataChunk",n,c,h,null))break;const t=(new Date).valueOf(),r=t-i;i=t,o=Math.max(1,Math.round(500/Math.max(1,r)))}a+=l,c++}}catch(t){await e.send("ReceiveJSDataChunk",n,-1,null,t.toString())}}),0)}(kr,e,t,n),Dr=e.attachDispatcher({beginInvokeDotNetFromJS:(e,t,n,r,o)=>{kr.send("BeginInvokeDotNetFromJS",e?e.toString():null,t,n,r||0,o)},endInvokeJSFromDotNet:(e,t,n)=>{kr.send("EndInvokeJSFromDotNet",e,t,n)},sendByteArray:(e,t)=>{kr.send("ReceiveByteArray",e,t)}});const a=await Rr(r,i,Tr);if(!await Tr.startCircuit(a))return void i.log(or.Error,"Failed to start the circuit.");let c=!1;const l=()=>{if(!c){const e=new FormData,t=Tr.circuitId;e.append("circuitId",t),c=navigator.sendBeacon("_blazor/disconnect",e)}};et.disconnect=l,window.addEventListener("unload",l,{capture:!1,once:!0}),i.log(or.Information,"Blazor server-side application started."),o.invokeAfterStartedCallbacks(et)}(null==t?void 0:t.circuit,n),r.length&&await async function(e,t){(function(){if(window.parent!==window&&!window.opener&&window.frameElement){const e=window.sessionStorage&&window.sessionStorage["Microsoft.AspNetCore.Components.WebAssembly.Authentication.CachedAuthSettings"],t=e&&JSON.parse(e);return t&&t.redirect_uri&&location.href.startsWith(t.redirect_uri)}return!1})()&&await new Promise((()=>{})),function(e){const t=D;D=(e,n,r)=>{((e,t,n)=>{const r=function(e){return ge[e]}(e);r.eventDelegator.getHandler(t)&&eo.invokeWhenHeapUnlocked(n)})(e,n,(()=>t(e,n,r)))}}(),et._internal.applyHotReload=(e,t,n,r)=>{Wr.invokeDotNetStaticMethod("Microsoft.AspNetCore.Components.WebAssembly","ApplyHotReloadDelta",e,t,n,r)},et._internal.getApplyUpdateCapabilities=()=>Wr.invokeDotNetStaticMethod("Microsoft.AspNetCore.Components.WebAssembly","GetApplyUpdateCapabilities"),et._internal.invokeJSFromDotNet=yo,et._internal.invokeJSJson=wo,et._internal.endInvokeDotNetFromJS=vo,et._internal.receiveWebAssemblyDotNetDataStream=bo,et._internal.receiveByteArray=_o;const n=Pr(eo);et.platform=n,et._internal.renderBatch=(e,t)=>{const n=eo.beginHeapLock();try{we(e,new co(t))}finally{n.release()}},et._internal.navigationManager.listenForNavigationEvents((async(e,t,n)=>{await Wr.invokeDotNetStaticMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","NotifyLocationChanged",e,t,n)}),(async(e,t,n,r)=>{const o=await Wr.invokeDotNetStaticMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","NotifyLocationChangingAsync",t,n,r);et._internal.navigationManager.endLocationChanging(e,o)}));const r=new mo(t||[]);let o,i;et._internal.registeredComponents={getRegisteredComponentsCount:()=>r.getCount(),getId:e=>r.getId(e),getAssembly:e=>r.getAssembly(e),getTypeName:e=>r.getTypeName(e),getParameterDefinitions:e=>r.getParameterDefinitions(e)||"",getParameterValues:e=>r.getParameterValues(e)||""},et._internal.getPersistedState=()=>mr(document)||"",et._internal.attachRootComponentToElement=(e,t,n)=>{const o=r.resolveRegisteredElement(e);o?ye(n,o,t,!1):function(e,t,n){const r="::after",o="::before";let i=!1;if(e.endsWith(r))e=e.slice(0,-r.length),i=!0;else if(e.endsWith(o))throw new Error(`The '${o}' selector is not supported.`);const s=v(e)||document.querySelector(e);if(!s)throw new Error(`Could not find any element matching selector '${e}'.`);ye(n||0,O(s,!0),t,i)}(e,t,n)};try{const t=await n.start(null!=e?e:{});o=t.resourceLoader,i=t.jsInitializer}catch(e){throw new Error(`Failed to start platform. Reason: ${e}`)}n.callEntryPoint(o.bootConfig.entryAssembly),i.invokeAfterStartedCallbacks(et)}(null==t?void 0:t.webAssembly,r)}(t),customElements.define("blazor-ssr",Eo)}et.start=Co,window.DotNet=e,document&&document.currentScript&&"false"!==document.currentScript.getAttribute("autostart")&&Co()})(); \ No newline at end of file +(()=>{"use strict";var e,t,n,r={};r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),function(e){const t=[],n="__jsObjectId",r="__dotNetObject",o="__byte[]",i="__dotNetStream",s="__jsStreamReferenceLength";let a,c;class l{constructor(e){this._jsObject=e,this._cachedFunctions=new Map}findFunction(e){const t=this._cachedFunctions.get(e);if(t)return t;let n,r=this._jsObject;if(e.split(".").forEach((t=>{if(!(t in r))throw new Error(`Could not find '${e}' ('${t}' was undefined).`);n=r,r=r[t]})),r instanceof Function)return r=r.bind(n),this._cachedFunctions.set(e,r),r;throw new Error(`The value '${e}' is not a function.`)}getWrappedObject(){return this._jsObject}}const h={0:new l(window)};h[0]._cachedFunctions.set("import",(e=>("string"==typeof e&&e.startsWith("./")&&(e=new URL(e.substr(2),document.baseURI).toString()),import(e))));let d,u=1;function p(e){t.push(e)}function f(e){if(e&&"object"==typeof e){h[u]=new l(e);const t={[n]:u};return u++,t}throw new Error(`Cannot create a JSObjectReference from the value '${e}'.`)}function g(e){let t=-1;if(e instanceof ArrayBuffer&&(e=new Uint8Array(e)),e instanceof Blob)t=e.size;else{if(!(e.buffer instanceof ArrayBuffer))throw new Error("Supplied value is not a typed array or blob.");if(void 0===e.byteLength)throw new Error(`Cannot create a JSStreamReference from the value '${e}' as it doesn't have a byteLength.`);t=e.byteLength}const r={[s]:t};try{const t=f(e);r[n]=t[n]}catch(t){throw new Error(`Cannot create a JSStreamReference from the value '${e}'.`)}return r}function m(e,n){c=e;const r=n?JSON.parse(n,((e,n)=>t.reduce(((t,n)=>n(e,t)),n))):null;return c=void 0,r}function y(){if(void 0===a)throw new Error("No call dispatcher has been set.");if(null===a)throw new Error("There are multiple .NET runtimes present, so a default dispatcher could not be resolved. Use DotNetObject to invoke .NET instance methods.");return a}e.attachDispatcher=function(e){const t=new w(e);return void 0===a?a=t:a&&(a=null),t},e.attachReviver=p,e.invokeMethod=function(e,t,...n){return y().invokeDotNetStaticMethod(e,t,...n)},e.invokeMethodAsync=function(e,t,...n){return y().invokeDotNetStaticMethodAsync(e,t,...n)},e.createJSObjectReference=f,e.createJSStreamReference=g,e.disposeJSObjectReference=function(e){const t=e&&e[n];"number"==typeof t&&_(t)},function(e){e[e.Default=0]="Default",e[e.JSObjectReference=1]="JSObjectReference",e[e.JSStreamReference=2]="JSStreamReference",e[e.JSVoidResult=3]="JSVoidResult"}(d=e.JSCallResultType||(e.JSCallResultType={}));class w{constructor(e){this._dotNetCallDispatcher=e,this._byteArraysToBeRevived=new Map,this._pendingDotNetToJSStreams=new Map,this._pendingAsyncCalls={},this._nextAsyncCallId=1}getDotNetCallDispatcher(){return this._dotNetCallDispatcher}invokeJSFromDotNet(e,t,n,r){const o=m(this,t),i=I(b(e,r)(...o||[]),n);return null==i?null:T(this,i)}beginInvokeJSFromDotNet(e,t,n,r,o){const i=new Promise((e=>{const r=m(this,n);e(b(t,o)(...r||[]))}));e&&i.then((t=>T(this,[e,!0,I(t,r)]))).then((t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!0,t)),(t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!1,JSON.stringify([e,!1,v(t)]))))}endInvokeDotNetFromJS(e,t,n){const r=t?m(this,n):new Error(n);this.completePendingCall(parseInt(e,10),t,r)}invokeDotNetStaticMethod(e,t,...n){return this.invokeDotNetMethod(e,t,null,n)}invokeDotNetStaticMethodAsync(e,t,...n){return this.invokeDotNetMethodAsync(e,t,null,n)}invokeDotNetMethod(e,t,n,r){if(this._dotNetCallDispatcher.invokeDotNetFromJS){const o=T(this,r),i=this._dotNetCallDispatcher.invokeDotNetFromJS(e,t,n,o);return i?m(this,i):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeDotNetMethodAsync instead.")}invokeDotNetMethodAsync(e,t,n,r){if(e&&n)throw new Error(`For instance method calls, assemblyName should be null. Received '${e}'.`);const o=this._nextAsyncCallId++,i=new Promise(((e,t)=>{this._pendingAsyncCalls[o]={resolve:e,reject:t}}));try{const i=T(this,r);this._dotNetCallDispatcher.beginInvokeDotNetFromJS(o,e,t,n,i)}catch(e){this.completePendingCall(o,!1,e)}return i}receiveByteArray(e,t){this._byteArraysToBeRevived.set(e,t)}processByteArray(e){const t=this._byteArraysToBeRevived.get(e);return t?(this._byteArraysToBeRevived.delete(e),t):null}supplyDotNetStream(e,t){if(this._pendingDotNetToJSStreams.has(e)){const n=this._pendingDotNetToJSStreams.get(e);this._pendingDotNetToJSStreams.delete(e),n.resolve(t)}else{const n=new C;n.resolve(t),this._pendingDotNetToJSStreams.set(e,n)}}getDotNetStreamPromise(e){let t;if(this._pendingDotNetToJSStreams.has(e))t=this._pendingDotNetToJSStreams.get(e).streamPromise,this._pendingDotNetToJSStreams.delete(e);else{const n=new C;this._pendingDotNetToJSStreams.set(e,n),t=n.streamPromise}return t}completePendingCall(e,t,n){if(!this._pendingAsyncCalls.hasOwnProperty(e))throw new Error(`There is no pending async call with ID ${e}.`);const r=this._pendingAsyncCalls[e];delete this._pendingAsyncCalls[e],t?r.resolve(n):r.reject(n)}}function v(e){return e instanceof Error?`${e.message}\n${e.stack}`:e?e.toString():"null"}function b(e,t){const n=h[t];if(n)return n.findFunction(e);throw new Error(`JS object instance with ID ${t} does not exist (has it been disposed?).`)}function _(e){delete h[e]}e.findJSFunction=b,e.disposeJSObjectReferenceById=_;class E{constructor(e,t){this._id=e,this._callDispatcher=t}invokeMethod(e,...t){return this._callDispatcher.invokeDotNetMethod(null,e,this._id,t)}invokeMethodAsync(e,...t){return this._callDispatcher.invokeDotNetMethodAsync(null,e,this._id,t)}dispose(){this._callDispatcher.invokeDotNetMethodAsync(null,"__Dispose",this._id,null).catch((e=>console.error(e)))}serializeAsArg(){return{[r]:this._id}}}e.DotNetObject=E,p((function(e,t){if(t&&"object"==typeof t){if(t.hasOwnProperty(r))return new E(t[r],c);if(t.hasOwnProperty(n)){const e=t[n],r=h[e];if(r)return r.getWrappedObject();throw new Error(`JS object instance with Id '${e}' does not exist. It may have been disposed.`)}if(t.hasOwnProperty(o)){const e=t[o],n=c.processByteArray(e);if(void 0===n)throw new Error(`Byte array index '${e}' does not exist.`);return n}if(t.hasOwnProperty(i)){const e=t[i],n=c.getDotNetStreamPromise(e);return new S(n)}}return t}));class S{constructor(e){this._streamPromise=e}stream(){return this._streamPromise}async arrayBuffer(){return new Response(await this.stream()).arrayBuffer()}}class C{constructor(){this.streamPromise=new Promise(((e,t)=>{this.resolve=e,this.reject=t}))}}function I(e,t){switch(t){case d.Default:return e;case d.JSObjectReference:return f(e);case d.JSStreamReference:return g(e);case d.JSVoidResult:return null;default:throw new Error(`Invalid JS call result type '${t}'.`)}}let k=0;function T(e,t){k=0,c=e;const n=JSON.stringify(t,D);return c=void 0,n}function D(e,t){if(t instanceof E)return t.serializeAsArg();if(t instanceof Uint8Array){c.getDotNetCallDispatcher().sendByteArray(k,t);const e={[o]:k};return k++,e}return t}}(e||(e={})),function(e){e[e.prependFrame=1]="prependFrame",e[e.removeFrame=2]="removeFrame",e[e.setAttribute=3]="setAttribute",e[e.removeAttribute=4]="removeAttribute",e[e.updateText=5]="updateText",e[e.stepIn=6]="stepIn",e[e.stepOut=7]="stepOut",e[e.updateMarkup=8]="updateMarkup",e[e.permutationListEntry=9]="permutationListEntry",e[e.permutationListEnd=10]="permutationListEnd"}(t||(t={})),function(e){e[e.element=1]="element",e[e.text=2]="text",e[e.attribute=3]="attribute",e[e.component=4]="component",e[e.region=5]="region",e[e.elementReferenceCapture=6]="elementReferenceCapture",e[e.markup=8]="markup"}(n||(n={}));class o{constructor(e,t){this.componentId=e,this.fieldValue=t}static fromEvent(e,t){const n=t.target;if(n instanceof Element){const t=function(e){return e instanceof HTMLInputElement?e.type&&"checkbox"===e.type.toLowerCase()?{value:e.checked}:{value:e.value}:e instanceof HTMLSelectElement||e instanceof HTMLTextAreaElement?{value:e.value}:null}(n);if(t)return new o(e,t.value)}return null}}const i=new Map,s=new Map,a=[];function c(e){return i.get(e)}function l(e){const t=i.get(e);return(null==t?void 0:t.browserEventName)||e}function h(e,t){e.forEach((e=>i.set(e,t)))}function d(e){const t=[];for(let n=0;ne.selected)).map((e=>e.value))}}{const e=function(e){return!!e&&"INPUT"===e.tagName&&"checkbox"===e.getAttribute("type")}(t);return{value:e?!!t.checked:t.value}}}}),h(["copy","cut","paste"],{createEventArgs:e=>({type:e.type})}),h(["drag","dragend","dragenter","dragleave","dragover","dragstart","drop"],{createEventArgs:e=>{return{...u(t=e),dataTransfer:t.dataTransfer?{dropEffect:t.dataTransfer.dropEffect,effectAllowed:t.dataTransfer.effectAllowed,files:Array.from(t.dataTransfer.files).map((e=>e.name)),items:Array.from(t.dataTransfer.items).map((e=>({kind:e.kind,type:e.type}))),types:t.dataTransfer.types}:null};var t}}),h(["focus","blur","focusin","focusout"],{createEventArgs:e=>({type:e.type})}),h(["keydown","keyup","keypress"],{createEventArgs:e=>{return{key:(t=e).key,code:t.code,location:t.location,repeat:t.repeat,ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),h(["contextmenu","click","mouseover","mouseout","mousemove","mousedown","mouseup","mouseleave","mouseenter","dblclick"],{createEventArgs:e=>u(e)}),h(["error"],{createEventArgs:e=>{return{message:(t=e).message,filename:t.filename,lineno:t.lineno,colno:t.colno,type:t.type};var t}}),h(["loadstart","timeout","abort","load","loadend","progress"],{createEventArgs:e=>{return{lengthComputable:(t=e).lengthComputable,loaded:t.loaded,total:t.total,type:t.type};var t}}),h(["touchcancel","touchend","touchmove","touchenter","touchleave","touchstart"],{createEventArgs:e=>{return{detail:(t=e).detail,touches:d(t.touches),targetTouches:d(t.targetTouches),changedTouches:d(t.changedTouches),ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),h(["gotpointercapture","lostpointercapture","pointercancel","pointerdown","pointerenter","pointerleave","pointermove","pointerout","pointerover","pointerup"],{createEventArgs:e=>{return{...u(t=e),pointerId:t.pointerId,width:t.width,height:t.height,pressure:t.pressure,tiltX:t.tiltX,tiltY:t.tiltY,pointerType:t.pointerType,isPrimary:t.isPrimary};var t}}),h(["wheel","mousewheel"],{createEventArgs:e=>{return{...u(t=e),deltaX:t.deltaX,deltaY:t.deltaY,deltaZ:t.deltaZ,deltaMode:t.deltaMode};var t}}),h(["toggle"],{createEventArgs:()=>({})});const p=["date","datetime-local","month","time","week"],f=new Map;let g,m,y=0;const w={async add(e,t,n){if(!n)throw new Error("initialParameters must be an object, even if empty.");const r="__bl-dynamic-root:"+(++y).toString();f.set(r,e);const o=await E().invokeMethodAsync("AddRootComponent",t,r),i=new _(o,m[t]);return await i.setParameters(n),i}};function v(e){const t=f.get(e);if(t)return f.delete(e),t}class b{invoke(e){return this._callback(e)}setCallback(t){this._selfJSObjectReference||(this._selfJSObjectReference=e.createJSObjectReference(this)),this._callback=t}getJSObjectReference(){return this._selfJSObjectReference}dispose(){this._selfJSObjectReference&&e.disposeJSObjectReference(this._selfJSObjectReference)}}class _{constructor(e,t){this._jsEventCallbackWrappers=new Map,this._componentId=e;for(const e of t)"eventcallback"===e.type&&this._jsEventCallbackWrappers.set(e.name.toLowerCase(),new b)}setParameters(e){const t={},n=Object.entries(e||{}),r=n.length;for(const[e,r]of n){const n=this._jsEventCallbackWrappers.get(e.toLowerCase());n&&r?(n.setCallback(r),t[e]=n.getJSObjectReference()):t[e]=r}return E().invokeMethodAsync("SetRootComponentParameters",this._componentId,r,t)}async dispose(){if(null!==this._componentId){await E().invokeMethodAsync("RemoveRootComponent",this._componentId),this._componentId=null;for(const e of this._jsEventCallbackWrappers.values())e.dispose()}}}function E(){if(!g)throw new Error("Dynamic root components have not been enabled in this application.");return g}const S=[];let C;const I=new Promise((e=>{C=e}));function k(e,t,n){return D(e,t.eventHandlerId,(()=>T(e).invokeMethodAsync("DispatchEventAsync",t,n)))}function T(e){const t=S[e];if(!t)throw new Error(`No interop methods are registered for renderer ${e}`);return t}let D=(e,t,n)=>n();const x=L(["abort","blur","canplay","canplaythrough","change","cuechange","durationchange","emptied","ended","error","focus","load","loadeddata","loadedmetadata","loadend","loadstart","mouseenter","mouseleave","pointerenter","pointerleave","pause","play","playing","progress","ratechange","reset","scroll","seeked","seeking","stalled","submit","suspend","timeupdate","toggle","unload","volumechange","waiting","DOMNodeInsertedIntoDocument","DOMNodeRemovedFromDocument"]),A={submit:!0},R=L(["click","dblclick","mousedown","mousemove","mouseup"]);class N{constructor(e){this.browserRendererId=e,this.afterClickCallbacks=[];const t=++N.nextEventDelegatorId;this.eventsCollectionKey=`_blazorEvents_${t}`,this.eventInfoStore=new P(this.onGlobalEvent.bind(this))}setListener(e,t,n,r){const o=this.getEventHandlerInfosForElement(e,!0),i=o.getHandler(t);if(i)this.eventInfoStore.update(i.eventHandlerId,n);else{const i={element:e,eventName:t,eventHandlerId:n,renderingComponentId:r};this.eventInfoStore.add(i),o.setHandler(t,i)}}getHandler(e){return this.eventInfoStore.get(e)}removeListener(e){const t=this.eventInfoStore.remove(e);if(t){const e=t.element,n=this.getEventHandlerInfosForElement(e,!1);n&&n.removeHandler(t.eventName)}}notifyAfterClick(e){this.afterClickCallbacks.push(e),this.eventInfoStore.addGlobalListener("click")}setStopPropagation(e,t,n){this.getEventHandlerInfosForElement(e,!0).stopPropagation(t,n)}setPreventDefault(e,t,n){this.getEventHandlerInfosForElement(e,!0).preventDefault(t,n)}onGlobalEvent(e){if(!(e.target instanceof Element))return;this.dispatchGlobalEventToAllElements(e.type,e);const t=(n=e.type,s.get(n));var n;t&&t.forEach((t=>this.dispatchGlobalEventToAllElements(t,e))),"click"===e.type&&this.afterClickCallbacks.forEach((t=>t(e)))}dispatchGlobalEventToAllElements(e,t){const n=t.composedPath();let r=n.shift(),i=null,s=!1;const a=Object.prototype.hasOwnProperty.call(x,e);let l=!1;for(;r;){const u=r,p=this.getEventHandlerInfosForElement(u,!1);if(p){const n=p.getHandler(e);if(n&&(h=u,d=t.type,!((h instanceof HTMLButtonElement||h instanceof HTMLInputElement||h instanceof HTMLTextAreaElement||h instanceof HTMLSelectElement)&&Object.prototype.hasOwnProperty.call(R,d)&&h.disabled))){if(!s){const n=c(e);i=(null==n?void 0:n.createEventArgs)?n.createEventArgs(t):{},s=!0}Object.prototype.hasOwnProperty.call(A,t.type)&&t.preventDefault(),k(this.browserRendererId,{eventHandlerId:n.eventHandlerId,eventName:e,eventFieldInfo:o.fromEvent(n.renderingComponentId,t)},i)}p.stopPropagation(e)&&(l=!0),p.preventDefault(e)&&t.preventDefault()}r=a||l?void 0:n.shift()}var h,d}getEventHandlerInfosForElement(e,t){return Object.prototype.hasOwnProperty.call(e,this.eventsCollectionKey)?e[this.eventsCollectionKey]:t?e[this.eventsCollectionKey]=new U:null}}N.nextEventDelegatorId=0;class P{constructor(e){this.globalListener=e,this.infosByEventHandlerId={},this.countByEventName={},a.push(this.handleEventNameAliasAdded.bind(this))}add(e){if(this.infosByEventHandlerId[e.eventHandlerId])throw new Error(`Event ${e.eventHandlerId} is already tracked`);this.infosByEventHandlerId[e.eventHandlerId]=e,this.addGlobalListener(e.eventName)}get(e){return this.infosByEventHandlerId[e]}addGlobalListener(e){if(e=l(e),Object.prototype.hasOwnProperty.call(this.countByEventName,e))this.countByEventName[e]++;else{this.countByEventName[e]=1;const t=Object.prototype.hasOwnProperty.call(x,e);document.addEventListener(e,this.globalListener,t)}}update(e,t){if(Object.prototype.hasOwnProperty.call(this.infosByEventHandlerId,t))throw new Error(`Event ${t} is already tracked`);const n=this.infosByEventHandlerId[e];delete this.infosByEventHandlerId[e],n.eventHandlerId=t,this.infosByEventHandlerId[t]=n}remove(e){const t=this.infosByEventHandlerId[e];if(t){delete this.infosByEventHandlerId[e];const n=l(t.eventName);0==--this.countByEventName[n]&&(delete this.countByEventName[n],document.removeEventListener(n,this.globalListener))}return t}handleEventNameAliasAdded(e,t){if(Object.prototype.hasOwnProperty.call(this.countByEventName,e)){const n=this.countByEventName[e];delete this.countByEventName[e],document.removeEventListener(e,this.globalListener),this.addGlobalListener(t),this.countByEventName[t]+=n-1}}}class U{constructor(){this.handlers={},this.preventDefaultFlags=null,this.stopPropagationFlags=null}getHandler(e){return Object.prototype.hasOwnProperty.call(this.handlers,e)?this.handlers[e]:null}setHandler(e,t){this.handlers[e]=t}removeHandler(e){delete this.handlers[e]}preventDefault(e,t){return void 0!==t&&(this.preventDefaultFlags=this.preventDefaultFlags||{},this.preventDefaultFlags[e]=t),!!this.preventDefaultFlags&&this.preventDefaultFlags[e]}stopPropagation(e,t){return void 0!==t&&(this.stopPropagationFlags=this.stopPropagationFlags||{},this.stopPropagationFlags[e]=t),!!this.stopPropagationFlags&&this.stopPropagationFlags[e]}}function L(e){const t={};return e.forEach((e=>{t[e]=!0})),t}const M=Z("_blazorLogicalChildren"),B=Z("_blazorLogicalParent"),F=Z("_blazorLogicalEnd");function $(e,t){if(!e.parentNode)throw new Error(`Comment not connected to the DOM ${e.textContent}`);const n=e.parentNode,r=O(n,!0),o=V(r);return Array.from(n.childNodes).forEach((e=>o.push(e))),e[B]=r,t&&(e[F]=t,O(t)),O(e)}function O(e,t){if(e.childNodes.length>0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return M in e||(e[M]=[]),e}function H(e,t){const n=document.createComment("!");return j(n,e,t),n}function j(e,t,n){const r=e;if(e instanceof Comment&&V(r)&&V(r).length>0)throw new Error("Not implemented: inserting non-empty logical container");if(z(r))throw new Error("Not implemented: moving existing logical children");const o=V(t);if(n0;)W(n,0)}const r=n;r.parentNode.removeChild(r)}function z(e){return e[B]||null}function J(e,t){return V(e)[t]}function q(e){const t=X(e);return"http://www.w3.org/2000/svg"===t.namespaceURI&&"foreignObject"!==t.tagName}function V(e){return e[M]}function K(e,t){const n=V(e);t.forEach((e=>{e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=Q(e.moveRangeStart)})),t.forEach((t=>{const r=document.createComment("marker");t.moveToBeforeMarker=r;const o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):Y(r,e)})),t.forEach((e=>{const t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd;let i=r;for(;i;){const e=i.nextSibling;if(n.insertBefore(i,t),i===o)break;i=e}n.removeChild(t)})),t.forEach((e=>{n[e.toSiblingIndex]=e.moveRangeStart}))}function X(e){if(e instanceof Element||e instanceof DocumentFragment)return e;if(e instanceof Comment)return e.parentNode;throw new Error("Not a valid logical element")}function G(e){const t=V(z(e));return t[Array.prototype.indexOf.call(t,e)+1]||null}function Y(e,t){if(t instanceof Element||t instanceof DocumentFragment)t.appendChild(e);else{if(!(t instanceof Comment))throw new Error(`Cannot append node because the parent is not a valid logical element. Parent: ${t}`);{const n=G(t);n?n.parentNode.insertBefore(e,n):Y(e,z(t))}}}function Q(e){if(e instanceof Element||e instanceof DocumentFragment)return e;const t=G(e);if(t)return t.previousSibling;{const t=z(e);return t instanceof Element||t instanceof DocumentFragment?t.lastChild:Q(t)}}function Z(e){return"function"==typeof Symbol?Symbol():e}function ee(e){return`_bl_${e}`}const te="__internalId";e.attachReviver(((e,t)=>t&&"object"==typeof t&&Object.prototype.hasOwnProperty.call(t,te)&&"string"==typeof t[te]?function(e){const t=`[${ee(e)}]`;return document.querySelector(t)}(t[te]):t));const ne="_blazorDeferredValue",re=document.createElement("template"),oe=document.createElementNS("http://www.w3.org/2000/svg","g"),ie={},se="__internal_",ae="preventDefault_",ce="stopPropagation_";class le{constructor(e){this.rootComponentIds=new Set,this.childComponentLocations={},this.eventDelegator=new N(e),this.eventDelegator.notifyAfterClick((e=>{if(!ve)return;if(0!==e.button||function(e){return e.ctrlKey||e.shiftKey||e.altKey||e.metaKey}(e))return;if(e.defaultPrevented)return;const t=function(e){const t=!window._blazorDisableComposedPath&&e.composedPath&&e.composedPath();if(t){for(let e=0;edocument.baseURI,getLocationHref:()=>location.href,scrollToElement:xe};function xe(e){const t=document.getElementById(e);return!!t&&(t.scrollIntoView(),!0)}function Ae(e,t,n=!1){const r=$e(e);!t.forceLoad&&He(r)?Re(r,!1,t.replaceHistoryEntry,t.historyEntryState,n):function(e,t){if(location.href===e){const t=e+"?";history.replaceState(null,"",t),location.replace(e)}else t?location.replace(e):location.href=e}(e,t.replaceHistoryEntry)}async function Re(e,t,n,r,o=!1){if(Ue(),function(e){const t=e.indexOf("#");return t>-1&&location.href.replace(location.hash,"")===e.substring(0,t)}(e))!function(e,t,n){Ne(e,t,n);const r=e.indexOf("#");r!==e.length-1&&xe(e.substring(r+1))}(e,n,r);else{if(!o&&_e&&!await Le(e,r,t))return;me=!0,Ne(e,n,r),await Me(t)}}function Ne(e,t,n){t?history.replaceState({userState:n,_index:Ee},"",e):(Ee++,history.pushState({userState:n,_index:Ee},"",e))}function Pe(e){return new Promise((t=>{const n=ke;ke=()=>{ke=n,t()},history.go(e)}))}function Ue(){Te&&(Te(!1),Te=null)}function Le(e,t,n){return new Promise((r=>{Ue(),Ie?(Se++,Te=r,Ie(Se,e,t,n)):r(!1)}))}async function Me(e){var t;Ce&&await Ce(location.href,null===(t=history.state)||void 0===t?void 0:t.userState,e)}async function Be(e){var t,n;ke&&await ke(e),Ee=null!==(n=null===(t=history.state)||void 0===t?void 0:t._index)&&void 0!==n?n:0}let Fe;function $e(e){return Fe=Fe||document.createElement("a"),Fe.href=e,Fe.href}function Oe(e,t){return e?e.tagName===t?e:Oe(e.parentElement,t):null}function He(e){const t=(n=document.baseURI).substring(0,n.lastIndexOf("/"));var n;const r=e.charAt(t.length);return e.startsWith(t)&&(""===r||"/"===r||"?"===r||"#"===r)}const je={focus:function(e,t){if(e instanceof HTMLElement)e.focus({preventScroll:t});else{if(!(e instanceof SVGElement))throw new Error("Unable to focus an invalid element.");if(!e.hasAttribute("tabindex"))throw new Error("Unable to focus an SVG element that does not have a tabindex.");e.focus({preventScroll:t})}},focusBySelector:function(e,t){const n=document.querySelector(e);n&&(n.hasAttribute("tabindex")||(n.tabIndex=-1),n.focus({preventScroll:!0}))}},We={init:function(e,t,n,r=50){const o=Je(t);(o||document.documentElement).style.overflowAnchor="none";const i=document.createRange();h(n.parentElement)&&(t.style.display="table-row",n.style.display="table-row");const s=new IntersectionObserver((function(r){r.forEach((r=>{var o;if(!r.isIntersecting)return;i.setStartAfter(t),i.setEndBefore(n);const s=i.getBoundingClientRect().height,a=null===(o=r.rootBounds)||void 0===o?void 0:o.height;r.target===t?e.invokeMethodAsync("OnSpacerBeforeVisible",r.intersectionRect.top-r.boundingClientRect.top,s,a):r.target===n&&n.offsetHeight>0&&e.invokeMethodAsync("OnSpacerAfterVisible",r.boundingClientRect.bottom-r.intersectionRect.bottom,s,a)}))}),{root:o,rootMargin:`${r}px`});s.observe(t),s.observe(n);const a=l(t),c=l(n);function l(e){const t={attributes:!0},n=new MutationObserver(((n,r)=>{h(e.parentElement)&&(r.disconnect(),e.style.display="table-row",r.observe(e,t)),s.unobserve(e),s.observe(e)}));return n.observe(e,t),n}function h(e){return null!==e&&(e instanceof HTMLTableElement&&""===e.style.display||"table"===e.style.display||e instanceof HTMLTableSectionElement&&""===e.style.display||"table-row-group"===e.style.display)}ze[e._id]={intersectionObserver:s,mutationObserverBefore:a,mutationObserverAfter:c}},dispose:function(e){const t=ze[e._id];t&&(t.intersectionObserver.disconnect(),t.mutationObserverBefore.disconnect(),t.mutationObserverAfter.disconnect(),e.dispose(),delete ze[e._id])}},ze={};function Je(e){return e&&e!==document.body&&e!==document.documentElement?"visible"!==getComputedStyle(e).overflowY?e:Je(e.parentElement):null}const qe={getAndRemoveExistingTitle:function(){var e;const t=document.head?document.head.getElementsByTagName("title"):[];if(0===t.length)return null;let n=null;for(let r=t.length-1;r>=0;r--){const o=t[r],i=o.previousSibling;i instanceof Comment&&null!==z(i)||(null===n&&(n=o.textContent),null===(e=o.parentNode)||void 0===e||e.removeChild(o))}return n}},Ve={init:function(e,t){t._blazorInputFileNextFileId=0,t.addEventListener("click",(function(){t.value=""})),t.addEventListener("change",(function(){t._blazorFilesById={};const n=Array.prototype.map.call(t.files,(function(e){const n={id:++t._blazorInputFileNextFileId,lastModified:new Date(e.lastModified).toISOString(),name:e.name,size:e.size,contentType:e.type,readPromise:void 0,arrayBuffer:void 0,blob:e};return t._blazorFilesById[n.id]=n,n}));e.invokeMethodAsync("NotifyChange",n)}))},toImageFile:async function(e,t,n,r,o){const i=Ke(e,t),s=await new Promise((function(e){const t=new Image;t.onload=function(){URL.revokeObjectURL(t.src),e(t)},t.onerror=function(){t.onerror=null,URL.revokeObjectURL(t.src)},t.src=URL.createObjectURL(i.blob)})),a=await new Promise((function(e){var t;const i=Math.min(1,r/s.width),a=Math.min(1,o/s.height),c=Math.min(i,a),l=document.createElement("canvas");l.width=Math.round(s.width*c),l.height=Math.round(s.height*c),null===(t=l.getContext("2d"))||void 0===t||t.drawImage(s,0,0,l.width,l.height),l.toBlob(e,n)})),c={id:++e._blazorInputFileNextFileId,lastModified:i.lastModified,name:i.name,size:(null==a?void 0:a.size)||0,contentType:n,blob:a||i.blob};return e._blazorFilesById[c.id]=c,c},readFileData:async function(e,t){return Ke(e,t).blob}};function Ke(e,t){const n=e._blazorFilesById[t];if(!n)throw new Error(`There is no file with ID ${t}. The file list may have changed. See https://aka.ms/aspnet/blazor-input-file-multiple-selections.`);return n}const Xe=new Set,Ge={enableNavigationPrompt:function(e){0===Xe.size&&window.addEventListener("beforeunload",Ye),Xe.add(e)},disableNavigationPrompt:function(e){Xe.delete(e),0===Xe.size&&window.removeEventListener("beforeunload",Ye)}};function Ye(e){e.preventDefault(),e.returnValue=!0}async function Qe(e,t,n){return e instanceof Blob?await async function(e,t,n){const r=e.slice(t,t+n),o=await r.arrayBuffer();return new Uint8Array(o)}(e,t,n):function(e,t,n){return new Uint8Array(e.buffer,e.byteOffset+t,n)}(e,t,n)}const Ze=new Map,et={navigateTo:function(e,t,n=!1){Ae(e,t instanceof Object?t:{forceLoad:t,replaceHistoryEntry:n})},registerCustomEventType:function(e,t){if(!t)throw new Error("The options parameter is required.");if(i.has(e))throw new Error(`The event '${e}' is already registered.`);if(t.browserEventName){const n=s.get(t.browserEventName);n?n.push(e):s.set(t.browserEventName,[e]),a.forEach((n=>n(e,t.browserEventName)))}i.set(e,t)},rootComponents:w,_internal:{navigationManager:De,domWrapper:je,Virtualize:We,PageTitle:qe,InputFile:Ve,NavigationLock:Ge,getJSDataStreamChunk:Qe,attachWebRendererInterop:function(t,n,r){const o=S.length;return S.push(t),Object.keys(n).length>0&&function(t,n,r){if(g)throw new Error("Dynamic root components have already been enabled.");g=t,m=n;for(const[t,o]of Object.entries(r)){const r=e.findJSFunction(t,0);for(const e of o)r(e,n[e])}}(T(o),n,r),C(),o}}};window.Blazor=et;const tt=[0,2e3,1e4,3e4,null];class nt{constructor(e){this._retryDelays=void 0!==e?[...e,null]:tt}nextRetryDelayInMilliseconds(e){return this._retryDelays[e.previousRetryCount]}}class rt{}rt.Authorization="Authorization",rt.Cookie="Cookie";class ot{constructor(e,t,n){this.statusCode=e,this.statusText=t,this.content=n}}class it{get(e,t){return this.send({...t,method:"GET",url:e})}post(e,t){return this.send({...t,method:"POST",url:e})}delete(e,t){return this.send({...t,method:"DELETE",url:e})}getCookieString(e){return""}}class st extends it{constructor(e,t){super(),this._innerClient=e,this._accessTokenFactory=t}async send(e){let t=!0;this._accessTokenFactory&&(!this._accessToken||e.url&&e.url.indexOf("/negotiate?")>0)&&(t=!1,this._accessToken=await this._accessTokenFactory()),this._setAuthorizationHeader(e);const n=await this._innerClient.send(e);return t&&401===n.statusCode&&this._accessTokenFactory?(this._accessToken=await this._accessTokenFactory(),this._setAuthorizationHeader(e),await this._innerClient.send(e)):n}_setAuthorizationHeader(e){e.headers||(e.headers={}),this._accessToken?e.headers[rt.Authorization]=`Bearer ${this._accessToken}`:this._accessTokenFactory&&e.headers[rt.Authorization]&&delete e.headers[rt.Authorization]}getCookieString(e){return this._innerClient.getCookieString(e)}}class at extends Error{constructor(e,t){const n=new.target.prototype;super(`${e}: Status code '${t}'`),this.statusCode=t,this.__proto__=n}}class ct extends Error{constructor(e="A timeout occurred."){const t=new.target.prototype;super(e),this.__proto__=t}}class lt extends Error{constructor(e="An abort occurred."){const t=new.target.prototype;super(e),this.__proto__=t}}class ht extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="UnsupportedTransportError",this.__proto__=n}}class dt extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="DisabledTransportError",this.__proto__=n}}class ut extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.transport=t,this.errorType="FailedToStartTransportError",this.__proto__=n}}class pt extends Error{constructor(e){const t=new.target.prototype;super(e),this.errorType="FailedToNegotiateWithServerError",this.__proto__=t}}class ft extends Error{constructor(e,t){const n=new.target.prototype;super(e),this.innerErrors=t,this.__proto__=n}}var gt;!function(e){e[e.Trace=0]="Trace",e[e.Debug=1]="Debug",e[e.Information=2]="Information",e[e.Warning=3]="Warning",e[e.Error=4]="Error",e[e.Critical=5]="Critical",e[e.None=6]="None"}(gt||(gt={}));class mt{constructor(){}log(e,t){}}mt.instance=new mt;const yt="0.0.0-DEV_BUILD";class wt{static isRequired(e,t){if(null==e)throw new Error(`The '${t}' argument is required.`)}static isNotEmpty(e,t){if(!e||e.match(/^\s*$/))throw new Error(`The '${t}' argument should not be empty.`)}static isIn(e,t,n){if(!(e in t))throw new Error(`Unknown ${n} value: ${e}.`)}}class vt{static get isBrowser(){return"object"==typeof window&&"object"==typeof window.document}static get isWebWorker(){return"object"==typeof self&&"importScripts"in self}static get isReactNative(){return"object"==typeof window&&void 0===window.document}static get isNode(){return!this.isBrowser&&!this.isWebWorker&&!this.isReactNative}}function bt(e,t){let n="";return _t(e)?(n=`Binary data of length ${e.byteLength}`,t&&(n+=`. Content: '${function(e){const t=new Uint8Array(e);let n="";return t.forEach((e=>{n+=`0x${e<16?"0":""}${e.toString(16)} `})),n.substr(0,n.length-1)}(e)}'`)):"string"==typeof e&&(n=`String data of length ${e.length}`,t&&(n+=`. Content: '${e}'`)),n}function _t(e){return e&&"undefined"!=typeof ArrayBuffer&&(e instanceof ArrayBuffer||e.constructor&&"ArrayBuffer"===e.constructor.name)}async function Et(e,t,n,r,o,i){const s={},[a,c]=It();s[a]=c,e.log(gt.Trace,`(${t} transport) sending data. ${bt(o,i.logMessageContent)}.`);const l=_t(o)?"arraybuffer":"text",h=await n.post(r,{content:o,headers:{...s,...i.headers},responseType:l,timeout:i.timeout,withCredentials:i.withCredentials});e.log(gt.Trace,`(${t} transport) request complete. Response status: ${h.statusCode}.`)}class St{constructor(e,t){this._subject=e,this._observer=t}dispose(){const e=this._subject.observers.indexOf(this._observer);e>-1&&this._subject.observers.splice(e,1),0===this._subject.observers.length&&this._subject.cancelCallback&&this._subject.cancelCallback().catch((e=>{}))}}class Ct{constructor(e){this._minLevel=e,this.out=console}log(e,t){if(e>=this._minLevel){const n=`[${(new Date).toISOString()}] ${gt[e]}: ${t}`;switch(e){case gt.Critical:case gt.Error:this.out.error(n);break;case gt.Warning:this.out.warn(n);break;case gt.Information:this.out.info(n);break;default:this.out.log(n)}}}}function It(){let e="X-SignalR-User-Agent";return vt.isNode&&(e="User-Agent"),[e,kt(yt,Tt(),vt.isNode?"NodeJS":"Browser",Dt())]}function kt(e,t,n,r){let o="Microsoft SignalR/";const i=e.split(".");return o+=`${i[0]}.${i[1]}`,o+=` (${e}; `,o+=t&&""!==t?`${t}; `:"Unknown OS; ",o+=`${n}`,o+=r?`; ${r}`:"; Unknown Runtime Version",o+=")",o}function Tt(){if(!vt.isNode)return"";switch(process.platform){case"win32":return"Windows NT";case"darwin":return"macOS";case"linux":return"Linux";default:return process.platform}}function Dt(){if(vt.isNode)return process.versions.node}function xt(e){return e.stack?e.stack:e.message?e.message:`${e}`}class At extends it{constructor(e){if(super(),this._logger=e,"undefined"==typeof fetch){const e=require;this._jar=new(e("tough-cookie").CookieJar),"undefined"==typeof fetch?this._fetchType=e("node-fetch"):this._fetchType=fetch,this._fetchType=e("fetch-cookie")(this._fetchType,this._jar)}else this._fetchType=fetch.bind(function(){if("undefined"!=typeof globalThis)return globalThis;if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if(void 0!==r.g)return r.g;throw new Error("could not find global")}());if("undefined"==typeof AbortController){const e=require;this._abortControllerType=e("abort-controller")}else this._abortControllerType=AbortController}async send(e){if(e.abortSignal&&e.abortSignal.aborted)throw new lt;if(!e.method)throw new Error("No method defined.");if(!e.url)throw new Error("No url defined.");const t=new this._abortControllerType;let n;e.abortSignal&&(e.abortSignal.onabort=()=>{t.abort(),n=new lt});let r,o=null;if(e.timeout){const r=e.timeout;o=setTimeout((()=>{t.abort(),this._logger.log(gt.Warning,"Timeout from HTTP request."),n=new ct}),r)}""===e.content&&(e.content=void 0),e.content&&(e.headers=e.headers||{},_t(e.content)?e.headers["Content-Type"]="application/octet-stream":e.headers["Content-Type"]="text/plain;charset=UTF-8");try{r=await this._fetchType(e.url,{body:e.content,cache:"no-cache",credentials:!0===e.withCredentials?"include":"same-origin",headers:{"X-Requested-With":"XMLHttpRequest",...e.headers},method:e.method,mode:"cors",redirect:"follow",signal:t.signal})}catch(e){if(n)throw n;throw this._logger.log(gt.Warning,`Error from HTTP request. ${e}.`),e}finally{o&&clearTimeout(o),e.abortSignal&&(e.abortSignal.onabort=null)}if(!r.ok){const e=await Rt(r,"text");throw new at(e||r.statusText,r.status)}const i=Rt(r,e.responseType),s=await i;return new ot(r.status,r.statusText,s)}getCookieString(e){return""}}function Rt(e,t){let n;switch(t){case"arraybuffer":n=e.arrayBuffer();break;case"text":default:n=e.text();break;case"blob":case"document":case"json":throw new Error(`${t} is not supported.`)}return n}class Nt extends it{constructor(e){super(),this._logger=e}send(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new lt):e.method?e.url?new Promise(((t,n)=>{const r=new XMLHttpRequest;r.open(e.method,e.url,!0),r.withCredentials=void 0===e.withCredentials||e.withCredentials,r.setRequestHeader("X-Requested-With","XMLHttpRequest"),""===e.content&&(e.content=void 0),e.content&&(_t(e.content)?r.setRequestHeader("Content-Type","application/octet-stream"):r.setRequestHeader("Content-Type","text/plain;charset=UTF-8"));const o=e.headers;o&&Object.keys(o).forEach((e=>{r.setRequestHeader(e,o[e])})),e.responseType&&(r.responseType=e.responseType),e.abortSignal&&(e.abortSignal.onabort=()=>{r.abort(),n(new lt)}),e.timeout&&(r.timeout=e.timeout),r.onload=()=>{e.abortSignal&&(e.abortSignal.onabort=null),r.status>=200&&r.status<300?t(new ot(r.status,r.statusText,r.response||r.responseText)):n(new at(r.response||r.responseText||r.statusText,r.status))},r.onerror=()=>{this._logger.log(gt.Warning,`Error from HTTP request. ${r.status}: ${r.statusText}.`),n(new at(r.statusText,r.status))},r.ontimeout=()=>{this._logger.log(gt.Warning,"Timeout from HTTP request."),n(new ct)},r.send(e.content)})):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}}class Pt extends it{constructor(e){if(super(),"undefined"!=typeof fetch)this._httpClient=new At(e);else{if("undefined"==typeof XMLHttpRequest)throw new Error("No usable HttpClient found.");this._httpClient=new Nt(e)}}send(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new lt):e.method?e.url?this._httpClient.send(e):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}getCookieString(e){return this._httpClient.getCookieString(e)}}var Ut,Lt,Mt,Bt;!function(e){e[e.None=0]="None",e[e.WebSockets=1]="WebSockets",e[e.ServerSentEvents=2]="ServerSentEvents",e[e.LongPolling=4]="LongPolling"}(Ut||(Ut={})),function(e){e[e.Text=1]="Text",e[e.Binary=2]="Binary"}(Lt||(Lt={}));class Ft{constructor(){this._isAborted=!1,this.onabort=null}abort(){this._isAborted||(this._isAborted=!0,this.onabort&&this.onabort())}get signal(){return this}get aborted(){return this._isAborted}}class $t{get pollAborted(){return this._pollAbort.aborted}constructor(e,t,n){this._httpClient=e,this._logger=t,this._pollAbort=new Ft,this._options=n,this._running=!1,this.onreceive=null,this.onclose=null}async connect(e,t){if(wt.isRequired(e,"url"),wt.isRequired(t,"transferFormat"),wt.isIn(t,Lt,"transferFormat"),this._url=e,this._logger.log(gt.Trace,"(LongPolling transport) Connecting."),t===Lt.Binary&&"undefined"!=typeof XMLHttpRequest&&"string"!=typeof(new XMLHttpRequest).responseType)throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");const[n,r]=It(),o={[n]:r,...this._options.headers},i={abortSignal:this._pollAbort.signal,headers:o,timeout:1e5,withCredentials:this._options.withCredentials};t===Lt.Binary&&(i.responseType="arraybuffer");const s=`${e}&_=${Date.now()}`;this._logger.log(gt.Trace,`(LongPolling transport) polling: ${s}.`);const a=await this._httpClient.get(s,i);200!==a.statusCode?(this._logger.log(gt.Error,`(LongPolling transport) Unexpected response code: ${a.statusCode}.`),this._closeError=new at(a.statusText||"",a.statusCode),this._running=!1):this._running=!0,this._receiving=this._poll(this._url,i)}async _poll(e,t){try{for(;this._running;)try{const n=`${e}&_=${Date.now()}`;this._logger.log(gt.Trace,`(LongPolling transport) polling: ${n}.`);const r=await this._httpClient.get(n,t);204===r.statusCode?(this._logger.log(gt.Information,"(LongPolling transport) Poll terminated by server."),this._running=!1):200!==r.statusCode?(this._logger.log(gt.Error,`(LongPolling transport) Unexpected response code: ${r.statusCode}.`),this._closeError=new at(r.statusText||"",r.statusCode),this._running=!1):r.content?(this._logger.log(gt.Trace,`(LongPolling transport) data received. ${bt(r.content,this._options.logMessageContent)}.`),this.onreceive&&this.onreceive(r.content)):this._logger.log(gt.Trace,"(LongPolling transport) Poll timed out, reissuing.")}catch(e){this._running?e instanceof ct?this._logger.log(gt.Trace,"(LongPolling transport) Poll timed out, reissuing."):(this._closeError=e,this._running=!1):this._logger.log(gt.Trace,`(LongPolling transport) Poll errored after shutdown: ${e.message}`)}}finally{this._logger.log(gt.Trace,"(LongPolling transport) Polling complete."),this.pollAborted||this._raiseOnClose()}}async send(e){return this._running?Et(this._logger,"LongPolling",this._httpClient,this._url,e,this._options):Promise.reject(new Error("Cannot send until the transport is connected"))}async stop(){this._logger.log(gt.Trace,"(LongPolling transport) Stopping polling."),this._running=!1,this._pollAbort.abort();try{await this._receiving,this._logger.log(gt.Trace,`(LongPolling transport) sending DELETE request to ${this._url}.`);const e={},[t,n]=It();e[t]=n;const r={headers:{...e,...this._options.headers},timeout:this._options.timeout,withCredentials:this._options.withCredentials};await this._httpClient.delete(this._url,r),this._logger.log(gt.Trace,"(LongPolling transport) DELETE request sent.")}finally{this._logger.log(gt.Trace,"(LongPolling transport) Stop finished."),this._raiseOnClose()}}_raiseOnClose(){if(this.onclose){let e="(LongPolling transport) Firing onclose event.";this._closeError&&(e+=" Error: "+this._closeError),this._logger.log(gt.Trace,e),this.onclose(this._closeError)}}}class Ot{constructor(e,t,n,r){this._httpClient=e,this._accessToken=t,this._logger=n,this._options=r,this.onreceive=null,this.onclose=null}async connect(e,t){return wt.isRequired(e,"url"),wt.isRequired(t,"transferFormat"),wt.isIn(t,Lt,"transferFormat"),this._logger.log(gt.Trace,"(SSE transport) Connecting."),this._url=e,this._accessToken&&(e+=(e.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(this._accessToken)}`),new Promise(((n,r)=>{let o,i=!1;if(t===Lt.Text){if(vt.isBrowser||vt.isWebWorker)o=new this._options.EventSource(e,{withCredentials:this._options.withCredentials});else{const t=this._httpClient.getCookieString(e),n={};n.Cookie=t;const[r,i]=It();n[r]=i,o=new this._options.EventSource(e,{withCredentials:this._options.withCredentials,headers:{...n,...this._options.headers}})}try{o.onmessage=e=>{if(this.onreceive)try{this._logger.log(gt.Trace,`(SSE transport) data received. ${bt(e.data,this._options.logMessageContent)}.`),this.onreceive(e.data)}catch(e){return void this._close(e)}},o.onerror=e=>{i?this._close():r(new Error("EventSource failed to connect. The connection could not be found on the server, either the connection ID is not present on the server, or a proxy is refusing/buffering the connection. If you have multiple servers check that sticky sessions are enabled."))},o.onopen=()=>{this._logger.log(gt.Information,`SSE connected to ${this._url}`),this._eventSource=o,i=!0,n()}}catch(e){return void r(e)}}else r(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"))}))}async send(e){return this._eventSource?Et(this._logger,"SSE",this._httpClient,this._url,e,this._options):Promise.reject(new Error("Cannot send until the transport is connected"))}stop(){return this._close(),Promise.resolve()}_close(e){this._eventSource&&(this._eventSource.close(),this._eventSource=void 0,this.onclose&&this.onclose(e))}}class Ht{constructor(e,t,n,r,o,i){this._logger=n,this._accessTokenFactory=t,this._logMessageContent=r,this._webSocketConstructor=o,this._httpClient=e,this.onreceive=null,this.onclose=null,this._headers=i}async connect(e,t){let n;return wt.isRequired(e,"url"),wt.isRequired(t,"transferFormat"),wt.isIn(t,Lt,"transferFormat"),this._logger.log(gt.Trace,"(WebSockets transport) Connecting."),this._accessTokenFactory&&(n=await this._accessTokenFactory()),new Promise(((r,o)=>{let i;e=e.replace(/^http/,"ws");const s=this._httpClient.getCookieString(e);let a=!1;if(vt.isReactNative){const t={},[r,o]=It();t[r]=o,n&&(t[rt.Authorization]=`Bearer ${n}`),s&&(t[rt.Cookie]=s),i=new this._webSocketConstructor(e,void 0,{headers:{...t,...this._headers}})}else n&&(e+=(e.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(n)}`);i||(i=new this._webSocketConstructor(e)),t===Lt.Binary&&(i.binaryType="arraybuffer"),i.onopen=t=>{this._logger.log(gt.Information,`WebSocket connected to ${e}.`),this._webSocket=i,a=!0,r()},i.onerror=e=>{let t=null;t="undefined"!=typeof ErrorEvent&&e instanceof ErrorEvent?e.error:"There was an error with the transport",this._logger.log(gt.Information,`(WebSockets transport) ${t}.`)},i.onmessage=e=>{if(this._logger.log(gt.Trace,`(WebSockets transport) data received. ${bt(e.data,this._logMessageContent)}.`),this.onreceive)try{this.onreceive(e.data)}catch(e){return void this._close(e)}},i.onclose=e=>{if(a)this._close(e);else{let t=null;t="undefined"!=typeof ErrorEvent&&e instanceof ErrorEvent?e.error:"WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.",o(new Error(t))}}}))}send(e){return this._webSocket&&this._webSocket.readyState===this._webSocketConstructor.OPEN?(this._logger.log(gt.Trace,`(WebSockets transport) sending data. ${bt(e,this._logMessageContent)}.`),this._webSocket.send(e),Promise.resolve()):Promise.reject("WebSocket is not in the OPEN state")}stop(){return this._webSocket&&this._close(void 0),Promise.resolve()}_close(e){this._webSocket&&(this._webSocket.onclose=()=>{},this._webSocket.onmessage=()=>{},this._webSocket.onerror=()=>{},this._webSocket.close(),this._webSocket=void 0),this._logger.log(gt.Trace,"(WebSockets transport) socket closed."),this.onclose&&(!this._isCloseEvent(e)||!1!==e.wasClean&&1e3===e.code?e instanceof Error?this.onclose(e):this.onclose():this.onclose(new Error(`WebSocket closed with status code: ${e.code} (${e.reason||"no reason given"}).`)))}_isCloseEvent(e){return e&&"boolean"==typeof e.wasClean&&"number"==typeof e.code}}class jt{constructor(e,t={}){var n;if(this._stopPromiseResolver=()=>{},this.features={},this._negotiateVersion=1,wt.isRequired(e,"url"),this._logger=void 0===(n=t.logger)?new Ct(gt.Information):null===n?mt.instance:void 0!==n.log?n:new Ct(n),this.baseUrl=this._resolveUrl(e),(t=t||{}).logMessageContent=void 0!==t.logMessageContent&&t.logMessageContent,"boolean"!=typeof t.withCredentials&&void 0!==t.withCredentials)throw new Error("withCredentials option was not a 'boolean' or 'undefined' value");t.withCredentials=void 0===t.withCredentials||t.withCredentials,t.timeout=void 0===t.timeout?1e5:t.timeout,"undefined"==typeof WebSocket||t.WebSocket||(t.WebSocket=WebSocket),"undefined"==typeof EventSource||t.EventSource||(t.EventSource=EventSource),this._httpClient=new st(t.httpClient||new Pt(this._logger),t.accessTokenFactory),this._connectionState="Disconnected",this._connectionStarted=!1,this._options=t,this.onreceive=null,this.onclose=null}async start(e){if(e=e||Lt.Binary,wt.isIn(e,Lt,"transferFormat"),this._logger.log(gt.Debug,`Starting connection with transfer format '${Lt[e]}'.`),"Disconnected"!==this._connectionState)return Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state."));if(this._connectionState="Connecting",this._startInternalPromise=this._startInternal(e),await this._startInternalPromise,"Disconnecting"===this._connectionState){const e="Failed to start the HttpConnection before stop() was called.";return this._logger.log(gt.Error,e),await this._stopPromise,Promise.reject(new lt(e))}if("Connected"!==this._connectionState){const e="HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!";return this._logger.log(gt.Error,e),Promise.reject(new lt(e))}this._connectionStarted=!0}send(e){return"Connected"!==this._connectionState?Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State.")):(this._sendQueue||(this._sendQueue=new Wt(this.transport)),this._sendQueue.send(e))}async stop(e){return"Disconnected"===this._connectionState?(this._logger.log(gt.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnected state.`),Promise.resolve()):"Disconnecting"===this._connectionState?(this._logger.log(gt.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnecting state.`),this._stopPromise):(this._connectionState="Disconnecting",this._stopPromise=new Promise((e=>{this._stopPromiseResolver=e})),await this._stopInternal(e),void await this._stopPromise)}async _stopInternal(e){this._stopError=e;try{await this._startInternalPromise}catch(e){}if(this.transport){try{await this.transport.stop()}catch(e){this._logger.log(gt.Error,`HttpConnection.transport.stop() threw error '${e}'.`),this._stopConnection()}this.transport=void 0}else this._logger.log(gt.Debug,"HttpConnection.transport is undefined in HttpConnection.stop() because start() failed.")}async _startInternal(e){let t=this.baseUrl;this._accessTokenFactory=this._options.accessTokenFactory,this._httpClient._accessTokenFactory=this._accessTokenFactory;try{if(this._options.skipNegotiation){if(this._options.transport!==Ut.WebSockets)throw new Error("Negotiation can only be skipped when using the WebSocket transport directly.");this.transport=this._constructTransport(Ut.WebSockets),await this._startTransport(t,e)}else{let n=null,r=0;do{if(n=await this._getNegotiationResponse(t),"Disconnecting"===this._connectionState||"Disconnected"===this._connectionState)throw new lt("The connection was stopped during negotiation.");if(n.error)throw new Error(n.error);if(n.ProtocolVersion)throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.");if(n.url&&(t=n.url),n.accessToken){const e=n.accessToken;this._accessTokenFactory=()=>e,this._httpClient._accessToken=e,this._httpClient._accessTokenFactory=void 0}r++}while(n.url&&r<100);if(100===r&&n.url)throw new Error("Negotiate redirection limit exceeded.");await this._createTransport(t,this._options.transport,n,e)}this.transport instanceof $t&&(this.features.inherentKeepAlive=!0),"Connecting"===this._connectionState&&(this._logger.log(gt.Debug,"The HttpConnection connected successfully."),this._connectionState="Connected")}catch(e){return this._logger.log(gt.Error,"Failed to start the connection: "+e),this._connectionState="Disconnected",this.transport=void 0,this._stopPromiseResolver(),Promise.reject(e)}}async _getNegotiationResponse(e){const t={},[n,r]=It();t[n]=r;const o=this._resolveNegotiateUrl(e);this._logger.log(gt.Debug,`Sending negotiation request: ${o}.`);try{const e=await this._httpClient.post(o,{content:"",headers:{...t,...this._options.headers},timeout:this._options.timeout,withCredentials:this._options.withCredentials});if(200!==e.statusCode)return Promise.reject(new Error(`Unexpected status code returned from negotiate '${e.statusCode}'`));const n=JSON.parse(e.content);return(!n.negotiateVersion||n.negotiateVersion<1)&&(n.connectionToken=n.connectionId),n}catch(e){let t="Failed to complete negotiation with the server: "+e;return e instanceof at&&404===e.statusCode&&(t+=" Either this is not a SignalR endpoint or there is a proxy blocking the connection."),this._logger.log(gt.Error,t),Promise.reject(new pt(t))}}_createConnectUrl(e,t){return t?e+(-1===e.indexOf("?")?"?":"&")+`id=${t}`:e}async _createTransport(e,t,n,r){let o=this._createConnectUrl(e,n.connectionToken);if(this._isITransport(t))return this._logger.log(gt.Debug,"Connection was provided an instance of ITransport, using that directly."),this.transport=t,await this._startTransport(o,r),void(this.connectionId=n.connectionId);const i=[],s=n.availableTransports||[];let a=n;for(const n of s){const s=this._resolveTransportOrError(n,t,r);if(s instanceof Error)i.push(`${n.transport} failed:`),i.push(s);else if(this._isITransport(s)){if(this.transport=s,!a){try{a=await this._getNegotiationResponse(e)}catch(e){return Promise.reject(e)}o=this._createConnectUrl(e,a.connectionToken)}try{return await this._startTransport(o,r),void(this.connectionId=a.connectionId)}catch(e){if(this._logger.log(gt.Error,`Failed to start the transport '${n.transport}': ${e}`),a=void 0,i.push(new ut(`${n.transport} failed: ${e}`,Ut[n.transport])),"Connecting"!==this._connectionState){const e="Failed to select transport before stop() was called.";return this._logger.log(gt.Debug,e),Promise.reject(new lt(e))}}}}return i.length>0?Promise.reject(new ft(`Unable to connect to the server with any of the available transports. ${i.join(" ")}`,i)):Promise.reject(new Error("None of the transports supported by the client are supported by the server."))}_constructTransport(e){switch(e){case Ut.WebSockets:if(!this._options.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new Ht(this._httpClient,this._accessTokenFactory,this._logger,this._options.logMessageContent,this._options.WebSocket,this._options.headers||{});case Ut.ServerSentEvents:if(!this._options.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new Ot(this._httpClient,this._httpClient._accessToken,this._logger,this._options);case Ut.LongPolling:return new $t(this._httpClient,this._logger,this._options);default:throw new Error(`Unknown transport: ${e}.`)}}_startTransport(e,t){return this.transport.onreceive=this.onreceive,this.transport.onclose=e=>this._stopConnection(e),this.transport.connect(e,t)}_resolveTransportOrError(e,t,n){const r=Ut[e.transport];if(null==r)return this._logger.log(gt.Debug,`Skipping transport '${e.transport}' because it is not supported by this client.`),new Error(`Skipping transport '${e.transport}' because it is not supported by this client.`);if(!function(e,t){return!e||0!=(t&e)}(t,r))return this._logger.log(gt.Debug,`Skipping transport '${Ut[r]}' because it was disabled by the client.`),new dt(`'${Ut[r]}' is disabled by the client.`,r);if(!(e.transferFormats.map((e=>Lt[e])).indexOf(n)>=0))return this._logger.log(gt.Debug,`Skipping transport '${Ut[r]}' because it does not support the requested transfer format '${Lt[n]}'.`),new Error(`'${Ut[r]}' does not support ${Lt[n]}.`);if(r===Ut.WebSockets&&!this._options.WebSocket||r===Ut.ServerSentEvents&&!this._options.EventSource)return this._logger.log(gt.Debug,`Skipping transport '${Ut[r]}' because it is not supported in your environment.'`),new ht(`'${Ut[r]}' is not supported in your environment.`,r);this._logger.log(gt.Debug,`Selecting transport '${Ut[r]}'.`);try{return this._constructTransport(r)}catch(e){return e}}_isITransport(e){return e&&"object"==typeof e&&"connect"in e}_stopConnection(e){if(this._logger.log(gt.Debug,`HttpConnection.stopConnection(${e}) called while in state ${this._connectionState}.`),this.transport=void 0,e=this._stopError||e,this._stopError=void 0,"Disconnected"!==this._connectionState){if("Connecting"===this._connectionState)throw this._logger.log(gt.Warning,`Call to HttpConnection.stopConnection(${e}) was ignored because the connection is still in the connecting state.`),new Error(`HttpConnection.stopConnection(${e}) was called while the connection is still in the connecting state.`);if("Disconnecting"===this._connectionState&&this._stopPromiseResolver(),e?this._logger.log(gt.Error,`Connection disconnected with error '${e}'.`):this._logger.log(gt.Information,"Connection disconnected."),this._sendQueue&&(this._sendQueue.stop().catch((e=>{this._logger.log(gt.Error,`TransportSendQueue.stop() threw error '${e}'.`)})),this._sendQueue=void 0),this.connectionId=void 0,this._connectionState="Disconnected",this._connectionStarted){this._connectionStarted=!1;try{this.onclose&&this.onclose(e)}catch(t){this._logger.log(gt.Error,`HttpConnection.onclose(${e}) threw error '${t}'.`)}}}else this._logger.log(gt.Debug,`Call to HttpConnection.stopConnection(${e}) was ignored because the connection is already in the disconnected state.`)}_resolveUrl(e){if(0===e.lastIndexOf("https://",0)||0===e.lastIndexOf("http://",0))return e;if(!vt.isBrowser)throw new Error(`Cannot resolve '${e}'.`);const t=window.document.createElement("a");return t.href=e,this._logger.log(gt.Information,`Normalizing '${e}' to '${t.href}'.`),t.href}_resolveNegotiateUrl(e){const t=e.indexOf("?");let n=e.substring(0,-1===t?e.length:t);return"/"!==n[n.length-1]&&(n+="/"),n+="negotiate",n+=-1===t?"":e.substring(t),-1===n.indexOf("negotiateVersion")&&(n+=-1===t?"?":"&",n+="negotiateVersion="+this._negotiateVersion),n}}class Wt{constructor(e){this._transport=e,this._buffer=[],this._executing=!0,this._sendBufferedData=new zt,this._transportResult=new zt,this._sendLoopPromise=this._sendLoop()}send(e){return this._bufferData(e),this._transportResult||(this._transportResult=new zt),this._transportResult.promise}stop(){return this._executing=!1,this._sendBufferedData.resolve(),this._sendLoopPromise}_bufferData(e){if(this._buffer.length&&typeof this._buffer[0]!=typeof e)throw new Error(`Expected data to be of type ${typeof this._buffer} but was of type ${typeof e}`);this._buffer.push(e),this._sendBufferedData.resolve()}async _sendLoop(){for(;;){if(await this._sendBufferedData.promise,!this._executing){this._transportResult&&this._transportResult.reject("Connection stopped.");break}this._sendBufferedData=new zt;const e=this._transportResult;this._transportResult=void 0;const t="string"==typeof this._buffer[0]?this._buffer.join(""):Wt._concatBuffers(this._buffer);this._buffer.length=0;try{await this._transport.send(t),e.resolve()}catch(t){e.reject(t)}}}static _concatBuffers(e){const t=e.map((e=>e.byteLength)).reduce(((e,t)=>e+t)),n=new Uint8Array(t);let r=0;for(const t of e)n.set(new Uint8Array(t),r),r+=t.byteLength;return n.buffer}}class zt{constructor(){this.promise=new Promise(((e,t)=>[this._resolver,this._rejecter]=[e,t]))}resolve(){this._resolver()}reject(e){this._rejecter(e)}}class Jt{static write(e){return`${e}${Jt.RecordSeparator}`}static parse(e){if(e[e.length-1]!==Jt.RecordSeparator)throw new Error("Message is incomplete.");const t=e.split(Jt.RecordSeparator);return t.pop(),t}}Jt.RecordSeparatorCode=30,Jt.RecordSeparator=String.fromCharCode(Jt.RecordSeparatorCode);class qt{writeHandshakeRequest(e){return Jt.write(JSON.stringify(e))}parseHandshakeResponse(e){let t,n;if(_t(e)){const r=new Uint8Array(e),o=r.indexOf(Jt.RecordSeparatorCode);if(-1===o)throw new Error("Message is incomplete.");const i=o+1;t=String.fromCharCode.apply(null,Array.prototype.slice.call(r.slice(0,i))),n=r.byteLength>i?r.slice(i).buffer:null}else{const r=e,o=r.indexOf(Jt.RecordSeparator);if(-1===o)throw new Error("Message is incomplete.");const i=o+1;t=r.substring(0,i),n=r.length>i?r.substring(i):null}const r=Jt.parse(t),o=JSON.parse(r[0]);if(o.type)throw new Error("Expected a handshake response from the server.");return[n,o]}}!function(e){e[e.Invocation=1]="Invocation",e[e.StreamItem=2]="StreamItem",e[e.Completion=3]="Completion",e[e.StreamInvocation=4]="StreamInvocation",e[e.CancelInvocation=5]="CancelInvocation",e[e.Ping=6]="Ping",e[e.Close=7]="Close"}(Mt||(Mt={}));class Vt{constructor(){this.observers=[]}next(e){for(const t of this.observers)t.next(e)}error(e){for(const t of this.observers)t.error&&t.error(e)}complete(){for(const e of this.observers)e.complete&&e.complete()}subscribe(e){return this.observers.push(e),new St(this,e)}}!function(e){e.Disconnected="Disconnected",e.Connecting="Connecting",e.Connected="Connected",e.Disconnecting="Disconnecting",e.Reconnecting="Reconnecting"}(Bt||(Bt={}));class Kt{static create(e,t,n,r,o,i){return new Kt(e,t,n,r,o,i)}constructor(e,t,n,r,o,i){this._nextKeepAlive=0,this._freezeEventListener=()=>{this._logger.log(gt.Warning,"The page is being frozen, this will likely lead to the connection being closed and messages being lost. For more information see the docs at https://learn.microsoft.com/aspnet/core/signalr/javascript-client#bsleep")},wt.isRequired(e,"connection"),wt.isRequired(t,"logger"),wt.isRequired(n,"protocol"),this.serverTimeoutInMilliseconds=null!=o?o:3e4,this.keepAliveIntervalInMilliseconds=null!=i?i:15e3,this._logger=t,this._protocol=n,this.connection=e,this._reconnectPolicy=r,this._handshakeProtocol=new qt,this.connection.onreceive=e=>this._processIncomingData(e),this.connection.onclose=e=>this._connectionClosed(e),this._callbacks={},this._methods={},this._closedCallbacks=[],this._reconnectingCallbacks=[],this._reconnectedCallbacks=[],this._invocationId=0,this._receivedHandshakeResponse=!1,this._connectionState=Bt.Disconnected,this._connectionStarted=!1,this._cachedPingMessage=this._protocol.writeMessage({type:Mt.Ping})}get state(){return this._connectionState}get connectionId(){return this.connection&&this.connection.connectionId||null}get baseUrl(){return this.connection.baseUrl||""}set baseUrl(e){if(this._connectionState!==Bt.Disconnected&&this._connectionState!==Bt.Reconnecting)throw new Error("The HubConnection must be in the Disconnected or Reconnecting state to change the url.");if(!e)throw new Error("The HubConnection url must be a valid url.");this.connection.baseUrl=e}start(){return this._startPromise=this._startWithStateTransitions(),this._startPromise}async _startWithStateTransitions(){if(this._connectionState!==Bt.Disconnected)return Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state."));this._connectionState=Bt.Connecting,this._logger.log(gt.Debug,"Starting HubConnection.");try{await this._startInternal(),vt.isBrowser&&window.document.addEventListener("freeze",this._freezeEventListener),this._connectionState=Bt.Connected,this._connectionStarted=!0,this._logger.log(gt.Debug,"HubConnection connected successfully.")}catch(e){return this._connectionState=Bt.Disconnected,this._logger.log(gt.Debug,`HubConnection failed to start successfully because of error '${e}'.`),Promise.reject(e)}}async _startInternal(){this._stopDuringStartError=void 0,this._receivedHandshakeResponse=!1;const e=new Promise(((e,t)=>{this._handshakeResolver=e,this._handshakeRejecter=t}));await this.connection.start(this._protocol.transferFormat);try{const t={protocol:this._protocol.name,version:this._protocol.version};if(this._logger.log(gt.Debug,"Sending handshake request."),await this._sendMessage(this._handshakeProtocol.writeHandshakeRequest(t)),this._logger.log(gt.Information,`Using HubProtocol '${this._protocol.name}'.`),this._cleanupTimeout(),this._resetTimeoutPeriod(),this._resetKeepAliveInterval(),await e,this._stopDuringStartError)throw this._stopDuringStartError;this.connection.features.inherentKeepAlive||await this._sendMessage(this._cachedPingMessage)}catch(e){throw this._logger.log(gt.Debug,`Hub handshake failed with error '${e}' during start(). Stopping HubConnection.`),this._cleanupTimeout(),this._cleanupPingTimer(),await this.connection.stop(e),e}}async stop(){const e=this._startPromise;this._stopPromise=this._stopInternal(),await this._stopPromise;try{await e}catch(e){}}_stopInternal(e){return this._connectionState===Bt.Disconnected?(this._logger.log(gt.Debug,`Call to HubConnection.stop(${e}) ignored because it is already in the disconnected state.`),Promise.resolve()):this._connectionState===Bt.Disconnecting?(this._logger.log(gt.Debug,`Call to HttpConnection.stop(${e}) ignored because the connection is already in the disconnecting state.`),this._stopPromise):(this._connectionState=Bt.Disconnecting,this._logger.log(gt.Debug,"Stopping HubConnection."),this._reconnectDelayHandle?(this._logger.log(gt.Debug,"Connection stopped during reconnect delay. Done reconnecting."),clearTimeout(this._reconnectDelayHandle),this._reconnectDelayHandle=void 0,this._completeClose(),Promise.resolve()):(this._cleanupTimeout(),this._cleanupPingTimer(),this._stopDuringStartError=e||new lt("The connection was stopped before the hub handshake could complete."),this.connection.stop(e)))}stream(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._createStreamInvocation(e,t,r);let i;const s=new Vt;return s.cancelCallback=()=>{const e=this._createCancelInvocation(o.invocationId);return delete this._callbacks[o.invocationId],i.then((()=>this._sendWithProtocol(e)))},this._callbacks[o.invocationId]=(e,t)=>{t?s.error(t):e&&(e.type===Mt.Completion?e.error?s.error(new Error(e.error)):s.complete():s.next(e.item))},i=this._sendWithProtocol(o).catch((e=>{s.error(e),delete this._callbacks[o.invocationId]})),this._launchStreams(n,i),s}_sendMessage(e){return this._resetKeepAliveInterval(),this.connection.send(e)}_sendWithProtocol(e){return this._sendMessage(this._protocol.writeMessage(e))}send(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._sendWithProtocol(this._createInvocation(e,t,!0,r));return this._launchStreams(n,o),o}invoke(e,...t){const[n,r]=this._replaceStreamingParams(t),o=this._createInvocation(e,t,!1,r);return new Promise(((e,t)=>{this._callbacks[o.invocationId]=(n,r)=>{r?t(r):n&&(n.type===Mt.Completion?n.error?t(new Error(n.error)):e(n.result):t(new Error(`Unexpected message type: ${n.type}`)))};const r=this._sendWithProtocol(o).catch((e=>{t(e),delete this._callbacks[o.invocationId]}));this._launchStreams(n,r)}))}on(e,t){e&&t&&(e=e.toLowerCase(),this._methods[e]||(this._methods[e]=[]),-1===this._methods[e].indexOf(t)&&this._methods[e].push(t))}off(e,t){if(!e)return;e=e.toLowerCase();const n=this._methods[e];if(n)if(t){const r=n.indexOf(t);-1!==r&&(n.splice(r,1),0===n.length&&delete this._methods[e])}else delete this._methods[e]}onclose(e){e&&this._closedCallbacks.push(e)}onreconnecting(e){e&&this._reconnectingCallbacks.push(e)}onreconnected(e){e&&this._reconnectedCallbacks.push(e)}_processIncomingData(e){if(this._cleanupTimeout(),this._receivedHandshakeResponse||(e=this._processHandshakeResponse(e),this._receivedHandshakeResponse=!0),e){const t=this._protocol.parseMessages(e,this._logger);for(const e of t)switch(e.type){case Mt.Invocation:this._invokeClientMethod(e);break;case Mt.StreamItem:case Mt.Completion:{const t=this._callbacks[e.invocationId];if(t){e.type===Mt.Completion&&delete this._callbacks[e.invocationId];try{t(e)}catch(e){this._logger.log(gt.Error,`Stream callback threw error: ${xt(e)}`)}}break}case Mt.Ping:break;case Mt.Close:{this._logger.log(gt.Information,"Close message received from server.");const t=e.error?new Error("Server returned an error on close: "+e.error):void 0;!0===e.allowReconnect?this.connection.stop(t):this._stopPromise=this._stopInternal(t);break}default:this._logger.log(gt.Warning,`Invalid message type: ${e.type}.`)}}this._resetTimeoutPeriod()}_processHandshakeResponse(e){let t,n;try{[n,t]=this._handshakeProtocol.parseHandshakeResponse(e)}catch(e){const t="Error parsing handshake response: "+e;this._logger.log(gt.Error,t);const n=new Error(t);throw this._handshakeRejecter(n),n}if(t.error){const e="Server returned handshake error: "+t.error;this._logger.log(gt.Error,e);const n=new Error(e);throw this._handshakeRejecter(n),n}return this._logger.log(gt.Debug,"Server handshake complete."),this._handshakeResolver(),n}_resetKeepAliveInterval(){this.connection.features.inherentKeepAlive||(this._nextKeepAlive=(new Date).getTime()+this.keepAliveIntervalInMilliseconds,this._cleanupPingTimer())}_resetTimeoutPeriod(){if(!(this.connection.features&&this.connection.features.inherentKeepAlive||(this._timeoutHandle=setTimeout((()=>this.serverTimeout()),this.serverTimeoutInMilliseconds),void 0!==this._pingServerHandle))){let e=this._nextKeepAlive-(new Date).getTime();e<0&&(e=0),this._pingServerHandle=setTimeout((async()=>{if(this._connectionState===Bt.Connected)try{await this._sendMessage(this._cachedPingMessage)}catch{this._cleanupPingTimer()}}),e)}}serverTimeout(){this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server."))}async _invokeClientMethod(e){const t=e.target.toLowerCase(),n=this._methods[t];if(!n)return this._logger.log(gt.Warning,`No client method with the name '${t}' found.`),void(e.invocationId&&(this._logger.log(gt.Warning,`No result given for '${t}' method and invocation ID '${e.invocationId}'.`),await this._sendWithProtocol(this._createCompletionMessage(e.invocationId,"Client didn't provide a result.",null))));const r=n.slice(),o=!!e.invocationId;let i,s,a;for(const n of r)try{const r=i;i=await n.apply(this,e.arguments),o&&i&&r&&(this._logger.log(gt.Error,`Multiple results provided for '${t}'. Sending error to server.`),a=this._createCompletionMessage(e.invocationId,"Client provided multiple results.",null)),s=void 0}catch(e){s=e,this._logger.log(gt.Error,`A callback for the method '${t}' threw error '${e}'.`)}a?await this._sendWithProtocol(a):o?(s?a=this._createCompletionMessage(e.invocationId,`${s}`,null):void 0!==i?a=this._createCompletionMessage(e.invocationId,null,i):(this._logger.log(gt.Warning,`No result given for '${t}' method and invocation ID '${e.invocationId}'.`),a=this._createCompletionMessage(e.invocationId,"Client didn't provide a result.",null)),await this._sendWithProtocol(a)):i&&this._logger.log(gt.Error,`Result given for '${t}' method but server is not expecting a result.`)}_connectionClosed(e){this._logger.log(gt.Debug,`HubConnection.connectionClosed(${e}) called while in state ${this._connectionState}.`),this._stopDuringStartError=this._stopDuringStartError||e||new lt("The underlying connection was closed before the hub handshake could complete."),this._handshakeResolver&&this._handshakeResolver(),this._cancelCallbacksWithError(e||new Error("Invocation canceled due to the underlying connection being closed.")),this._cleanupTimeout(),this._cleanupPingTimer(),this._connectionState===Bt.Disconnecting?this._completeClose(e):this._connectionState===Bt.Connected&&this._reconnectPolicy?this._reconnect(e):this._connectionState===Bt.Connected&&this._completeClose(e)}_completeClose(e){if(this._connectionStarted){this._connectionState=Bt.Disconnected,this._connectionStarted=!1,vt.isBrowser&&window.document.removeEventListener("freeze",this._freezeEventListener);try{this._closedCallbacks.forEach((t=>t.apply(this,[e])))}catch(t){this._logger.log(gt.Error,`An onclose callback called with error '${e}' threw error '${t}'.`)}}}async _reconnect(e){const t=Date.now();let n=0,r=void 0!==e?e:new Error("Attempting to reconnect due to a unknown error."),o=this._getNextRetryDelay(n++,0,r);if(null===o)return this._logger.log(gt.Debug,"Connection not reconnecting because the IRetryPolicy returned null on the first reconnect attempt."),void this._completeClose(e);if(this._connectionState=Bt.Reconnecting,e?this._logger.log(gt.Information,`Connection reconnecting because of error '${e}'.`):this._logger.log(gt.Information,"Connection reconnecting."),0!==this._reconnectingCallbacks.length){try{this._reconnectingCallbacks.forEach((t=>t.apply(this,[e])))}catch(t){this._logger.log(gt.Error,`An onreconnecting callback called with error '${e}' threw error '${t}'.`)}if(this._connectionState!==Bt.Reconnecting)return void this._logger.log(gt.Debug,"Connection left the reconnecting state in onreconnecting callback. Done reconnecting.")}for(;null!==o;){if(this._logger.log(gt.Information,`Reconnect attempt number ${n} will start in ${o} ms.`),await new Promise((e=>{this._reconnectDelayHandle=setTimeout(e,o)})),this._reconnectDelayHandle=void 0,this._connectionState!==Bt.Reconnecting)return void this._logger.log(gt.Debug,"Connection left the reconnecting state during reconnect delay. Done reconnecting.");try{if(await this._startInternal(),this._connectionState=Bt.Connected,this._logger.log(gt.Information,"HubConnection reconnected successfully."),0!==this._reconnectedCallbacks.length)try{this._reconnectedCallbacks.forEach((e=>e.apply(this,[this.connection.connectionId])))}catch(e){this._logger.log(gt.Error,`An onreconnected callback called with connectionId '${this.connection.connectionId}; threw error '${e}'.`)}return}catch(e){if(this._logger.log(gt.Information,`Reconnect attempt failed because of error '${e}'.`),this._connectionState!==Bt.Reconnecting)return this._logger.log(gt.Debug,`Connection moved to the '${this._connectionState}' from the reconnecting state during reconnect attempt. Done reconnecting.`),void(this._connectionState===Bt.Disconnecting&&this._completeClose());r=e instanceof Error?e:new Error(e.toString()),o=this._getNextRetryDelay(n++,Date.now()-t,r)}}this._logger.log(gt.Information,`Reconnect retries have been exhausted after ${Date.now()-t} ms and ${n} failed attempts. Connection disconnecting.`),this._completeClose()}_getNextRetryDelay(e,t,n){try{return this._reconnectPolicy.nextRetryDelayInMilliseconds({elapsedMilliseconds:t,previousRetryCount:e,retryReason:n})}catch(n){return this._logger.log(gt.Error,`IRetryPolicy.nextRetryDelayInMilliseconds(${e}, ${t}) threw error '${n}'.`),null}}_cancelCallbacksWithError(e){const t=this._callbacks;this._callbacks={},Object.keys(t).forEach((n=>{const r=t[n];try{r(null,e)}catch(t){this._logger.log(gt.Error,`Stream 'error' callback called with '${e}' threw error: ${xt(t)}`)}}))}_cleanupPingTimer(){this._pingServerHandle&&(clearTimeout(this._pingServerHandle),this._pingServerHandle=void 0)}_cleanupTimeout(){this._timeoutHandle&&clearTimeout(this._timeoutHandle)}_createInvocation(e,t,n,r){if(n)return 0!==r.length?{arguments:t,streamIds:r,target:e,type:Mt.Invocation}:{arguments:t,target:e,type:Mt.Invocation};{const n=this._invocationId;return this._invocationId++,0!==r.length?{arguments:t,invocationId:n.toString(),streamIds:r,target:e,type:Mt.Invocation}:{arguments:t,invocationId:n.toString(),target:e,type:Mt.Invocation}}}_launchStreams(e,t){if(0!==e.length){t||(t=Promise.resolve());for(const n in e)e[n].subscribe({complete:()=>{t=t.then((()=>this._sendWithProtocol(this._createCompletionMessage(n))))},error:e=>{let r;r=e instanceof Error?e.message:e&&e.toString?e.toString():"Unknown error",t=t.then((()=>this._sendWithProtocol(this._createCompletionMessage(n,r))))},next:e=>{t=t.then((()=>this._sendWithProtocol(this._createStreamItemMessage(n,e))))}})}}_replaceStreamingParams(e){const t=[],n=[];for(let r=0;r=55296&&o<=56319&&r65535&&(h-=65536,i.push(h>>>10&1023|55296),h=56320|1023&h),i.push(h)}else i.push(a);i.length>=hn&&(s+=String.fromCharCode.apply(String,i),i.length=0)}return i.length>0&&(s+=String.fromCharCode.apply(String,i)),s}var un,pn=on?new TextDecoder:null,fn=on?"undefined"!=typeof process&&"force"!==(null===(en=null===process||void 0===process?void 0:process.env)||void 0===en?void 0:en.TEXT_DECODER)?200:0:tn,gn=function(e,t){this.type=e,this.data=t},mn=(un=function(e,t){return un=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},un(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}un(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),yn=function(e){function t(n){var r=e.call(this,n)||this,o=Object.create(t.prototype);return Object.setPrototypeOf(r,o),Object.defineProperty(r,"name",{configurable:!0,enumerable:!1,value:t.name}),r}return mn(t,e),t}(Error),wn={type:-1,encode:function(e){var t,n,r,o;return e instanceof Date?function(e){var t,n=e.sec,r=e.nsec;if(n>=0&&r>=0&&n<=17179869183){if(0===r&&n<=4294967295){var o=new Uint8Array(4);return(t=new DataView(o.buffer)).setUint32(0,n),o}var i=n/4294967296,s=4294967295&n;return o=new Uint8Array(8),(t=new DataView(o.buffer)).setUint32(0,r<<2|3&i),t.setUint32(4,s),o}return o=new Uint8Array(12),(t=new DataView(o.buffer)).setUint32(0,r),nn(t,4,n),o}((r=1e6*((t=e.getTime())-1e3*(n=Math.floor(t/1e3))),{sec:n+(o=Math.floor(r/1e9)),nsec:r-1e9*o})):null},decode:function(e){var t=function(e){var t=new DataView(e.buffer,e.byteOffset,e.byteLength);switch(e.byteLength){case 4:return{sec:t.getUint32(0),nsec:0};case 8:var n=t.getUint32(0);return{sec:4294967296*(3&n)+t.getUint32(4),nsec:n>>>2};case 12:return{sec:rn(t,4),nsec:t.getUint32(0)};default:throw new yn("Unrecognized data size for timestamp (expected 4, 8, or 12): ".concat(e.length))}}(e);return new Date(1e3*t.sec+t.nsec/1e6)}},vn=function(){function e(){this.builtInEncoders=[],this.builtInDecoders=[],this.encoders=[],this.decoders=[],this.register(wn)}return e.prototype.register=function(e){var t=e.type,n=e.encode,r=e.decode;if(t>=0)this.encoders[t]=n,this.decoders[t]=r;else{var o=1+t;this.builtInEncoders[o]=n,this.builtInDecoders[o]=r}},e.prototype.tryToEncode=function(e,t){for(var n=0;nthis.maxDepth)throw new Error("Too deep objects in depth ".concat(t));null==e?this.encodeNil():"boolean"==typeof e?this.encodeBoolean(e):"number"==typeof e?this.encodeNumber(e):"string"==typeof e?this.encodeString(e):this.encodeObject(e,t)},e.prototype.ensureBufferSizeToWrite=function(e){var t=this.pos+e;this.view.byteLength=0?e<128?this.writeU8(e):e<256?(this.writeU8(204),this.writeU8(e)):e<65536?(this.writeU8(205),this.writeU16(e)):e<4294967296?(this.writeU8(206),this.writeU32(e)):(this.writeU8(207),this.writeU64(e)):e>=-32?this.writeU8(224|e+32):e>=-128?(this.writeU8(208),this.writeI8(e)):e>=-32768?(this.writeU8(209),this.writeI16(e)):e>=-2147483648?(this.writeU8(210),this.writeI32(e)):(this.writeU8(211),this.writeI64(e)):this.forceFloat32?(this.writeU8(202),this.writeF32(e)):(this.writeU8(203),this.writeF64(e))},e.prototype.writeStringHeader=function(e){if(e<32)this.writeU8(160+e);else if(e<256)this.writeU8(217),this.writeU8(e);else if(e<65536)this.writeU8(218),this.writeU16(e);else{if(!(e<4294967296))throw new Error("Too long string: ".concat(e," bytes in UTF-8"));this.writeU8(219),this.writeU32(e)}},e.prototype.encodeString=function(e){if(e.length>cn){var t=sn(e);this.ensureBufferSizeToWrite(5+t),this.writeStringHeader(t),ln(e,this.bytes,this.pos),this.pos+=t}else t=sn(e),this.ensureBufferSizeToWrite(5+t),this.writeStringHeader(t),function(e,t,n){for(var r=e.length,o=n,i=0;i>6&31|192;else{if(s>=55296&&s<=56319&&i>12&15|224,t[o++]=s>>6&63|128):(t[o++]=s>>18&7|240,t[o++]=s>>12&63|128,t[o++]=s>>6&63|128)}t[o++]=63&s|128}else t[o++]=s}}(e,this.bytes,this.pos),this.pos+=t},e.prototype.encodeObject=function(e,t){var n=this.extensionCodec.tryToEncode(e,this.context);if(null!=n)this.encodeExtension(n);else if(Array.isArray(e))this.encodeArray(e,t);else if(ArrayBuffer.isView(e))this.encodeBinary(e);else{if("object"!=typeof e)throw new Error("Unrecognized object: ".concat(Object.prototype.toString.apply(e)));this.encodeMap(e,t)}},e.prototype.encodeBinary=function(e){var t=e.byteLength;if(t<256)this.writeU8(196),this.writeU8(t);else if(t<65536)this.writeU8(197),this.writeU16(t);else{if(!(t<4294967296))throw new Error("Too large binary: ".concat(t));this.writeU8(198),this.writeU32(t)}var n=bn(e);this.writeU8a(n)},e.prototype.encodeArray=function(e,t){var n=e.length;if(n<16)this.writeU8(144+n);else if(n<65536)this.writeU8(220),this.writeU16(n);else{if(!(n<4294967296))throw new Error("Too large array: ".concat(n));this.writeU8(221),this.writeU32(n)}for(var r=0,o=e;r0&&e<=this.maxKeyLength},e.prototype.find=function(e,t,n){e:for(var r=0,o=this.caches[n-1];r=this.maxLengthPerKey?n[Math.random()*n.length|0]=r:n.push(r)},e.prototype.decode=function(e,t,n){var r=this.find(e,t,n);if(null!=r)return this.hit++,r;this.miss++;var o=dn(e,t,n),i=Uint8Array.prototype.slice.call(e,t,t+n);return this.store(i,o),o},e}(),Dn=function(e,t){var n,r,o,i,s={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:a(0),throw:a(1),return:a(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function a(i){return function(a){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return s.label++,{value:i[1],done:!1};case 5:s.label++,r=i[1],i=[0];continue;case 7:i=s.ops.pop(),s.trys.pop();continue;default:if(!((o=(o=s.trys).length>0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]1||a(e,t)}))})}function a(e,t){try{(n=o[e](t)).value instanceof An?Promise.resolve(n.value.v).then(c,l):h(i[0][2],n)}catch(e){h(i[0][3],e)}var n}function c(e){a("next",e)}function l(e){a("throw",e)}function h(e,t){e(t),i.shift(),i.length&&a(i[0][0],i[0][1])}},Nn=-1,Pn=new DataView(new ArrayBuffer(0)),Un=new Uint8Array(Pn.buffer),Ln=function(){try{Pn.getInt8(0)}catch(e){return e.constructor}throw new Error("never reached")}(),Mn=new Ln("Insufficient data"),Bn=new Tn,Fn=function(){function e(e,t,n,r,o,i,s,a){void 0===e&&(e=vn.defaultCodec),void 0===t&&(t=void 0),void 0===n&&(n=tn),void 0===r&&(r=tn),void 0===o&&(o=tn),void 0===i&&(i=tn),void 0===s&&(s=tn),void 0===a&&(a=Bn),this.extensionCodec=e,this.context=t,this.maxStrLength=n,this.maxBinLength=r,this.maxArrayLength=o,this.maxMapLength=i,this.maxExtLength=s,this.keyDecoder=a,this.totalPos=0,this.pos=0,this.view=Pn,this.bytes=Un,this.headByte=Nn,this.stack=[]}return e.prototype.reinitializeState=function(){this.totalPos=0,this.headByte=Nn,this.stack.length=0},e.prototype.setBuffer=function(e){this.bytes=bn(e),this.view=function(e){if(e instanceof ArrayBuffer)return new DataView(e);var t=bn(e);return new DataView(t.buffer,t.byteOffset,t.byteLength)}(this.bytes),this.pos=0},e.prototype.appendBuffer=function(e){if(this.headByte!==Nn||this.hasRemaining(1)){var t=this.bytes.subarray(this.pos),n=bn(e),r=new Uint8Array(t.length+n.length);r.set(t),r.set(n,t.length),this.setBuffer(r)}else this.setBuffer(e)},e.prototype.hasRemaining=function(e){return this.view.byteLength-this.pos>=e},e.prototype.createExtraByteError=function(e){var t=this.view,n=this.pos;return new RangeError("Extra ".concat(t.byteLength-n," of ").concat(t.byteLength," byte(s) found at buffer[").concat(e,"]"))},e.prototype.decode=function(e){this.reinitializeState(),this.setBuffer(e);var t=this.doDecodeSync();if(this.hasRemaining(1))throw this.createExtraByteError(this.pos);return t},e.prototype.decodeMulti=function(e){return Dn(this,(function(t){switch(t.label){case 0:this.reinitializeState(),this.setBuffer(e),t.label=1;case 1:return this.hasRemaining(1)?[4,this.doDecodeSync()]:[3,3];case 2:return t.sent(),[3,1];case 3:return[2]}}))},e.prototype.decodeAsync=function(e){var t,n,r,o,i,s,a;return i=this,void 0,a=function(){var i,s,a,c,l,h,d,u;return Dn(this,(function(p){switch(p.label){case 0:i=!1,p.label=1;case 1:p.trys.push([1,6,7,12]),t=xn(e),p.label=2;case 2:return[4,t.next()];case 3:if((n=p.sent()).done)return[3,5];if(a=n.value,i)throw this.createExtraByteError(this.totalPos);this.appendBuffer(a);try{s=this.doDecodeSync(),i=!0}catch(e){if(!(e instanceof Ln))throw e}this.totalPos+=this.pos,p.label=4;case 4:return[3,2];case 5:return[3,12];case 6:return c=p.sent(),r={error:c},[3,12];case 7:return p.trys.push([7,,10,11]),n&&!n.done&&(o=t.return)?[4,o.call(t)]:[3,9];case 8:p.sent(),p.label=9;case 9:return[3,11];case 10:if(r)throw r.error;return[7];case 11:return[7];case 12:if(i){if(this.hasRemaining(1))throw this.createExtraByteError(this.totalPos);return[2,s]}throw h=(l=this).headByte,d=l.pos,u=l.totalPos,new RangeError("Insufficient data in parsing ".concat(Cn(h)," at ").concat(u," (").concat(d," in the current buffer)"))}}))},new((s=void 0)||(s=Promise))((function(e,t){function n(e){try{o(a.next(e))}catch(e){t(e)}}function r(e){try{o(a.throw(e))}catch(e){t(e)}}function o(t){var o;t.done?e(t.value):(o=t.value,o instanceof s?o:new s((function(e){e(o)}))).then(n,r)}o((a=a.apply(i,[])).next())}))},e.prototype.decodeArrayStream=function(e){return this.decodeMultiAsync(e,!0)},e.prototype.decodeStream=function(e){return this.decodeMultiAsync(e,!1)},e.prototype.decodeMultiAsync=function(e,t){return Rn(this,arguments,(function(){var n,r,o,i,s,a,c,l,h;return Dn(this,(function(d){switch(d.label){case 0:n=t,r=-1,d.label=1;case 1:d.trys.push([1,13,14,19]),o=xn(e),d.label=2;case 2:return[4,An(o.next())];case 3:if((i=d.sent()).done)return[3,12];if(s=i.value,t&&0===r)throw this.createExtraByteError(this.totalPos);this.appendBuffer(s),n&&(r=this.readArraySize(),n=!1,this.complete()),d.label=4;case 4:d.trys.push([4,9,,10]),d.label=5;case 5:return[4,An(this.doDecodeSync())];case 6:return[4,d.sent()];case 7:return d.sent(),0==--r?[3,8]:[3,5];case 8:return[3,10];case 9:if(!((a=d.sent())instanceof Ln))throw a;return[3,10];case 10:this.totalPos+=this.pos,d.label=11;case 11:return[3,2];case 12:return[3,19];case 13:return c=d.sent(),l={error:c},[3,19];case 14:return d.trys.push([14,,17,18]),i&&!i.done&&(h=o.return)?[4,An(h.call(o))]:[3,16];case 15:d.sent(),d.label=16;case 16:return[3,18];case 17:if(l)throw l.error;return[7];case 18:return[7];case 19:return[2]}}))}))},e.prototype.doDecodeSync=function(){e:for(;;){var e=this.readHeadByte(),t=void 0;if(e>=224)t=e-256;else if(e<192)if(e<128)t=e;else if(e<144){if(0!=(r=e-128)){this.pushMapState(r),this.complete();continue e}t={}}else if(e<160){if(0!=(r=e-144)){this.pushArrayState(r),this.complete();continue e}t=[]}else{var n=e-160;t=this.decodeUtf8String(n,0)}else if(192===e)t=null;else if(194===e)t=!1;else if(195===e)t=!0;else if(202===e)t=this.readF32();else if(203===e)t=this.readF64();else if(204===e)t=this.readU8();else if(205===e)t=this.readU16();else if(206===e)t=this.readU32();else if(207===e)t=this.readU64();else if(208===e)t=this.readI8();else if(209===e)t=this.readI16();else if(210===e)t=this.readI32();else if(211===e)t=this.readI64();else if(217===e)n=this.lookU8(),t=this.decodeUtf8String(n,1);else if(218===e)n=this.lookU16(),t=this.decodeUtf8String(n,2);else if(219===e)n=this.lookU32(),t=this.decodeUtf8String(n,4);else if(220===e){if(0!==(r=this.readU16())){this.pushArrayState(r),this.complete();continue e}t=[]}else if(221===e){if(0!==(r=this.readU32())){this.pushArrayState(r),this.complete();continue e}t=[]}else if(222===e){if(0!==(r=this.readU16())){this.pushMapState(r),this.complete();continue e}t={}}else if(223===e){if(0!==(r=this.readU32())){this.pushMapState(r),this.complete();continue e}t={}}else if(196===e){var r=this.lookU8();t=this.decodeBinary(r,1)}else if(197===e)r=this.lookU16(),t=this.decodeBinary(r,2);else if(198===e)r=this.lookU32(),t=this.decodeBinary(r,4);else if(212===e)t=this.decodeExtension(1,0);else if(213===e)t=this.decodeExtension(2,0);else if(214===e)t=this.decodeExtension(4,0);else if(215===e)t=this.decodeExtension(8,0);else if(216===e)t=this.decodeExtension(16,0);else if(199===e)r=this.lookU8(),t=this.decodeExtension(r,1);else if(200===e)r=this.lookU16(),t=this.decodeExtension(r,2);else{if(201!==e)throw new yn("Unrecognized type byte: ".concat(Cn(e)));r=this.lookU32(),t=this.decodeExtension(r,4)}this.complete();for(var o=this.stack;o.length>0;){var i=o[o.length-1];if(0===i.type){if(i.array[i.position]=t,i.position++,i.position!==i.size)continue e;o.pop(),t=i.array}else{if(1===i.type){if("string"!=(s=typeof t)&&"number"!==s)throw new yn("The type of key must be string or number but "+typeof t);if("__proto__"===t)throw new yn("The key __proto__ is not allowed");i.key=t,i.type=2;continue e}if(i.map[i.key]=t,i.readCount++,i.readCount!==i.size){i.key=null,i.type=1;continue e}o.pop(),t=i.map}}return t}var s},e.prototype.readHeadByte=function(){return this.headByte===Nn&&(this.headByte=this.readU8()),this.headByte},e.prototype.complete=function(){this.headByte=Nn},e.prototype.readArraySize=function(){var e=this.readHeadByte();switch(e){case 220:return this.readU16();case 221:return this.readU32();default:if(e<160)return e-144;throw new yn("Unrecognized array type byte: ".concat(Cn(e)))}},e.prototype.pushMapState=function(e){if(e>this.maxMapLength)throw new yn("Max length exceeded: map length (".concat(e,") > maxMapLengthLength (").concat(this.maxMapLength,")"));this.stack.push({type:1,size:e,key:null,readCount:0,map:{}})},e.prototype.pushArrayState=function(e){if(e>this.maxArrayLength)throw new yn("Max length exceeded: array length (".concat(e,") > maxArrayLength (").concat(this.maxArrayLength,")"));this.stack.push({type:0,size:e,array:new Array(e),position:0})},e.prototype.decodeUtf8String=function(e,t){var n;if(e>this.maxStrLength)throw new yn("Max length exceeded: UTF-8 byte length (".concat(e,") > maxStrLength (").concat(this.maxStrLength,")"));if(this.bytes.byteLengthfn?function(e,t,n){var r=e.subarray(t,t+n);return pn.decode(r)}(this.bytes,o,e):dn(this.bytes,o,e),this.pos+=t+e,r},e.prototype.stateIsMapKey=function(){return this.stack.length>0&&1===this.stack[this.stack.length-1].type},e.prototype.decodeBinary=function(e,t){if(e>this.maxBinLength)throw new yn("Max length exceeded: bin length (".concat(e,") > maxBinLength (").concat(this.maxBinLength,")"));if(!this.hasRemaining(e+t))throw Mn;var n=this.pos+t,r=this.bytes.subarray(n,n+e);return this.pos+=t+e,r},e.prototype.decodeExtension=function(e,t){if(e>this.maxExtLength)throw new yn("Max length exceeded: ext length (".concat(e,") > maxExtLength (").concat(this.maxExtLength,")"));var n=this.view.getInt8(this.pos+t),r=this.decodeBinary(e,t+1);return this.extensionCodec.decode(r,n,this.context)},e.prototype.lookU8=function(){return this.view.getUint8(this.pos)},e.prototype.lookU16=function(){return this.view.getUint16(this.pos)},e.prototype.lookU32=function(){return this.view.getUint32(this.pos)},e.prototype.readU8=function(){var e=this.view.getUint8(this.pos);return this.pos++,e},e.prototype.readI8=function(){var e=this.view.getInt8(this.pos);return this.pos++,e},e.prototype.readU16=function(){var e=this.view.getUint16(this.pos);return this.pos+=2,e},e.prototype.readI16=function(){var e=this.view.getInt16(this.pos);return this.pos+=2,e},e.prototype.readU32=function(){var e=this.view.getUint32(this.pos);return this.pos+=4,e},e.prototype.readI32=function(){var e=this.view.getInt32(this.pos);return this.pos+=4,e},e.prototype.readU64=function(){var e,t,n=(e=this.view,t=this.pos,4294967296*e.getUint32(t)+e.getUint32(t+4));return this.pos+=8,n},e.prototype.readI64=function(){var e=rn(this.view,this.pos);return this.pos+=8,e},e.prototype.readF32=function(){var e=this.view.getFloat32(this.pos);return this.pos+=4,e},e.prototype.readF64=function(){var e=this.view.getFloat64(this.pos);return this.pos+=8,e},e}();class $n{static write(e){let t=e.byteLength||e.length;const n=[];do{let e=127&t;t>>=7,t>0&&(e|=128),n.push(e)}while(t>0);t=e.byteLength||e.length;const r=new Uint8Array(n.length+t);return r.set(n,0),r.set(e,n.length),r.buffer}static parse(e){const t=[],n=new Uint8Array(e),r=[0,7,14,21,28];for(let o=0;o7)throw new Error("Messages bigger than 2GB are not supported.");if(!(n.byteLength>=o+s+a))throw new Error("Incomplete message.");t.push(n.slice?n.slice(o+s,o+s+a):n.subarray(o+s,o+s+a)),o=o+s+a}return t}}const On=new Uint8Array([145,Mt.Ping]);class Hn{constructor(e){this.name="messagepack",this.version=1,this.transferFormat=Lt.Binary,this._errorResult=1,this._voidResult=2,this._nonVoidResult=3,e=e||{},this._encoder=new Sn(e.extensionCodec,e.context,e.maxDepth,e.initialBufferSize,e.sortKeys,e.forceFloat32,e.ignoreUndefined,e.forceIntegerToFloat),this._decoder=new Fn(e.extensionCodec,e.context,e.maxStrLength,e.maxBinLength,e.maxArrayLength,e.maxMapLength,e.maxExtLength)}parseMessages(e,t){if(!(n=e)||"undefined"==typeof ArrayBuffer||!(n instanceof ArrayBuffer||n.constructor&&"ArrayBuffer"===n.constructor.name))throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer.");var n;null===t&&(t=mt.instance);const r=$n.parse(e),o=[];for(const e of r){const n=this._parseMessage(e,t);n&&o.push(n)}return o}writeMessage(e){switch(e.type){case Mt.Invocation:return this._writeInvocation(e);case Mt.StreamInvocation:return this._writeStreamInvocation(e);case Mt.StreamItem:return this._writeStreamItem(e);case Mt.Completion:return this._writeCompletion(e);case Mt.Ping:return $n.write(On);case Mt.CancelInvocation:return this._writeCancelInvocation(e);default:throw new Error("Invalid message type.")}}_parseMessage(e,t){if(0===e.length)throw new Error("Invalid payload.");const n=this._decoder.decode(e);if(0===n.length||!(n instanceof Array))throw new Error("Invalid payload.");const r=n[0];switch(r){case Mt.Invocation:return this._createInvocationMessage(this._readHeaders(n),n);case Mt.StreamItem:return this._createStreamItemMessage(this._readHeaders(n),n);case Mt.Completion:return this._createCompletionMessage(this._readHeaders(n),n);case Mt.Ping:return this._createPingMessage(n);case Mt.Close:return this._createCloseMessage(n);default:return t.log(gt.Information,"Unknown message type '"+r+"' ignored."),null}}_createCloseMessage(e){if(e.length<2)throw new Error("Invalid payload for Close message.");return{allowReconnect:e.length>=3?e[2]:void 0,error:e[1],type:Mt.Close}}_createPingMessage(e){if(e.length<1)throw new Error("Invalid payload for Ping message.");return{type:Mt.Ping}}_createInvocationMessage(e,t){if(t.length<5)throw new Error("Invalid payload for Invocation message.");const n=t[2];return n?{arguments:t[4],headers:e,invocationId:n,streamIds:[],target:t[3],type:Mt.Invocation}:{arguments:t[4],headers:e,streamIds:[],target:t[3],type:Mt.Invocation}}_createStreamItemMessage(e,t){if(t.length<4)throw new Error("Invalid payload for StreamItem message.");return{headers:e,invocationId:t[2],item:t[3],type:Mt.StreamItem}}_createCompletionMessage(e,t){if(t.length<4)throw new Error("Invalid payload for Completion message.");const n=t[3];if(n!==this._voidResult&&t.length<5)throw new Error("Invalid payload for Completion message.");let r,o;switch(n){case this._errorResult:r=t[4];break;case this._nonVoidResult:o=t[4]}return{error:r,headers:e,invocationId:t[2],result:o,type:Mt.Completion}}_writeInvocation(e){let t;return t=e.streamIds?this._encoder.encode([Mt.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments,e.streamIds]):this._encoder.encode([Mt.Invocation,e.headers||{},e.invocationId||null,e.target,e.arguments]),$n.write(t.slice())}_writeStreamInvocation(e){let t;return t=e.streamIds?this._encoder.encode([Mt.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments,e.streamIds]):this._encoder.encode([Mt.StreamInvocation,e.headers||{},e.invocationId,e.target,e.arguments]),$n.write(t.slice())}_writeStreamItem(e){const t=this._encoder.encode([Mt.StreamItem,e.headers||{},e.invocationId,e.item]);return $n.write(t.slice())}_writeCompletion(e){const t=e.error?this._errorResult:void 0!==e.result?this._nonVoidResult:this._voidResult;let n;switch(t){case this._errorResult:n=this._encoder.encode([Mt.Completion,e.headers||{},e.invocationId,t,e.error]);break;case this._voidResult:n=this._encoder.encode([Mt.Completion,e.headers||{},e.invocationId,t]);break;case this._nonVoidResult:n=this._encoder.encode([Mt.Completion,e.headers||{},e.invocationId,t,e.result])}return $n.write(n.slice())}_writeCancelInvocation(e){const t=this._encoder.encode([Mt.CancelInvocation,e.headers||{},e.invocationId]);return $n.write(t.slice())}_readHeaders(e){const t=e[1];if("object"!=typeof t)throw new Error("Invalid headers.");return t}}let jn=!1;function Wn(){const e=document.querySelector("#blazor-error-ui");e&&(e.style.display="block"),jn||(jn=!0,document.querySelectorAll("#blazor-error-ui .reload").forEach((e=>{e.onclick=function(e){location.reload(),e.preventDefault()}})),document.querySelectorAll("#blazor-error-ui .dismiss").forEach((e=>{e.onclick=function(e){const t=document.querySelector("#blazor-error-ui");t&&(t.style.display="none"),e.preventDefault()}})))}const zn="function"==typeof TextDecoder?new TextDecoder("utf-8"):null,Jn=zn?zn.decode.bind(zn):function(e){let t=0;const n=e.length,r=[],o=[];for(;t65535&&(o-=65536,r.push(o>>>10&1023|55296),o=56320|1023&o),r.push(o)}r.length>1024&&(o.push(String.fromCharCode.apply(null,r)),r.length=0)}return o.push(String.fromCharCode.apply(null,r)),o.join("")},qn=Math.pow(2,32),Vn=Math.pow(2,21)-1;function Kn(e,t){return e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24}function Xn(e,t){return e[t]+(e[t+1]<<8)+(e[t+2]<<16)+(e[t+3]<<24>>>0)}function Gn(e,t){const n=Xn(e,t+4);if(n>Vn)throw new Error(`Cannot read uint64 with high order part ${n}, because the result would exceed Number.MAX_SAFE_INTEGER.`);return n*qn+Xn(e,t)}class Yn{constructor(e){this.batchData=e;const t=new tr(e);this.arrayRangeReader=new nr(e),this.arrayBuilderSegmentReader=new rr(e),this.diffReader=new Qn(e),this.editReader=new Zn(e,t),this.frameReader=new er(e,t)}updatedComponents(){return Kn(this.batchData,this.batchData.length-20)}referenceFrames(){return Kn(this.batchData,this.batchData.length-16)}disposedComponentIds(){return Kn(this.batchData,this.batchData.length-12)}disposedEventHandlerIds(){return Kn(this.batchData,this.batchData.length-8)}updatedComponentsEntry(e,t){const n=e+4*t;return Kn(this.batchData,n)}referenceFramesEntry(e,t){return e+20*t}disposedComponentIdsEntry(e,t){const n=e+4*t;return Kn(this.batchData,n)}disposedEventHandlerIdsEntry(e,t){const n=e+8*t;return Gn(this.batchData,n)}}class Qn{constructor(e){this.batchDataUint8=e}componentId(e){return Kn(this.batchDataUint8,e)}edits(e){return e+4}editsEntry(e,t){return e+16*t}}class Zn{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}editType(e){return Kn(this.batchDataUint8,e)}siblingIndex(e){return Kn(this.batchDataUint8,e+4)}newTreeIndex(e){return Kn(this.batchDataUint8,e+8)}moveToSiblingIndex(e){return Kn(this.batchDataUint8,e+8)}removedAttributeName(e){const t=Kn(this.batchDataUint8,e+12);return this.stringReader.readString(t)}}class er{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}frameType(e){return Kn(this.batchDataUint8,e)}subtreeLength(e){return Kn(this.batchDataUint8,e+4)}elementReferenceCaptureId(e){const t=Kn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}componentId(e){return Kn(this.batchDataUint8,e+8)}elementName(e){const t=Kn(this.batchDataUint8,e+8);return this.stringReader.readString(t)}textContent(e){const t=Kn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}markupContent(e){const t=Kn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeName(e){const t=Kn(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeValue(e){const t=Kn(this.batchDataUint8,e+8);return this.stringReader.readString(t)}attributeEventHandlerId(e){return Gn(this.batchDataUint8,e+12)}}class tr{constructor(e){this.batchDataUint8=e,this.stringTableStartIndex=Kn(e,e.length-4)}readString(e){if(-1===e)return null;{const n=Kn(this.batchDataUint8,this.stringTableStartIndex+4*e),r=function(e,t){let n=0,r=0;for(let o=0;o<4;o++){const i=e[t+o];if(n|=(127&i)<this.nextBatchId)return this.fatalError?(this.logger.log(or.Debug,`Received a new batch ${e} but errored out on a previous batch ${this.nextBatchId-1}`),void await n.send("OnRenderCompleted",this.nextBatchId-1,this.fatalError.toString())):void this.logger.log(or.Debug,`Waiting for batch ${this.nextBatchId}. Batch ${e} not processed.`);try{this.nextBatchId++,this.logger.log(or.Debug,`Applying batch ${e}.`),we(this.browserRendererId,new Yn(t)),await this.completeBatch(n,e)}catch(t){throw this.fatalError=t.toString(),this.logger.log(or.Error,`There was an error applying batch ${e}.`),n.send("OnRenderCompleted",e,t.toString()),t}}getLastBatchid(){return this.nextBatchId-1}async completeBatch(e,t){try{await e.send("OnRenderCompleted",t,null)}catch{this.logger.log(or.Warning,`Failed to deliver completion notification for render '${t}'.`)}}}class sr{log(e,t){}}sr.instance=new sr;class ar{constructor(e){this.minLevel=e}log(e,t){if(e>=this.minLevel){const n=`[${(new Date).toISOString()}] ${or[e]}: ${t}`;switch(e){case or.Critical:case or.Error:console.error(n);break;case or.Warning:console.warn(n);break;case or.Information:console.info(n);break;default:console.log(n)}}}}class cr{constructor(e,t){this.circuitId=void 0,this.components=e,this.applicationState=t}reconnect(e){if(!this.circuitId)throw new Error("Circuit host not initialized.");return e.state!==Bt.Connected?Promise.resolve(!1):e.invoke("ConnectCircuit",this.circuitId)}initialize(e){if(this.circuitId)throw new Error(`Circuit host '${this.circuitId}' already initialized.`);this.circuitId=e}async startCircuit(e){if(e.state!==Bt.Connected)return!1;const t=await e.invoke("StartCircuit",De.getBaseURI(),De.getLocationHref(),JSON.stringify(this.components.map((e=>e.toRecord()))),this.applicationState||"");return!!t&&(this.initialize(t),!0)}resolveElement(e){const t=v(e);if(t)return O(t,!0);const n=Number.parseInt(e);if(!Number.isNaN(n))return $(this.components[n].start,this.components[n].end);throw new Error(`Invalid sequence number or identifier '${e}'.`)}}const lr={configureSignalR:e=>{},logLevel:or.Warning,reconnectionOptions:{maxRetries:8,retryIntervalMilliseconds:2e4,dialogId:"components-reconnect-modal"}};class hr{constructor(e,t,n,r){this.maxRetries=t,this.document=n,this.logger=r,this.addedToDom=!1,this.modal=this.document.createElement("div"),this.modal.id=e,this.maxRetries=t,this.modal.style.cssText=["position: fixed","top: 0","right: 0","bottom: 0","left: 0","z-index: 1050","display: none","overflow: hidden","background-color: #fff","opacity: 0.8","text-align: center","font-weight: bold","transition: visibility 0s linear 500ms"].join(";"),this.message=this.document.createElement("h5"),this.message.style.cssText="margin-top: 20px",this.button=this.document.createElement("button"),this.button.style.cssText="margin:5px auto 5px",this.button.textContent="Retry";const o=this.document.createElement("a");o.addEventListener("click",(()=>location.reload())),o.textContent="reload",this.reloadParagraph=this.document.createElement("p"),this.reloadParagraph.textContent="Alternatively, ",this.reloadParagraph.appendChild(o),this.modal.appendChild(this.message),this.modal.appendChild(this.button),this.modal.appendChild(this.reloadParagraph),this.loader=this.getLoader(),this.message.after(this.loader),this.button.addEventListener("click",(async()=>{this.show();try{await et.reconnect()||this.rejected()}catch(e){this.logger.log(or.Error,e),this.failed()}}))}show(){this.addedToDom||(this.addedToDom=!0,this.document.body.appendChild(this.modal)),this.modal.style.display="block",this.loader.style.display="inline-block",this.button.style.display="none",this.reloadParagraph.style.display="none",this.message.textContent="Attempting to reconnect to the server...",this.modal.style.visibility="hidden",setTimeout((()=>{this.modal.style.visibility="visible"}),0)}update(e){this.message.textContent=`Attempting to reconnect to the server: ${e} of ${this.maxRetries}`}hide(){this.modal.style.display="none"}failed(){this.button.style.display="block",this.reloadParagraph.style.display="none",this.loader.style.display="none";const e=this.document.createTextNode("Reconnection failed. Try "),t=this.document.createElement("a");t.textContent="reloading",t.setAttribute("href",""),t.addEventListener("click",(()=>location.reload()));const n=this.document.createTextNode(" the page if you're unable to reconnect.");this.message.replaceChildren(e,t,n)}rejected(){this.button.style.display="none",this.reloadParagraph.style.display="none",this.loader.style.display="none";const e=this.document.createTextNode("Could not reconnect to the server. "),t=this.document.createElement("a");t.textContent="Reload",t.setAttribute("href",""),t.addEventListener("click",(()=>location.reload()));const n=this.document.createTextNode(" the page to restore functionality.");this.message.replaceChildren(e,t,n)}getLoader(){const e=this.document.createElement("div");return e.style.cssText=["border: 0.3em solid #f3f3f3","border-top: 0.3em solid #3498db","border-radius: 50%","width: 2em","height: 2em","display: inline-block"].join(";"),e.animate([{transform:"rotate(0deg)"},{transform:"rotate(360deg)"}],{duration:2e3,iterations:1/0}),e}}class dr{constructor(e,t,n){this.dialog=e,this.maxRetries=t,this.document=n,this.document=n;const r=this.document.getElementById(dr.MaxRetriesId);r&&(r.innerText=this.maxRetries.toString())}show(){this.removeClasses(),this.dialog.classList.add(dr.ShowClassName)}update(e){const t=this.document.getElementById(dr.CurrentAttemptId);t&&(t.innerText=e.toString())}hide(){this.removeClasses(),this.dialog.classList.add(dr.HideClassName)}failed(){this.removeClasses(),this.dialog.classList.add(dr.FailedClassName)}rejected(){this.removeClasses(),this.dialog.classList.add(dr.RejectedClassName)}removeClasses(){this.dialog.classList.remove(dr.ShowClassName,dr.HideClassName,dr.FailedClassName,dr.RejectedClassName)}}dr.ShowClassName="components-reconnect-show",dr.HideClassName="components-reconnect-hide",dr.FailedClassName="components-reconnect-failed",dr.RejectedClassName="components-reconnect-rejected",dr.MaxRetriesId="components-reconnect-max-retries",dr.CurrentAttemptId="components-reconnect-current-attempt";class ur{constructor(e,t,n){this._currentReconnectionProcess=null,this._logger=e,this._reconnectionDisplay=t,this._reconnectCallback=n||et.reconnect}onConnectionDown(e,t){if(!this._reconnectionDisplay){const t=document.getElementById(e.dialogId);this._reconnectionDisplay=t?new dr(t,e.maxRetries,document):new hr(e.dialogId,e.maxRetries,document,this._logger)}this._currentReconnectionProcess||(this._currentReconnectionProcess=new pr(e,this._logger,this._reconnectCallback,this._reconnectionDisplay))}onConnectionUp(){this._currentReconnectionProcess&&(this._currentReconnectionProcess.dispose(),this._currentReconnectionProcess=null)}}class pr{constructor(e,t,n,r){this.logger=t,this.reconnectCallback=n,this.isDisposed=!1,this.reconnectDisplay=r,this.reconnectDisplay.show(),this.attemptPeriodicReconnection(e)}dispose(){this.isDisposed=!0,this.reconnectDisplay.hide()}async attemptPeriodicReconnection(e){for(let t=0;tpr.MaximumFirstRetryInterval?pr.MaximumFirstRetryInterval:e.retryIntervalMilliseconds;if(await this.delay(n),this.isDisposed)break;try{return await this.reconnectCallback()?void 0:void this.reconnectDisplay.rejected()}catch(e){this.logger.log(or.Error,e)}}this.reconnectDisplay.failed()}delay(e){return new Promise((t=>setTimeout(t,e)))}}function fr(e,t){switch(t){case"webassembly":return function(e){const t=yr(e,"webassembly"),n=[];for(let e=0;ee.id-t.id))}(e);case"server":return function(e){const t=yr(e,"server"),n=[];for(let e=0;ee.sequence-t.sequence))}(e)}}pr.MaximumFirstRetryInterval=3e3;const gr=/^\s*Blazor-Component-State:(?[a-zA-Z0-9+/=]+)$/;function mr(e){var t;if(e.nodeType===Node.COMMENT_NODE){const n=e.textContent||"",r=gr.exec(n),o=r&&r.groups&&r.groups.state;return o&&(null===(t=e.parentNode)||void 0===t||t.removeChild(e)),o}if(!e.hasChildNodes())return;const n=e.childNodes;for(let e=0;e.*)$/);function vr(e,t){const n=e.currentElement;if(n&&n.nodeType===Node.COMMENT_NODE&&n.textContent){const r=wr.exec(n.textContent),o=r&&r.groups&&r.groups.descriptor;if(!o)return;try{const r=function(e){const t=JSON.parse(e),{type:n}=t;if("server"!==n&&"webassembly"!==n)throw new Error(`Invalid component type '${n}'.`);return t}(o);switch(t){case"webassembly":return function(e,t,n){const{type:r,assembly:o,typeName:i,parameterDefinitions:s,parameterValues:a,prerenderId:c}=e,l=c?br(c,n):void 0;if(c&&!l)throw new Error(`Could not find an end component comment for '${t}'.`);if("webassembly"===r){if(!o)throw new Error("assembly must be defined when using a descriptor.");if(!i)throw new Error("typeName must be defined when using a descriptor.");return{type:r,assembly:o,typeName:i,parameterDefinitions:s&&atob(s),parameterValues:a&&atob(a),start:t,prerenderId:c,end:l}}}(r,n,e);case"server":return function(e,t,n){const{type:r,descriptor:o,sequence:i,prerenderId:s}=e,a=s?br(s,n):void 0;if(s&&!a)throw new Error(`Could not find an end component comment for '${t}'.`);if("server"===r){if(!o)throw new Error("descriptor must be defined when using a descriptor.");if(void 0===i)throw new Error("sequence must be defined when using a descriptor.");if(!Number.isInteger(i))throw new Error(`Error parsing the sequence '${i}' for component '${JSON.stringify(e)}'`);return{type:r,sequence:i,descriptor:o,start:t,prerenderId:s,end:a}}}(r,n,e)}}catch(e){throw new Error(`Found malformed component comment at ${n.textContent}`)}}}function br(e,t){for(;t.next()&&t.currentElement;){const n=t.currentElement;if(n.nodeType!==Node.COMMENT_NODE)continue;if(!n.textContent)continue;const r=wr.exec(n.textContent),o=r&&r[1];if(o)return _r(o,e),n}}function _r(e,t){const n=JSON.parse(e);if(1!==Object.keys(n).length)throw new Error(`Invalid end of component comment: '${e}'`);const r=n.prerenderId;if(!r)throw new Error(`End of component comment must have a value for the prerendered property: '${e}'`);if(r!==t)throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${t}', '${r}'`)}class Er{constructor(e){this.childNodes=e,this.currentIndex=-1,this.length=e.length}next(){return this.currentIndex++,this.currentIndexasync function(e,n){const r=function(e){const t=document.baseURI;return t.endsWith("/")?`${t}${e}`:`${t}/${e}`}(n),o=await import(r);if(void 0===o)return;const{beforeStart:i,afterStarted:s}=o;return s&&e.afterStartedCallbacks.push(s),i?i(...t):void 0}(this,e))))}async invokeAfterStartedCallbacks(e){await I,await Promise.all(this.afterStartedCallbacks.map((t=>t(e))))}}let kr,Tr,Dr,xr,Ar=!1;async function Rr(e,t,n){var r,o;const i=new Hn;i.name="blazorpack";const s=(new Yt).withUrl("_blazor").withHubProtocol(i);e.configureSignalR(s);const a=s.build();a.on("JS.AttachComponent",((e,t)=>ye(0,n.resolveElement(t),e,!1))),a.on("JS.BeginInvokeJS",Dr.beginInvokeJSFromDotNet.bind(Dr)),a.on("JS.EndInvokeDotNet",Dr.endInvokeDotNetFromJS.bind(Dr)),a.on("JS.ReceiveByteArray",Dr.receiveByteArray.bind(Dr)),a.on("JS.BeginTransmitStream",(e=>{const t=new ReadableStream({start(t){a.stream("SendDotNetStreamToJS",e).subscribe({next:e=>t.enqueue(e),complete:()=>t.close(),error:e=>t.error(e)})}});Dr.supplyDotNetStream(e,t)}));const c=ir.getOrCreate(t);a.on("JS.RenderBatch",((e,n)=>{t.log(or.Debug,`Received render batch with id ${e} and ${n.byteLength} bytes.`),c.processBatch(e,n,a)})),a.on("JS.EndLocationChanging",et._internal.navigationManager.endLocationChanging),a.onclose((t=>!Ar&&e.reconnectionHandler.onConnectionDown(e.reconnectionOptions,t))),a.on("JS.Error",(e=>{Ar=!0,Nr(a,e,t),Wn()}));try{await a.start(),kr=a}catch(e){if(Nr(a,e,t),"FailedToNegotiateWithServerError"===e.errorType)throw e;Wn(),e.innerErrors&&(e.innerErrors.some((e=>"UnsupportedTransportError"===e.errorType&&e.transport===Ut.WebSockets))?t.log(or.Error,"Unable to connect, please ensure you are using an updated browser that supports WebSockets."):e.innerErrors.some((e=>"FailedToStartTransportError"===e.errorType&&e.transport===Ut.WebSockets))?t.log(or.Error,"Unable to connect, please ensure WebSockets are available. A VPN or proxy may be blocking the connection."):e.innerErrors.some((e=>"DisabledTransportError"===e.errorType&&e.transport===Ut.LongPolling))&&t.log(or.Error,"Unable to initiate a SignalR connection to the server. This might be because the server is not configured to support WebSockets. For additional details, visit https://aka.ms/blazor-server-websockets-error."))}return(null===(o=null===(r=a.connection)||void 0===r?void 0:r.features)||void 0===o?void 0:o.inherentKeepAlive)&&t.log(or.Warning,"Failed to connect via WebSockets, using the Long Polling fallback transport. This may be due to a VPN or proxy blocking the connection. To troubleshoot this, visit https://aka.ms/blazor-server-using-fallback-long-polling."),a}function Nr(e,t,n){n.log(or.Error,t),e&&e.stop()}function Pr(e){return xr=e,xr}var Ur,Lr;const Mr=navigator,Br=Mr.userAgentData&&Mr.userAgentData.brands,Fr=Br?Br.some((e=>"Google Chrome"===e.brand||"Microsoft Edge"===e.brand||"Chromium"===e.brand)):window.chrome,$r=null!==(Lr=null===(Ur=Mr.userAgentData)||void 0===Ur?void 0:Ur.platform)&&void 0!==Lr?Lr:navigator.platform;let Or,Hr,jr,Wr,zr,Jr,qr=!1,Vr=!1;function Kr(){return(qr||Vr)&&(Fr||navigator.userAgent.includes("Firefox"))}const Xr=Math.pow(2,32),Gr=Math.pow(2,21)-1;let Yr=null,Qr="Production";function Zr(e){return Hr.getI32(e)}const eo={start:function(t){return async function(t){const n={},{dotnet:r}=await async function(e){if("undefined"==typeof WebAssembly||!WebAssembly.validate)throw new Error("This browser does not support WebAssembly.");const t=await async function(e,t){const n=void 0!==e?e("manifest","blazor.boot.json","_framework/blazor.boot.json",""):i("_framework/blazor.boot.json");let r;r=n?"string"==typeof n?await i(n):await n:await i("_framework/blazor.boot.json"),Qr=t||r.headers.get("Blazor-Environment")||"Production";const o=await r.json();return o.modifiableAssemblies=r.headers.get("DOTNET-MODIFIABLE-ASSEMBLIES"),o.aspnetCoreBrowserTools=r.headers.get("ASPNETCORE-BROWSER-TOOLS"),o;function i(e){return fetch(e,{method:"GET",credentials:"include",cache:"no-cache"})}}(e.loadBootResource,e.environment),n=Object.keys(t.resources.runtime).filter((e=>e.startsWith("dotnet.")&&e.endsWith(".js")))[0],r=t.resources.runtime[n];let o=`_framework/${n}`;if(e.loadBootResource){const t="dotnetjs",i=e.loadBootResource(t,n,o,r);if("string"==typeof i)o=i;else if(i)throw new Error(`For a ${t} resource, custom loaders must supply a URI string.`)}if(t.cacheBootResources){const e=document.createElement("link");e.rel="modulepreload",e.href=o,e.crossOrigin="anonymous",e.integrity=r,document.head.appendChild(e)}const i=new URL(o,document.baseURI).toString();return await import(i)}(t),o=function(e,t){const n={maxParallelDownloads:1e6,enableDownloadRetry:!1,applicationEnvironment:Qr},r={...window.Module||{},onConfigLoaded:async n=>{n.environmentVariables||(n.environmentVariables={}),0===n.icuDataMode&&(n.environmentVariables.__BLAZOR_SHARDED_ICU="1"),n.aspnetCoreBrowserTools&&(n.environmentVariables.__ASPNETCORE_BROWSER_TOOLS=n.aspnetCoreBrowserTools),et._internal.getApplicationEnvironment=()=>n.applicationEnvironment,t.jsInitializer=await async function(e,t){const n=e.resources.libraryInitializers,r=new Ir;return n&&await r.importInitializersAsync(Object.keys(n),[t,e.resources.extensions]),r}(n,e)},onDownloadResourceProgress:to,config:n,disableDotnet6Compatibility:!1,print:ro,printErr:oo};return r}(t,n);r.withStartupOptions(t).withModuleConfig(o),Jr=await r.create();const{MONO:i,BINDING:s,Module:a,setModuleImports:c,INTERNAL:l}=Jr;jr=a,Or=s,Hr=i,zr=l;const h=zr.resourceLoader;n.resourceLoader=h,function(e){qr=!!e.bootConfig.resources.pdb,Vr=e.bootConfig.debugBuild;const t=$r.match(/^Mac/i)?"Cmd":"Alt";Kr()&&console.info(`Debugging hotkey: Shift+${t}+D (when application has focus)`),document.addEventListener("keydown",(e=>{e.shiftKey&&(e.metaKey||e.altKey)&&"KeyD"===e.code&&(Vr||qr?navigator.userAgent.includes("Firefox")?async function(){const e=await fetch(`_framework/debug?url=${encodeURIComponent(location.href)}&isFirefox=true`);200!==e.status&&console.warn(await e.text())}():Fr?function(){const e=document.createElement("a");e.href=`_framework/debug?url=${encodeURIComponent(location.href)}`,e.target="_blank",e.rel="noopener noreferrer",e.click()}():console.error("Currently, only Microsoft Edge (80+), Google Chrome, or Chromium, are supported for debugging."):console.error("Cannot start debugging, because the application was not compiled with debugging enabled."))}))}(h),et._internal.dotNetCriticalError=oo,et._internal.loadLazyAssembly=e=>async function(e,t){const n=e.bootConfig.resources,r=n.lazyAssembly;if(!r)throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly.");if(!r.hasOwnProperty(t))throw new Error(`${t} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);const o=t,i=function(e,t){const n=e.lastIndexOf(".");if(n<0)throw new Error(`No extension to replace in '${e}'`);return e.substr(0,n)+".pdb"}(t),s=Kr()&&n.pdb&&r.hasOwnProperty(i),a=e.loadResource(o,`_framework/${o}`,r[o],"assembly").response.then((e=>e.arrayBuffer()));if(s){const t=await e.loadResource(i,`_framework/${i}`,r[i],"pdb").response.then((e=>e.arrayBuffer())),[n,o]=await Promise.all([a,t]);return{dll:new Uint8Array(n),pdb:new Uint8Array(o)}}{const e=await a;return{dll:new Uint8Array(e),pdb:null}}}(zr.resourceLoader,e),et._internal.loadSatelliteAssemblies=(e,t)=>async function(e,t,n){const r=e.bootConfig.resources.satelliteResources;r&&await Promise.all(t.filter((e=>r.hasOwnProperty(e))).map((t=>e.loadResources(r[t],(e=>`_framework/${e}`),"assembly"))).reduce(((e,t)=>e.concat(t)),new Array).map((async e=>{const t=await e.response,r=await t.arrayBuffer(),o={dll:new Uint8Array(r)};n(o)})))}(h,e,t),c("blazor-internal",{Blazor:{_internal:et._internal}});const d=await Jr.getAssemblyExports("Microsoft.AspNetCore.Components.WebAssembly");return Object.assign(et._internal,{dotNetExports:{...d.Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime}}),Wr=e.attachDispatcher({beginInvokeDotNetFromJS:(e,t,n,r,o)=>{if(so(),!r&&!t)throw new Error("Either assemblyName or dotNetObjectId must have a non null value.");const i=r?r.toString():t;et._internal.dotNetExports.BeginInvokeDotNet(e?e.toString():null,i,n,o)},endInvokeJSFromDotNet:(e,t,n)=>{et._internal.dotNetExports.EndInvokeJS(n)},sendByteArray:(e,t)=>{et._internal.dotNetExports.ReceiveByteArrayFromJS(e,t)},invokeDotNetFromJS:(e,t,n,r)=>(so(),et._internal.dotNetExports.InvokeDotNet(e||null,t,null!=n?n:0,r))}),n}(t)},callEntryPoint:async function(e){try{await Jr.runMain(e,[])}catch(e){console.error(e),Wn()}},toUint8Array:function(e){const t=io(e),n=Zr(t),r=new Uint8Array(n);return r.set(jr.HEAPU8.subarray(t+4,t+4+n)),r},getArrayLength:function(e){return Zr(io(e))},getArrayEntryPtr:function(e,t,n){return io(e)+4+t*n},getObjectFieldsBaseAddress:function(e){return e+8},readInt16Field:function(e,t){return n=e+(t||0),Hr.getI16(n);var n},readInt32Field:function(e,t){return Zr(e+(t||0))},readUint64Field:function(e,t){return function(e){const t=e>>2,n=jr.HEAPU32[t+1];if(n>Gr)throw new Error(`Cannot read uint64 with high order part ${n}, because the result would exceed Number.MAX_SAFE_INTEGER.`);return n*Xr+jr.HEAPU32[t]}(e+(t||0))},readFloatField:function(e,t){return n=e+(t||0),Hr.getF32(n);var n},readObjectField:function(e,t){return Zr(e+(t||0))},readStringField:function(e,t,n){const r=Zr(e+(t||0));if(0===r)return null;if(n){const e=Or.unbox_mono_obj(r);return"boolean"==typeof e?e?"":null:e}return Or.conv_string(r)},readStructField:function(e,t){return e+(t||0)},beginHeapLock:function(){return so(),Yr=ao.create(),Yr},invokeWhenHeapUnlocked:function(e){Yr?Yr.enqueuePostReleaseAction(e):e()}};function to(e,t){const n=e/t*100;document.documentElement.style.setProperty("--blazor-load-percentage",`${n}%`),document.documentElement.style.setProperty("--blazor-load-percentage-text",`"${Math.floor(n)}%"`)}const no=["DEBUGGING ENABLED"],ro=e=>no.indexOf(e)<0&&console.log(e),oo=e=>{console.error(e||"(null)"),Wn()};function io(e){return e+12}function so(){if(Yr)throw new Error("Assertion failed - heap is currently locked")}class ao{enqueuePostReleaseAction(e){this.postReleaseActions||(this.postReleaseActions=[]),this.postReleaseActions.push(e)}release(){var e;if(Yr!==this)throw new Error("Trying to release a lock which isn't current");for(zr.mono_wasm_gc_unlock(),Yr=null;null===(e=this.postReleaseActions)||void 0===e?void 0:e.length;)this.postReleaseActions.shift()(),so()}static create(){return zr.mono_wasm_gc_lock(),new ao}}class co{constructor(e){this.batchAddress=e,this.arrayRangeReader=lo,this.arrayBuilderSegmentReader=ho,this.diffReader=uo,this.editReader=po,this.frameReader=fo}updatedComponents(){return xr.readStructField(this.batchAddress,0)}referenceFrames(){return xr.readStructField(this.batchAddress,lo.structLength)}disposedComponentIds(){return xr.readStructField(this.batchAddress,2*lo.structLength)}disposedEventHandlerIds(){return xr.readStructField(this.batchAddress,3*lo.structLength)}updatedComponentsEntry(e,t){return go(e,t,uo.structLength)}referenceFramesEntry(e,t){return go(e,t,fo.structLength)}disposedComponentIdsEntry(e,t){const n=go(e,t,4);return xr.readInt32Field(n)}disposedEventHandlerIdsEntry(e,t){const n=go(e,t,8);return xr.readUint64Field(n)}}const lo={structLength:8,values:e=>xr.readObjectField(e,0),count:e=>xr.readInt32Field(e,4)},ho={structLength:12,values:e=>{const t=xr.readObjectField(e,0),n=xr.getObjectFieldsBaseAddress(t);return xr.readObjectField(n,0)},offset:e=>xr.readInt32Field(e,4),count:e=>xr.readInt32Field(e,8)},uo={structLength:4+ho.structLength,componentId:e=>xr.readInt32Field(e,0),edits:e=>xr.readStructField(e,4),editsEntry:(e,t)=>go(e,t,po.structLength)},po={structLength:20,editType:e=>xr.readInt32Field(e,0),siblingIndex:e=>xr.readInt32Field(e,4),newTreeIndex:e=>xr.readInt32Field(e,8),moveToSiblingIndex:e=>xr.readInt32Field(e,8),removedAttributeName:e=>xr.readStringField(e,16)},fo={structLength:36,frameType:e=>xr.readInt16Field(e,4),subtreeLength:e=>xr.readInt32Field(e,8),elementReferenceCaptureId:e=>xr.readStringField(e,16),componentId:e=>xr.readInt32Field(e,12),elementName:e=>xr.readStringField(e,16),textContent:e=>xr.readStringField(e,16),markupContent:e=>xr.readStringField(e,16),attributeName:e=>xr.readStringField(e,16),attributeValue:e=>xr.readStringField(e,24,!0),attributeEventHandlerId:e=>xr.readUint64Field(e,8)};function go(e,t,n){return xr.getArrayEntryPtr(e,t,n)}class mo{constructor(e){this.preregisteredComponents=e;const t={};for(let n=0;n{this.childNodes.forEach((e=>{if(e instanceof HTMLTemplateElement){const t=e.getAttribute("blazor-component-id");t&&function(e,t){const n=function(e){const t=`bl:${e}`,n=document.createNodeIterator(document,NodeFilter.SHOW_COMMENT);let r=null;for(;(r=n.nextNode())&&r.textContent!==t;);if(!r)return null;const o=`/bl:${e}`;let i=null;for(;(i=n.nextNode())&&i.textContent!==o;);return i?{startMarker:r,endMarker:i}:null}(e);if(n){const{startMarker:e,endMarker:r}=n,o=new Range;for(o.setStart(e,e.textContent.length),o.setEnd(r,0),o.deleteContents();t.childNodes[0];)r.parentNode.insertBefore(t.childNodes[0],r)}}(t,e.content)}}))}))}}let So=!1;async function Co(t){if(So)throw new Error("Blazor has already started.");So=!0,await async function(t){const n=fr(document,"server"),r=fr(document,"webassembly");n.length&&await async function(t,n){const r=function(e){const t={...lr,...e};return e&&e.reconnectionOptions&&(t.reconnectionOptions={...lr.reconnectionOptions,...e.reconnectionOptions}),t}(t),o=await async function(e){const t=await fetch("_blazor/initializers",{method:"GET",credentials:"include",cache:"no-cache"}),n=await t.json(),r=new Ir;return await r.importInitializersAsync(n,[e]),r}(r),i=new ar(r.logLevel);et.reconnect=async e=>{if(Ar)return!1;const t=e||await Rr(r,i,Tr);return await Tr.reconnect(t)?(r.reconnectionHandler.onConnectionUp(),!0):(i.log(or.Information,"Reconnection attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server."),!1)},et.defaultReconnectionHandler=new ur(i),r.reconnectionHandler=r.reconnectionHandler||et.defaultReconnectionHandler,i.log(or.Information,"Starting up Blazor server-side application.");const s=mr(document);Tr=new cr(n||[],s||""),et._internal.navigationManager.listenForNavigationEvents(((e,t,n)=>kr.send("OnLocationChanged",e,t,n)),((e,t,n,r)=>kr.send("OnLocationChanging",e,t,n,r))),et._internal.forceCloseConnection=()=>kr.stop(),et._internal.sendJSDataStream=(e,t,n)=>function(e,t,n,r){setTimeout((async()=>{let o=5,i=(new Date).valueOf();try{const s=t instanceof Blob?t.size:t.byteLength;let a=0,c=0;for(;a1)await e.send("ReceiveJSDataChunk",n,c,h,null);else{if(!await e.invoke("ReceiveJSDataChunk",n,c,h,null))break;const t=(new Date).valueOf(),r=t-i;i=t,o=Math.max(1,Math.round(500/Math.max(1,r)))}a+=l,c++}}catch(t){await e.send("ReceiveJSDataChunk",n,-1,null,t.toString())}}),0)}(kr,e,t,n),Dr=e.attachDispatcher({beginInvokeDotNetFromJS:(e,t,n,r,o)=>{kr.send("BeginInvokeDotNetFromJS",e?e.toString():null,t,n,r||0,o)},endInvokeJSFromDotNet:(e,t,n)=>{kr.send("EndInvokeJSFromDotNet",e,t,n)},sendByteArray:(e,t)=>{kr.send("ReceiveByteArray",e,t)}});const a=await Rr(r,i,Tr);if(!await Tr.startCircuit(a))return void i.log(or.Error,"Failed to start the circuit.");let c=!1;const l=()=>{if(!c){const e=new FormData,t=Tr.circuitId;e.append("circuitId",t),c=navigator.sendBeacon("_blazor/disconnect",e)}};et.disconnect=l,window.addEventListener("unload",l,{capture:!1,once:!0}),i.log(or.Information,"Blazor server-side application started."),o.invokeAfterStartedCallbacks(et)}(null==t?void 0:t.circuit,n),r.length&&await async function(e,t){(function(){if(window.parent!==window&&!window.opener&&window.frameElement){const e=window.sessionStorage&&window.sessionStorage["Microsoft.AspNetCore.Components.WebAssembly.Authentication.CachedAuthSettings"],t=e&&JSON.parse(e);return t&&t.redirect_uri&&location.href.startsWith(t.redirect_uri)}return!1})()&&await new Promise((()=>{})),function(e){const t=D;D=(e,n,r)=>{((e,t,n)=>{const r=function(e){return ge[e]}(e);r.eventDelegator.getHandler(t)&&eo.invokeWhenHeapUnlocked(n)})(e,n,(()=>t(e,n,r)))}}(),et._internal.applyHotReload=(e,t,n,r)=>{Wr.invokeDotNetStaticMethod("Microsoft.AspNetCore.Components.WebAssembly","ApplyHotReloadDelta",e,t,n,r)},et._internal.getApplyUpdateCapabilities=()=>Wr.invokeDotNetStaticMethod("Microsoft.AspNetCore.Components.WebAssembly","GetApplyUpdateCapabilities"),et._internal.invokeJSFromDotNet=yo,et._internal.invokeJSJson=wo,et._internal.endInvokeDotNetFromJS=vo,et._internal.receiveWebAssemblyDotNetDataStream=bo,et._internal.receiveByteArray=_o;const n=Pr(eo);et.platform=n,et._internal.renderBatch=(e,t)=>{const n=eo.beginHeapLock();try{we(e,new co(t))}finally{n.release()}},et._internal.navigationManager.listenForNavigationEvents((async(e,t,n)=>{await Wr.invokeDotNetStaticMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","NotifyLocationChanged",e,t,n)}),(async(e,t,n,r)=>{const o=await Wr.invokeDotNetStaticMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","NotifyLocationChangingAsync",t,n,r);et._internal.navigationManager.endLocationChanging(e,o)}));const r=new mo(t||[]);let o,i;et._internal.registeredComponents={getRegisteredComponentsCount:()=>r.getCount(),getId:e=>r.getId(e),getAssembly:e=>r.getAssembly(e),getTypeName:e=>r.getTypeName(e),getParameterDefinitions:e=>r.getParameterDefinitions(e)||"",getParameterValues:e=>r.getParameterValues(e)||""},et._internal.getPersistedState=()=>mr(document)||"",et._internal.attachRootComponentToElement=(e,t,n)=>{const o=r.resolveRegisteredElement(e);o?ye(n,o,t,!1):function(e,t,n){const r="::after",o="::before";let i=!1;if(e.endsWith(r))e=e.slice(0,-r.length),i=!0;else if(e.endsWith(o))throw new Error(`The '${o}' selector is not supported.`);const s=v(e)||document.querySelector(e);if(!s)throw new Error(`Could not find any element matching selector '${e}'.`);ye(n||0,O(s,!0),t,i)}(e,t,n)};try{const t=await n.start(null!=e?e:{});o=t.resourceLoader,i=t.jsInitializer}catch(e){throw new Error(`Failed to start platform. Reason: ${e}`)}n.callEntryPoint(o.bootConfig.entryAssembly),i.invokeAfterStartedCallbacks(et)}(null==t?void 0:t.webAssembly,r)}(t),customElements.define("blazor-ssr",Eo)}et.start=Co,window.DotNet=e,document&&document.currentScript&&"false"!==document.currentScript.getAttribute("autostart")&&Co()})(); \ No newline at end of file diff --git a/src/Components/Web.JS/dist/Release/blazor.webview.js b/src/Components/Web.JS/dist/Release/blazor.webview.js index 709b19e91c5a..88c2a2f4cc58 100644 --- a/src/Components/Web.JS/dist/Release/blazor.webview.js +++ b/src/Components/Web.JS/dist/Release/blazor.webview.js @@ -1 +1 @@ -(()=>{"use strict";var e,t,n,r={d:(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};r.d({},{e:()=>It}),function(e){const t=[],n="__jsObjectId",r="__dotNetObject",o="__byte[]",a="__dotNetStream",s="__jsStreamReferenceLength";let i,c;class l{constructor(e){this._jsObject=e,this._cachedFunctions=new Map}findFunction(e){const t=this._cachedFunctions.get(e);if(t)return t;let n,r=this._jsObject;if(e.split(".").forEach((t=>{if(!(t in r))throw new Error(`Could not find '${e}' ('${t}' was undefined).`);n=r,r=r[t]})),r instanceof Function)return r=r.bind(n),this._cachedFunctions.set(e,r),r;throw new Error(`The value '${e}' is not a function.`)}getWrappedObject(){return this._jsObject}}const u={0:new l(window)};u[0]._cachedFunctions.set("import",(e=>("string"==typeof e&&e.startsWith("./")&&(e=new URL(e.substr(2),document.baseURI).toString()),import(e))));let d,h=1;function f(e){t.push(e)}function p(e){if(e&&"object"==typeof e){u[h]=new l(e);const t={[n]:h};return h++,t}throw new Error(`Cannot create a JSObjectReference from the value '${e}'.`)}function m(e){let t=-1;if(e instanceof ArrayBuffer&&(e=new Uint8Array(e)),e instanceof Blob)t=e.size;else{if(!(e.buffer instanceof ArrayBuffer))throw new Error("Supplied value is not a typed array or blob.");if(void 0===e.byteLength)throw new Error(`Cannot create a JSStreamReference from the value '${e}' as it doesn't have a byteLength.`);t=e.byteLength}const r={[s]:t};try{const t=p(e);r[n]=t[n]}catch(t){throw new Error(`Cannot create a JSStreamReference from the value '${e}'.`)}return r}function v(e,n){c=e;const r=n?JSON.parse(n,((e,n)=>t.reduce(((t,n)=>n(e,t)),n))):null;return c=void 0,r}function g(){if(void 0===i)throw new Error("No call dispatcher has been set.");if(null===i)throw new Error("There are multiple .NET runtimes present, so a default dispatcher could not be resolved. Use DotNetObject to invoke .NET instance methods.");return i}e.attachDispatcher=function(e){const t=new y(e);return void 0===i?i=t:i&&(i=null),t},e.attachReviver=f,e.invokeMethod=function(e,t,...n){return g().invokeDotNetStaticMethod(e,t,...n)},e.invokeMethodAsync=function(e,t,...n){return g().invokeDotNetStaticMethodAsync(e,t,...n)},e.createJSObjectReference=p,e.createJSStreamReference=m,e.disposeJSObjectReference=function(e){const t=e&&e[n];"number"==typeof t&&function(e){delete u[e]}(t)},function(e){e[e.Default=0]="Default",e[e.JSObjectReference=1]="JSObjectReference",e[e.JSStreamReference=2]="JSStreamReference",e[e.JSVoidResult=3]="JSVoidResult"}(d=e.JSCallResultType||(e.JSCallResultType={}));class y{constructor(e){this._dotNetCallDispatcher=e,this._byteArraysToBeRevived=new Map,this._pendingDotNetToJSStreams=new Map,this._pendingAsyncCalls={},this._nextAsyncCallId=1}getDotNetCallDispatcher(){return this._dotNetCallDispatcher}invokeJSFromDotNet(e,t,n,r){const o=v(this,t),a=D(w(e,r)(...o||[]),n);return null==a?null:A(this,a)}beginInvokeJSFromDotNet(e,t,n,r,o){const a=new Promise((e=>{const r=v(this,n);e(w(t,o)(...r||[]))}));e&&a.then((t=>A(this,[e,!0,D(t,r)]))).then((t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!0,t)),(t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!1,JSON.stringify([e,!1,b(t)]))))}endInvokeDotNetFromJS(e,t,n){const r=t?v(this,n):new Error(n);this.completePendingCall(parseInt(e,10),t,r)}invokeDotNetStaticMethod(e,t,...n){return this.invokeDotNetMethod(e,t,null,n)}invokeDotNetStaticMethodAsync(e,t,...n){return this.invokeDotNetMethodAsync(e,t,null,n)}invokeDotNetMethod(e,t,n,r){if(this._dotNetCallDispatcher.invokeDotNetFromJS){const o=A(this,r),a=this._dotNetCallDispatcher.invokeDotNetFromJS(e,t,n,o);return a?v(this,a):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeDotNetMethodAsync instead.")}invokeDotNetMethodAsync(e,t,n,r){if(e&&n)throw new Error(`For instance method calls, assemblyName should be null. Received '${e}'.`);const o=this._nextAsyncCallId++,a=new Promise(((e,t)=>{this._pendingAsyncCalls[o]={resolve:e,reject:t}}));try{const a=A(this,r);this._dotNetCallDispatcher.beginInvokeDotNetFromJS(o,e,t,n,a)}catch(e){this.completePendingCall(o,!1,e)}return a}receiveByteArray(e,t){this._byteArraysToBeRevived.set(e,t)}processByteArray(e){const t=this._byteArraysToBeRevived.get(e);return t?(this._byteArraysToBeRevived.delete(e),t):null}supplyDotNetStream(e,t){if(this._pendingDotNetToJSStreams.has(e)){const n=this._pendingDotNetToJSStreams.get(e);this._pendingDotNetToJSStreams.delete(e),n.resolve(t)}else{const n=new I;n.resolve(t),this._pendingDotNetToJSStreams.set(e,n)}}getDotNetStreamPromise(e){let t;if(this._pendingDotNetToJSStreams.has(e))t=this._pendingDotNetToJSStreams.get(e).streamPromise,this._pendingDotNetToJSStreams.delete(e);else{const n=new I;this._pendingDotNetToJSStreams.set(e,n),t=n.streamPromise}return t}completePendingCall(e,t,n){if(!this._pendingAsyncCalls.hasOwnProperty(e))throw new Error(`There is no pending async call with ID ${e}.`);const r=this._pendingAsyncCalls[e];delete this._pendingAsyncCalls[e],t?r.resolve(n):r.reject(n)}}function b(e){return e instanceof Error?`${e.message}\n${e.stack}`:e?e.toString():"null"}function w(e,t){const n=u[t];if(n)return n.findFunction(e);throw new Error(`JS object instance with ID ${t} does not exist (has it been disposed?).`)}e.findJSFunction=w;class E{constructor(e,t){this._id=e,this._callDispatcher=t}invokeMethod(e,...t){return this._callDispatcher.invokeDotNetMethod(null,e,this._id,t)}invokeMethodAsync(e,...t){return this._callDispatcher.invokeDotNetMethodAsync(null,e,this._id,t)}dispose(){this._callDispatcher.invokeDotNetMethodAsync(null,"__Dispose",this._id,null).catch((e=>console.error(e)))}serializeAsArg(){return{[r]:this._id}}}e.DotNetObject=E,f((function(e,t){if(t&&"object"==typeof t){if(t.hasOwnProperty(r))return new E(t[r],c);if(t.hasOwnProperty(n)){const e=t[n],r=u[e];if(r)return r.getWrappedObject();throw new Error(`JS object instance with Id '${e}' does not exist. It may have been disposed.`)}if(t.hasOwnProperty(o)){const e=t[o],n=c.processByteArray(e);if(void 0===n)throw new Error(`Byte array index '${e}' does not exist.`);return n}if(t.hasOwnProperty(a)){const e=t[a],n=c.getDotNetStreamPromise(e);return new S(n)}}return t}));class S{constructor(e){this._streamPromise=e}stream(){return this._streamPromise}async arrayBuffer(){return new Response(await this.stream()).arrayBuffer()}}class I{constructor(){this.streamPromise=new Promise(((e,t)=>{this.resolve=e,this.reject=t}))}}function D(e,t){switch(t){case d.Default:return e;case d.JSObjectReference:return p(e);case d.JSStreamReference:return m(e);case d.JSVoidResult:return null;default:throw new Error(`Invalid JS call result type '${t}'.`)}}let C=0;function A(e,t){C=0,c=e;const n=JSON.stringify(t,N);return c=void 0,n}function N(e,t){if(t instanceof E)return t.serializeAsArg();if(t instanceof Uint8Array){c.getDotNetCallDispatcher().sendByteArray(C,t);const e={[o]:C};return C++,e}return t}}(e||(e={})),function(e){e[e.prependFrame=1]="prependFrame",e[e.removeFrame=2]="removeFrame",e[e.setAttribute=3]="setAttribute",e[e.removeAttribute=4]="removeAttribute",e[e.updateText=5]="updateText",e[e.stepIn=6]="stepIn",e[e.stepOut=7]="stepOut",e[e.updateMarkup=8]="updateMarkup",e[e.permutationListEntry=9]="permutationListEntry",e[e.permutationListEnd=10]="permutationListEnd"}(t||(t={})),function(e){e[e.element=1]="element",e[e.text=2]="text",e[e.attribute=3]="attribute",e[e.component=4]="component",e[e.region=5]="region",e[e.elementReferenceCapture=6]="elementReferenceCapture",e[e.markup=8]="markup"}(n||(n={}));class o{constructor(e,t){this.componentId=e,this.fieldValue=t}static fromEvent(e,t){const n=t.target;if(n instanceof Element){const t=function(e){return e instanceof HTMLInputElement?e.type&&"checkbox"===e.type.toLowerCase()?{value:e.checked}:{value:e.value}:e instanceof HTMLSelectElement||e instanceof HTMLTextAreaElement?{value:e.value}:null}(n);if(t)return new o(e,t.value)}return null}}const a=new Map,s=new Map,i=[];function c(e){return a.get(e)}function l(e){const t=a.get(e);return(null==t?void 0:t.browserEventName)||e}function u(e,t){e.forEach((e=>a.set(e,t)))}function d(e){const t=[];for(let n=0;ne.selected)).map((e=>e.value))}}{const e=function(e){return!!e&&"INPUT"===e.tagName&&"checkbox"===e.getAttribute("type")}(t);return{value:e?!!t.checked:t.value}}}}),u(["copy","cut","paste"],{createEventArgs:e=>({type:e.type})}),u(["drag","dragend","dragenter","dragleave","dragover","dragstart","drop"],{createEventArgs:e=>{return{...h(t=e),dataTransfer:t.dataTransfer?{dropEffect:t.dataTransfer.dropEffect,effectAllowed:t.dataTransfer.effectAllowed,files:Array.from(t.dataTransfer.files).map((e=>e.name)),items:Array.from(t.dataTransfer.items).map((e=>({kind:e.kind,type:e.type}))),types:t.dataTransfer.types}:null};var t}}),u(["focus","blur","focusin","focusout"],{createEventArgs:e=>({type:e.type})}),u(["keydown","keyup","keypress"],{createEventArgs:e=>{return{key:(t=e).key,code:t.code,location:t.location,repeat:t.repeat,ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),u(["contextmenu","click","mouseover","mouseout","mousemove","mousedown","mouseup","mouseleave","mouseenter","dblclick"],{createEventArgs:e=>h(e)}),u(["error"],{createEventArgs:e=>{return{message:(t=e).message,filename:t.filename,lineno:t.lineno,colno:t.colno,type:t.type};var t}}),u(["loadstart","timeout","abort","load","loadend","progress"],{createEventArgs:e=>{return{lengthComputable:(t=e).lengthComputable,loaded:t.loaded,total:t.total,type:t.type};var t}}),u(["touchcancel","touchend","touchmove","touchenter","touchleave","touchstart"],{createEventArgs:e=>{return{detail:(t=e).detail,touches:d(t.touches),targetTouches:d(t.targetTouches),changedTouches:d(t.changedTouches),ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),u(["gotpointercapture","lostpointercapture","pointercancel","pointerdown","pointerenter","pointerleave","pointermove","pointerout","pointerover","pointerup"],{createEventArgs:e=>{return{...h(t=e),pointerId:t.pointerId,width:t.width,height:t.height,pressure:t.pressure,tiltX:t.tiltX,tiltY:t.tiltY,pointerType:t.pointerType,isPrimary:t.isPrimary};var t}}),u(["wheel","mousewheel"],{createEventArgs:e=>{return{...h(t=e),deltaX:t.deltaX,deltaY:t.deltaY,deltaZ:t.deltaZ,deltaMode:t.deltaMode};var t}}),u(["toggle"],{createEventArgs:()=>({})});const f=["date","datetime-local","month","time","week"],p=new Map;let m,v,g=0;const y={async add(e,t,n){if(!n)throw new Error("initialParameters must be an object, even if empty.");const r="__bl-dynamic-root:"+(++g).toString();p.set(r,e);const o=await E().invokeMethodAsync("AddRootComponent",t,r),a=new w(o,v[t]);return await a.setParameters(n),a}};class b{invoke(e){return this._callback(e)}setCallback(t){this._selfJSObjectReference||(this._selfJSObjectReference=e.createJSObjectReference(this)),this._callback=t}getJSObjectReference(){return this._selfJSObjectReference}dispose(){this._selfJSObjectReference&&e.disposeJSObjectReference(this._selfJSObjectReference)}}class w{constructor(e,t){this._jsEventCallbackWrappers=new Map,this._componentId=e;for(const e of t)"eventcallback"===e.type&&this._jsEventCallbackWrappers.set(e.name.toLowerCase(),new b)}setParameters(e){const t={},n=Object.entries(e||{}),r=n.length;for(const[e,r]of n){const n=this._jsEventCallbackWrappers.get(e.toLowerCase());n&&r?(n.setCallback(r),t[e]=n.getJSObjectReference()):t[e]=r}return E().invokeMethodAsync("SetRootComponentParameters",this._componentId,r,t)}async dispose(){if(null!==this._componentId){await E().invokeMethodAsync("RemoveRootComponent",this._componentId),this._componentId=null;for(const e of this._jsEventCallbackWrappers.values())e.dispose()}}}function E(){if(!m)throw new Error("Dynamic root components have not been enabled in this application.");return m}const S=[];let I;const D=new Promise((e=>{I=e}));function C(e,t,n){return N(e,t.eventHandlerId,(()=>A(e).invokeMethodAsync("DispatchEventAsync",t,n)))}function A(e){const t=S[e];if(!t)throw new Error(`No interop methods are registered for renderer ${e}`);return t}let N=(e,t,n)=>n();const k=x(["abort","blur","canplay","canplaythrough","change","cuechange","durationchange","emptied","ended","error","focus","load","loadeddata","loadedmetadata","loadend","loadstart","mouseenter","mouseleave","pointerenter","pointerleave","pause","play","playing","progress","ratechange","reset","scroll","seeked","seeking","stalled","submit","suspend","timeupdate","toggle","unload","volumechange","waiting","DOMNodeInsertedIntoDocument","DOMNodeRemovedFromDocument"]),T={submit:!0},R=x(["click","dblclick","mousedown","mousemove","mouseup"]);class _{constructor(e){this.browserRendererId=e,this.afterClickCallbacks=[];const t=++_.nextEventDelegatorId;this.eventsCollectionKey=`_blazorEvents_${t}`,this.eventInfoStore=new O(this.onGlobalEvent.bind(this))}setListener(e,t,n,r){const o=this.getEventHandlerInfosForElement(e,!0),a=o.getHandler(t);if(a)this.eventInfoStore.update(a.eventHandlerId,n);else{const a={element:e,eventName:t,eventHandlerId:n,renderingComponentId:r};this.eventInfoStore.add(a),o.setHandler(t,a)}}getHandler(e){return this.eventInfoStore.get(e)}removeListener(e){const t=this.eventInfoStore.remove(e);if(t){const e=t.element,n=this.getEventHandlerInfosForElement(e,!1);n&&n.removeHandler(t.eventName)}}notifyAfterClick(e){this.afterClickCallbacks.push(e),this.eventInfoStore.addGlobalListener("click")}setStopPropagation(e,t,n){this.getEventHandlerInfosForElement(e,!0).stopPropagation(t,n)}setPreventDefault(e,t,n){this.getEventHandlerInfosForElement(e,!0).preventDefault(t,n)}onGlobalEvent(e){if(!(e.target instanceof Element))return;this.dispatchGlobalEventToAllElements(e.type,e);const t=(n=e.type,s.get(n));var n;t&&t.forEach((t=>this.dispatchGlobalEventToAllElements(t,e))),"click"===e.type&&this.afterClickCallbacks.forEach((t=>t(e)))}dispatchGlobalEventToAllElements(e,t){const n=t.composedPath();let r=n.shift(),a=null,s=!1;const i=Object.prototype.hasOwnProperty.call(k,e);let l=!1;for(;r;){const h=r,f=this.getEventHandlerInfosForElement(h,!1);if(f){const n=f.getHandler(e);if(n&&(u=h,d=t.type,!((u instanceof HTMLButtonElement||u instanceof HTMLInputElement||u instanceof HTMLTextAreaElement||u instanceof HTMLSelectElement)&&Object.prototype.hasOwnProperty.call(R,d)&&u.disabled))){if(!s){const n=c(e);a=(null==n?void 0:n.createEventArgs)?n.createEventArgs(t):{},s=!0}Object.prototype.hasOwnProperty.call(T,t.type)&&t.preventDefault(),C(this.browserRendererId,{eventHandlerId:n.eventHandlerId,eventName:e,eventFieldInfo:o.fromEvent(n.renderingComponentId,t)},a)}f.stopPropagation(e)&&(l=!0),f.preventDefault(e)&&t.preventDefault()}r=i||l?void 0:n.shift()}var u,d}getEventHandlerInfosForElement(e,t){return Object.prototype.hasOwnProperty.call(e,this.eventsCollectionKey)?e[this.eventsCollectionKey]:t?e[this.eventsCollectionKey]=new L:null}}_.nextEventDelegatorId=0;class O{constructor(e){this.globalListener=e,this.infosByEventHandlerId={},this.countByEventName={},i.push(this.handleEventNameAliasAdded.bind(this))}add(e){if(this.infosByEventHandlerId[e.eventHandlerId])throw new Error(`Event ${e.eventHandlerId} is already tracked`);this.infosByEventHandlerId[e.eventHandlerId]=e,this.addGlobalListener(e.eventName)}get(e){return this.infosByEventHandlerId[e]}addGlobalListener(e){if(e=l(e),Object.prototype.hasOwnProperty.call(this.countByEventName,e))this.countByEventName[e]++;else{this.countByEventName[e]=1;const t=Object.prototype.hasOwnProperty.call(k,e);document.addEventListener(e,this.globalListener,t)}}update(e,t){if(Object.prototype.hasOwnProperty.call(this.infosByEventHandlerId,t))throw new Error(`Event ${t} is already tracked`);const n=this.infosByEventHandlerId[e];delete this.infosByEventHandlerId[e],n.eventHandlerId=t,this.infosByEventHandlerId[t]=n}remove(e){const t=this.infosByEventHandlerId[e];if(t){delete this.infosByEventHandlerId[e];const n=l(t.eventName);0==--this.countByEventName[n]&&(delete this.countByEventName[n],document.removeEventListener(n,this.globalListener))}return t}handleEventNameAliasAdded(e,t){if(Object.prototype.hasOwnProperty.call(this.countByEventName,e)){const n=this.countByEventName[e];delete this.countByEventName[e],document.removeEventListener(e,this.globalListener),this.addGlobalListener(t),this.countByEventName[t]+=n-1}}}class L{constructor(){this.handlers={},this.preventDefaultFlags=null,this.stopPropagationFlags=null}getHandler(e){return Object.prototype.hasOwnProperty.call(this.handlers,e)?this.handlers[e]:null}setHandler(e,t){this.handlers[e]=t}removeHandler(e){delete this.handlers[e]}preventDefault(e,t){return void 0!==t&&(this.preventDefaultFlags=this.preventDefaultFlags||{},this.preventDefaultFlags[e]=t),!!this.preventDefaultFlags&&this.preventDefaultFlags[e]}stopPropagation(e,t){return void 0!==t&&(this.stopPropagationFlags=this.stopPropagationFlags||{},this.stopPropagationFlags[e]=t),!!this.stopPropagationFlags&&this.stopPropagationFlags[e]}}function x(e){const t={};return e.forEach((e=>{t[e]=!0})),t}const F=q("_blazorLogicalChildren"),P=q("_blazorLogicalParent"),M=q("_blazorLogicalEnd");function B(e,t){if(e.childNodes.length>0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return F in e||(e[F]=[]),e}function H(e,t){const n=document.createComment("!");return j(n,e,t),n}function j(e,t,n){const r=e;if(e instanceof Comment&&K(r)&&K(r).length>0)throw new Error("Not implemented: inserting non-empty logical container");if(U(r))throw new Error("Not implemented: moving existing logical children");const o=K(t);if(n0;)J(n,0)}const r=n;r.parentNode.removeChild(r)}function U(e){return e[P]||null}function $(e,t){return K(e)[t]}function z(e){const t=X(e);return"http://www.w3.org/2000/svg"===t.namespaceURI&&"foreignObject"!==t.tagName}function K(e){return e[F]}function V(e,t){const n=K(e);t.forEach((e=>{e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=G(e.moveRangeStart)})),t.forEach((t=>{const r=document.createComment("marker");t.moveToBeforeMarker=r;const o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):Y(r,e)})),t.forEach((e=>{const t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd;let a=r;for(;a;){const e=a.nextSibling;if(n.insertBefore(a,t),a===o)break;a=e}n.removeChild(t)})),t.forEach((e=>{n[e.toSiblingIndex]=e.moveRangeStart}))}function X(e){if(e instanceof Element||e instanceof DocumentFragment)return e;if(e instanceof Comment)return e.parentNode;throw new Error("Not a valid logical element")}function W(e){const t=K(U(e));return t[Array.prototype.indexOf.call(t,e)+1]||null}function Y(e,t){if(t instanceof Element||t instanceof DocumentFragment)t.appendChild(e);else{if(!(t instanceof Comment))throw new Error(`Cannot append node because the parent is not a valid logical element. Parent: ${t}`);{const n=W(t);n?n.parentNode.insertBefore(e,n):Y(e,U(t))}}}function G(e){if(e instanceof Element||e instanceof DocumentFragment)return e;const t=W(e);if(t)return t.previousSibling;{const t=U(e);return t instanceof Element||t instanceof DocumentFragment?t.lastChild:G(t)}}function q(e){return"function"==typeof Symbol?Symbol():e}function Z(e){return`_bl_${e}`}const Q="__internalId";e.attachReviver(((e,t)=>t&&"object"==typeof t&&Object.prototype.hasOwnProperty.call(t,Q)&&"string"==typeof t[Q]?function(e){const t=`[${Z(e)}]`;return document.querySelector(t)}(t[Q]):t));const ee="_blazorDeferredValue",te=document.createElement("template"),ne=document.createElementNS("http://www.w3.org/2000/svg","g"),re={},oe="__internal_",ae="preventDefault_",se="stopPropagation_";class ie{constructor(e){this.rootComponentIds=new Set,this.childComponentLocations={},this.eventDelegator=new _(e),this.eventDelegator.notifyAfterClick((e=>{if(!me)return;if(0!==e.button||function(e){return e.ctrlKey||e.shiftKey||e.altKey||e.metaKey}(e))return;if(e.defaultPrevented)return;const t=function(e){const t=!window._blazorDisableComposedPath&&e.composedPath&&e.composedPath();if(t){for(let e=0;edocument.baseURI,getLocationHref:()=>location.href,scrollToElement:Ce};function Ce(e){const t=document.getElementById(e);return!!t&&(t.scrollIntoView(),!0)}function Ae(e,t,n=!1){const r=Fe(e);!t.forceLoad&&Me(r)?Ne(r,!1,t.replaceHistoryEntry,t.historyEntryState,n):function(e,t){if(location.href===e){const t=e+"?";history.replaceState(null,"",t),location.replace(e)}else t?location.replace(e):location.href=e}(e,t.replaceHistoryEntry)}async function Ne(e,t,n,r,o=!1){if(Re(),function(e){const t=e.indexOf("#");return t>-1&&location.href.replace(location.hash,"")===e.substring(0,t)}(e))!function(e,t,n){ke(e,t,n);const r=e.indexOf("#");r!==e.length-1&&Ce(e.substring(r+1))}(e,n,r);else{if(!o&&ge&&!await _e(e,r,t))return;pe=!0,ke(e,n,r),await Oe(t)}}function ke(e,t,n){t?history.replaceState({userState:n,_index:ye},"",e):(ye++,history.pushState({userState:n,_index:ye},"",e))}function Te(e){return new Promise((t=>{const n=Se;Se=()=>{Se=n,t()},history.go(e)}))}function Re(){Ie&&(Ie(!1),Ie=null)}function _e(e,t,n){return new Promise((r=>{Re(),Ee?(be++,Ie=r,Ee(be,e,t,n)):r(!1)}))}async function Oe(e){var t;we&&await we(location.href,null===(t=history.state)||void 0===t?void 0:t.userState,e)}async function Le(e){var t,n;Se&&await Se(e),ye=null!==(n=null===(t=history.state)||void 0===t?void 0:t._index)&&void 0!==n?n:0}let xe;function Fe(e){return xe=xe||document.createElement("a"),xe.href=e,xe.href}function Pe(e,t){return e?e.tagName===t?e:Pe(e.parentElement,t):null}function Me(e){const t=(n=document.baseURI).substring(0,n.lastIndexOf("/"));var n;const r=e.charAt(t.length);return e.startsWith(t)&&(""===r||"/"===r||"?"===r||"#"===r)}const Be={focus:function(e,t){if(e instanceof HTMLElement)e.focus({preventScroll:t});else{if(!(e instanceof SVGElement))throw new Error("Unable to focus an invalid element.");if(!e.hasAttribute("tabindex"))throw new Error("Unable to focus an SVG element that does not have a tabindex.");e.focus({preventScroll:t})}},focusBySelector:function(e,t){const n=document.querySelector(e);n&&(n.hasAttribute("tabindex")||(n.tabIndex=-1),n.focus({preventScroll:!0}))}},He={init:function(e,t,n,r=50){const o=Je(t);(o||document.documentElement).style.overflowAnchor="none";const a=document.createRange();u(n.parentElement)&&(t.style.display="table-row",n.style.display="table-row");const s=new IntersectionObserver((function(r){r.forEach((r=>{var o;if(!r.isIntersecting)return;a.setStartAfter(t),a.setEndBefore(n);const s=a.getBoundingClientRect().height,i=null===(o=r.rootBounds)||void 0===o?void 0:o.height;r.target===t?e.invokeMethodAsync("OnSpacerBeforeVisible",r.intersectionRect.top-r.boundingClientRect.top,s,i):r.target===n&&n.offsetHeight>0&&e.invokeMethodAsync("OnSpacerAfterVisible",r.boundingClientRect.bottom-r.intersectionRect.bottom,s,i)}))}),{root:o,rootMargin:`${r}px`});s.observe(t),s.observe(n);const i=l(t),c=l(n);function l(e){const t={attributes:!0},n=new MutationObserver(((n,r)=>{u(e.parentElement)&&(r.disconnect(),e.style.display="table-row",r.observe(e,t)),s.unobserve(e),s.observe(e)}));return n.observe(e,t),n}function u(e){return null!==e&&(e instanceof HTMLTableElement&&""===e.style.display||"table"===e.style.display||e instanceof HTMLTableSectionElement&&""===e.style.display||"table-row-group"===e.style.display)}je[e._id]={intersectionObserver:s,mutationObserverBefore:i,mutationObserverAfter:c}},dispose:function(e){const t=je[e._id];t&&(t.intersectionObserver.disconnect(),t.mutationObserverBefore.disconnect(),t.mutationObserverAfter.disconnect(),e.dispose(),delete je[e._id])}},je={};function Je(e){return e&&e!==document.body&&e!==document.documentElement?"visible"!==getComputedStyle(e).overflowY?e:Je(e.parentElement):null}const Ue={getAndRemoveExistingTitle:function(){var e;const t=document.head?document.head.getElementsByTagName("title"):[];if(0===t.length)return null;let n=null;for(let r=t.length-1;r>=0;r--){const o=t[r],a=o.previousSibling;a instanceof Comment&&null!==U(a)||(null===n&&(n=o.textContent),null===(e=o.parentNode)||void 0===e||e.removeChild(o))}return n}},$e={init:function(e,t){t._blazorInputFileNextFileId=0,t.addEventListener("click",(function(){t.value=""})),t.addEventListener("change",(function(){t._blazorFilesById={};const n=Array.prototype.map.call(t.files,(function(e){const n={id:++t._blazorInputFileNextFileId,lastModified:new Date(e.lastModified).toISOString(),name:e.name,size:e.size,contentType:e.type,readPromise:void 0,arrayBuffer:void 0,blob:e};return t._blazorFilesById[n.id]=n,n}));e.invokeMethodAsync("NotifyChange",n)}))},toImageFile:async function(e,t,n,r,o){const a=ze(e,t),s=await new Promise((function(e){const t=new Image;t.onload=function(){URL.revokeObjectURL(t.src),e(t)},t.onerror=function(){t.onerror=null,URL.revokeObjectURL(t.src)},t.src=URL.createObjectURL(a.blob)})),i=await new Promise((function(e){var t;const a=Math.min(1,r/s.width),i=Math.min(1,o/s.height),c=Math.min(a,i),l=document.createElement("canvas");l.width=Math.round(s.width*c),l.height=Math.round(s.height*c),null===(t=l.getContext("2d"))||void 0===t||t.drawImage(s,0,0,l.width,l.height),l.toBlob(e,n)})),c={id:++e._blazorInputFileNextFileId,lastModified:a.lastModified,name:a.name,size:(null==i?void 0:i.size)||0,contentType:n,blob:i||a.blob};return e._blazorFilesById[c.id]=c,c},readFileData:async function(e,t){return ze(e,t).blob}};function ze(e,t){const n=e._blazorFilesById[t];if(!n)throw new Error(`There is no file with ID ${t}. The file list may have changed. See https://aka.ms/aspnet/blazor-input-file-multiple-selections.`);return n}const Ke=new Set,Ve={enableNavigationPrompt:function(e){0===Ke.size&&window.addEventListener("beforeunload",Xe),Ke.add(e)},disableNavigationPrompt:function(e){Ke.delete(e),0===Ke.size&&window.removeEventListener("beforeunload",Xe)}};function Xe(e){e.preventDefault(),e.returnValue=!0}const We=new Map,Ye={navigateTo:function(e,t,n=!1){Ae(e,t instanceof Object?t:{forceLoad:t,replaceHistoryEntry:n})},registerCustomEventType:function(e,t){if(!t)throw new Error("The options parameter is required.");if(a.has(e))throw new Error(`The event '${e}' is already registered.`);if(t.browserEventName){const n=s.get(t.browserEventName);n?n.push(e):s.set(t.browserEventName,[e]),i.forEach((n=>n(e,t.browserEventName)))}a.set(e,t)},rootComponents:y,_internal:{navigationManager:De,domWrapper:Be,Virtualize:He,PageTitle:Ue,InputFile:$e,NavigationLock:Ve,getJSDataStreamChunk:async function(e,t,n){return e instanceof Blob?await async function(e,t,n){const r=e.slice(t,t+n),o=await r.arrayBuffer();return new Uint8Array(o)}(e,t,n):function(e,t,n){return new Uint8Array(e.buffer,e.byteOffset+t,n)}(e,t,n)},attachWebRendererInterop:function(t,n,r){const o=S.length;return S.push(t),Object.keys(n).length>0&&function(t,n,r){if(m)throw new Error("Dynamic root components have already been enabled.");m=t,v=n;for(const[t,o]of Object.entries(r)){const r=e.findJSFunction(t,0);for(const e of o)r(e,n[e])}}(A(o),n,r),I(),o}}};window.Blazor=Ye;let Ge=!1;const qe="function"==typeof TextDecoder?new TextDecoder("utf-8"):null,Ze=qe?qe.decode.bind(qe):function(e){let t=0;const n=e.length,r=[],o=[];for(;t65535&&(o-=65536,r.push(o>>>10&1023|55296),o=56320|1023&o),r.push(o)}r.length>1024&&(o.push(String.fromCharCode.apply(null,r)),r.length=0)}return o.push(String.fromCharCode.apply(null,r)),o.join("")},Qe=Math.pow(2,32),et=Math.pow(2,21)-1;function tt(e,t){return e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24}function nt(e,t){return e[t]+(e[t+1]<<8)+(e[t+2]<<16)+(e[t+3]<<24>>>0)}function rt(e,t){const n=nt(e,t+4);if(n>et)throw new Error(`Cannot read uint64 with high order part ${n}, because the result would exceed Number.MAX_SAFE_INTEGER.`);return n*Qe+nt(e,t)}class ot{constructor(e){this.batchData=e;const t=new ct(e);this.arrayRangeReader=new lt(e),this.arrayBuilderSegmentReader=new ut(e),this.diffReader=new at(e),this.editReader=new st(e,t),this.frameReader=new it(e,t)}updatedComponents(){return tt(this.batchData,this.batchData.length-20)}referenceFrames(){return tt(this.batchData,this.batchData.length-16)}disposedComponentIds(){return tt(this.batchData,this.batchData.length-12)}disposedEventHandlerIds(){return tt(this.batchData,this.batchData.length-8)}updatedComponentsEntry(e,t){const n=e+4*t;return tt(this.batchData,n)}referenceFramesEntry(e,t){return e+20*t}disposedComponentIdsEntry(e,t){const n=e+4*t;return tt(this.batchData,n)}disposedEventHandlerIdsEntry(e,t){const n=e+8*t;return rt(this.batchData,n)}}class at{constructor(e){this.batchDataUint8=e}componentId(e){return tt(this.batchDataUint8,e)}edits(e){return e+4}editsEntry(e,t){return e+16*t}}class st{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}editType(e){return tt(this.batchDataUint8,e)}siblingIndex(e){return tt(this.batchDataUint8,e+4)}newTreeIndex(e){return tt(this.batchDataUint8,e+8)}moveToSiblingIndex(e){return tt(this.batchDataUint8,e+8)}removedAttributeName(e){const t=tt(this.batchDataUint8,e+12);return this.stringReader.readString(t)}}class it{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}frameType(e){return tt(this.batchDataUint8,e)}subtreeLength(e){return tt(this.batchDataUint8,e+4)}elementReferenceCaptureId(e){const t=tt(this.batchDataUint8,e+4);return this.stringReader.readString(t)}componentId(e){return tt(this.batchDataUint8,e+8)}elementName(e){const t=tt(this.batchDataUint8,e+8);return this.stringReader.readString(t)}textContent(e){const t=tt(this.batchDataUint8,e+4);return this.stringReader.readString(t)}markupContent(e){const t=tt(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeName(e){const t=tt(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeValue(e){const t=tt(this.batchDataUint8,e+8);return this.stringReader.readString(t)}attributeEventHandlerId(e){return rt(this.batchDataUint8,e+12)}}class ct{constructor(e){this.batchDataUint8=e,this.stringTableStartIndex=tt(e,e.length-4)}readString(e){if(-1===e)return null;{const n=tt(this.batchDataUint8,this.stringTableStartIndex+4*e),r=function(e,t){let n=0,r=0;for(let o=0;o<4;o++){const a=e[t+o];if(n|=(127&a)<async function(e,n){const r=function(e){const t=document.baseURI;return t.endsWith("/")?`${t}${e}`:`${t}/${e}`}(n),o=await import(r);if(void 0===o)return;const{beforeStart:a,afterStarted:s}=o;return s&&e.afterStartedCallbacks.push(s),a?a(...t):void 0}(this,e))))}async invokeAfterStartedCallbacks(e){await D,await Promise.all(this.afterStartedCallbacks.map((t=>t(e))))}}let It,Dt=!1;async function Ct(){if(Dt)throw new Error("Blazor has already started.");Dt=!0;const t=await async function(){const e=await fetch("_framework/blazor.modules.json",{method:"GET",credentials:"include",cache:"no-cache"}),t=await e.json(),n=new St;return await n.importInitializersAsync(t,[]),n}();(function(){const e={AttachToDocument:(e,t)=>{!function(e,t,n){const r="::after",o="::before";let a=!1;if(e.endsWith(r))e=e.slice(0,-r.length),a=!0;else if(e.endsWith(o))throw new Error(`The '${o}' selector is not supported.`);const s=function(e){const t=p.get(e);if(t)return p.delete(e),t}(e)||document.querySelector(e);if(!s)throw new Error(`Could not find any element matching selector '${e}'.`);!function(e,t,n,r){let o=fe[0];o||(o=new ie(0),fe[0]=o),o.attachRootComponentToLogicalElement(n,t,r)}(0,B(s,!0),t,a)}(t,e)},RenderBatch:(e,t)=>{try{const n=Et(t);(function(e,t){const n=fe[0];if(!n)throw new Error("There is no browser renderer with ID 0.");const r=t.arrayRangeReader,o=t.updatedComponents(),a=r.values(o),s=r.count(o),i=t.referenceFrames(),c=r.values(i),l=t.diffReader;for(let e=0;e{ht=!0,console.error(`${e}\n${t}`),function(){const e=document.querySelector("#blazor-error-ui");e&&(e.style.display="block"),Ge||(Ge=!0,document.querySelectorAll("#blazor-error-ui .reload").forEach((e=>{e.onclick=function(e){location.reload(),e.preventDefault()}})),document.querySelectorAll("#blazor-error-ui .dismiss").forEach((e=>{e.onclick=function(e){const t=document.querySelector("#blazor-error-ui");t&&(t.style.display="none"),e.preventDefault()}})))}()},BeginInvokeJS:It.beginInvokeJSFromDotNet,EndInvokeDotNet:It.endInvokeDotNetFromJS,SendByteArrayToJS:wt,Navigate:De.navigateTo,SetHasLocationChangingListeners:De.setHasLocationChangingListeners,EndLocationChanging:De.endLocationChanging};window.external.receiveMessage((t=>{const n=function(e){if(ht||!e||!e.startsWith(dt))return null;const t=e.substring(dt.length),[n,...r]=JSON.parse(t);return{messageType:n,args:r}}(t);if(n){if(!Object.prototype.hasOwnProperty.call(e,n.messageType))throw new Error(`Unsupported IPC message type '${n.messageType}'`);e[n.messageType].apply(null,n.args)}}))})(),It=e.attachDispatcher({beginInvokeDotNetFromJS:pt,endInvokeJSFromDotNet:mt,sendByteArray:vt}),Ye._internal.receiveWebViewDotNetDataStream=At,De.enableNavigationInterception(),De.listenForNavigationEvents(gt,yt),bt("AttachPage",De.getBaseURI(),De.getLocationHref()),await t.invokeAfterStartedCallbacks(Ye)}function At(e,t,n,r){!function(e,t,n,r,o){let a=We.get(t);if(!a){const n=new ReadableStream({start(e){We.set(t,e),a=e}});e.supplyDotNetStream(t,n)}o?(a.error(o),We.delete(t)):0===r?(a.close(),We.delete(t)):a.enqueue(n.length===r?n:n.subarray(0,r))}(It,e,t,n,r)}Ye.start=Ct,window.DotNet=e,document&&document.currentScript&&"false"!==document.currentScript.getAttribute("autostart")&&Ct()})(); \ No newline at end of file +(()=>{"use strict";var e,t,n,r={d:(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};r.d({},{e:()=>It}),function(e){const t=[],n="__jsObjectId",r="__dotNetObject",o="__byte[]",a="__dotNetStream",s="__jsStreamReferenceLength";let i,c;class l{constructor(e){this._jsObject=e,this._cachedFunctions=new Map}findFunction(e){const t=this._cachedFunctions.get(e);if(t)return t;let n,r=this._jsObject;if(e.split(".").forEach((t=>{if(!(t in r))throw new Error(`Could not find '${e}' ('${t}' was undefined).`);n=r,r=r[t]})),r instanceof Function)return r=r.bind(n),this._cachedFunctions.set(e,r),r;throw new Error(`The value '${e}' is not a function.`)}getWrappedObject(){return this._jsObject}}const u={0:new l(window)};u[0]._cachedFunctions.set("import",(e=>("string"==typeof e&&e.startsWith("./")&&(e=new URL(e.substr(2),document.baseURI).toString()),import(e))));let d,h=1;function f(e){t.push(e)}function p(e){if(e&&"object"==typeof e){u[h]=new l(e);const t={[n]:h};return h++,t}throw new Error(`Cannot create a JSObjectReference from the value '${e}'.`)}function m(e){let t=-1;if(e instanceof ArrayBuffer&&(e=new Uint8Array(e)),e instanceof Blob)t=e.size;else{if(!(e.buffer instanceof ArrayBuffer))throw new Error("Supplied value is not a typed array or blob.");if(void 0===e.byteLength)throw new Error(`Cannot create a JSStreamReference from the value '${e}' as it doesn't have a byteLength.`);t=e.byteLength}const r={[s]:t};try{const t=p(e);r[n]=t[n]}catch(t){throw new Error(`Cannot create a JSStreamReference from the value '${e}'.`)}return r}function v(e,n){c=e;const r=n?JSON.parse(n,((e,n)=>t.reduce(((t,n)=>n(e,t)),n))):null;return c=void 0,r}function g(){if(void 0===i)throw new Error("No call dispatcher has been set.");if(null===i)throw new Error("There are multiple .NET runtimes present, so a default dispatcher could not be resolved. Use DotNetObject to invoke .NET instance methods.");return i}e.attachDispatcher=function(e){const t=new y(e);return void 0===i?i=t:i&&(i=null),t},e.attachReviver=f,e.invokeMethod=function(e,t,...n){return g().invokeDotNetStaticMethod(e,t,...n)},e.invokeMethodAsync=function(e,t,...n){return g().invokeDotNetStaticMethodAsync(e,t,...n)},e.createJSObjectReference=p,e.createJSStreamReference=m,e.disposeJSObjectReference=function(e){const t=e&&e[n];"number"==typeof t&&E(t)},function(e){e[e.Default=0]="Default",e[e.JSObjectReference=1]="JSObjectReference",e[e.JSStreamReference=2]="JSStreamReference",e[e.JSVoidResult=3]="JSVoidResult"}(d=e.JSCallResultType||(e.JSCallResultType={}));class y{constructor(e){this._dotNetCallDispatcher=e,this._byteArraysToBeRevived=new Map,this._pendingDotNetToJSStreams=new Map,this._pendingAsyncCalls={},this._nextAsyncCallId=1}getDotNetCallDispatcher(){return this._dotNetCallDispatcher}invokeJSFromDotNet(e,t,n,r){const o=v(this,t),a=C(w(e,r)(...o||[]),n);return null==a?null:N(this,a)}beginInvokeJSFromDotNet(e,t,n,r,o){const a=new Promise((e=>{const r=v(this,n);e(w(t,o)(...r||[]))}));e&&a.then((t=>N(this,[e,!0,C(t,r)]))).then((t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!0,t)),(t=>this._dotNetCallDispatcher.endInvokeJSFromDotNet(e,!1,JSON.stringify([e,!1,b(t)]))))}endInvokeDotNetFromJS(e,t,n){const r=t?v(this,n):new Error(n);this.completePendingCall(parseInt(e,10),t,r)}invokeDotNetStaticMethod(e,t,...n){return this.invokeDotNetMethod(e,t,null,n)}invokeDotNetStaticMethodAsync(e,t,...n){return this.invokeDotNetMethodAsync(e,t,null,n)}invokeDotNetMethod(e,t,n,r){if(this._dotNetCallDispatcher.invokeDotNetFromJS){const o=N(this,r),a=this._dotNetCallDispatcher.invokeDotNetFromJS(e,t,n,o);return a?v(this,a):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeDotNetMethodAsync instead.")}invokeDotNetMethodAsync(e,t,n,r){if(e&&n)throw new Error(`For instance method calls, assemblyName should be null. Received '${e}'.`);const o=this._nextAsyncCallId++,a=new Promise(((e,t)=>{this._pendingAsyncCalls[o]={resolve:e,reject:t}}));try{const a=N(this,r);this._dotNetCallDispatcher.beginInvokeDotNetFromJS(o,e,t,n,a)}catch(e){this.completePendingCall(o,!1,e)}return a}receiveByteArray(e,t){this._byteArraysToBeRevived.set(e,t)}processByteArray(e){const t=this._byteArraysToBeRevived.get(e);return t?(this._byteArraysToBeRevived.delete(e),t):null}supplyDotNetStream(e,t){if(this._pendingDotNetToJSStreams.has(e)){const n=this._pendingDotNetToJSStreams.get(e);this._pendingDotNetToJSStreams.delete(e),n.resolve(t)}else{const n=new D;n.resolve(t),this._pendingDotNetToJSStreams.set(e,n)}}getDotNetStreamPromise(e){let t;if(this._pendingDotNetToJSStreams.has(e))t=this._pendingDotNetToJSStreams.get(e).streamPromise,this._pendingDotNetToJSStreams.delete(e);else{const n=new D;this._pendingDotNetToJSStreams.set(e,n),t=n.streamPromise}return t}completePendingCall(e,t,n){if(!this._pendingAsyncCalls.hasOwnProperty(e))throw new Error(`There is no pending async call with ID ${e}.`);const r=this._pendingAsyncCalls[e];delete this._pendingAsyncCalls[e],t?r.resolve(n):r.reject(n)}}function b(e){return e instanceof Error?`${e.message}\n${e.stack}`:e?e.toString():"null"}function w(e,t){const n=u[t];if(n)return n.findFunction(e);throw new Error(`JS object instance with ID ${t} does not exist (has it been disposed?).`)}function E(e){delete u[e]}e.findJSFunction=w,e.disposeJSObjectReferenceById=E;class S{constructor(e,t){this._id=e,this._callDispatcher=t}invokeMethod(e,...t){return this._callDispatcher.invokeDotNetMethod(null,e,this._id,t)}invokeMethodAsync(e,...t){return this._callDispatcher.invokeDotNetMethodAsync(null,e,this._id,t)}dispose(){this._callDispatcher.invokeDotNetMethodAsync(null,"__Dispose",this._id,null).catch((e=>console.error(e)))}serializeAsArg(){return{[r]:this._id}}}e.DotNetObject=S,f((function(e,t){if(t&&"object"==typeof t){if(t.hasOwnProperty(r))return new S(t[r],c);if(t.hasOwnProperty(n)){const e=t[n],r=u[e];if(r)return r.getWrappedObject();throw new Error(`JS object instance with Id '${e}' does not exist. It may have been disposed.`)}if(t.hasOwnProperty(o)){const e=t[o],n=c.processByteArray(e);if(void 0===n)throw new Error(`Byte array index '${e}' does not exist.`);return n}if(t.hasOwnProperty(a)){const e=t[a],n=c.getDotNetStreamPromise(e);return new I(n)}}return t}));class I{constructor(e){this._streamPromise=e}stream(){return this._streamPromise}async arrayBuffer(){return new Response(await this.stream()).arrayBuffer()}}class D{constructor(){this.streamPromise=new Promise(((e,t)=>{this.resolve=e,this.reject=t}))}}function C(e,t){switch(t){case d.Default:return e;case d.JSObjectReference:return p(e);case d.JSStreamReference:return m(e);case d.JSVoidResult:return null;default:throw new Error(`Invalid JS call result type '${t}'.`)}}let A=0;function N(e,t){A=0,c=e;const n=JSON.stringify(t,k);return c=void 0,n}function k(e,t){if(t instanceof S)return t.serializeAsArg();if(t instanceof Uint8Array){c.getDotNetCallDispatcher().sendByteArray(A,t);const e={[o]:A};return A++,e}return t}}(e||(e={})),function(e){e[e.prependFrame=1]="prependFrame",e[e.removeFrame=2]="removeFrame",e[e.setAttribute=3]="setAttribute",e[e.removeAttribute=4]="removeAttribute",e[e.updateText=5]="updateText",e[e.stepIn=6]="stepIn",e[e.stepOut=7]="stepOut",e[e.updateMarkup=8]="updateMarkup",e[e.permutationListEntry=9]="permutationListEntry",e[e.permutationListEnd=10]="permutationListEnd"}(t||(t={})),function(e){e[e.element=1]="element",e[e.text=2]="text",e[e.attribute=3]="attribute",e[e.component=4]="component",e[e.region=5]="region",e[e.elementReferenceCapture=6]="elementReferenceCapture",e[e.markup=8]="markup"}(n||(n={}));class o{constructor(e,t){this.componentId=e,this.fieldValue=t}static fromEvent(e,t){const n=t.target;if(n instanceof Element){const t=function(e){return e instanceof HTMLInputElement?e.type&&"checkbox"===e.type.toLowerCase()?{value:e.checked}:{value:e.value}:e instanceof HTMLSelectElement||e instanceof HTMLTextAreaElement?{value:e.value}:null}(n);if(t)return new o(e,t.value)}return null}}const a=new Map,s=new Map,i=[];function c(e){return a.get(e)}function l(e){const t=a.get(e);return(null==t?void 0:t.browserEventName)||e}function u(e,t){e.forEach((e=>a.set(e,t)))}function d(e){const t=[];for(let n=0;ne.selected)).map((e=>e.value))}}{const e=function(e){return!!e&&"INPUT"===e.tagName&&"checkbox"===e.getAttribute("type")}(t);return{value:e?!!t.checked:t.value}}}}),u(["copy","cut","paste"],{createEventArgs:e=>({type:e.type})}),u(["drag","dragend","dragenter","dragleave","dragover","dragstart","drop"],{createEventArgs:e=>{return{...h(t=e),dataTransfer:t.dataTransfer?{dropEffect:t.dataTransfer.dropEffect,effectAllowed:t.dataTransfer.effectAllowed,files:Array.from(t.dataTransfer.files).map((e=>e.name)),items:Array.from(t.dataTransfer.items).map((e=>({kind:e.kind,type:e.type}))),types:t.dataTransfer.types}:null};var t}}),u(["focus","blur","focusin","focusout"],{createEventArgs:e=>({type:e.type})}),u(["keydown","keyup","keypress"],{createEventArgs:e=>{return{key:(t=e).key,code:t.code,location:t.location,repeat:t.repeat,ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),u(["contextmenu","click","mouseover","mouseout","mousemove","mousedown","mouseup","mouseleave","mouseenter","dblclick"],{createEventArgs:e=>h(e)}),u(["error"],{createEventArgs:e=>{return{message:(t=e).message,filename:t.filename,lineno:t.lineno,colno:t.colno,type:t.type};var t}}),u(["loadstart","timeout","abort","load","loadend","progress"],{createEventArgs:e=>{return{lengthComputable:(t=e).lengthComputable,loaded:t.loaded,total:t.total,type:t.type};var t}}),u(["touchcancel","touchend","touchmove","touchenter","touchleave","touchstart"],{createEventArgs:e=>{return{detail:(t=e).detail,touches:d(t.touches),targetTouches:d(t.targetTouches),changedTouches:d(t.changedTouches),ctrlKey:t.ctrlKey,shiftKey:t.shiftKey,altKey:t.altKey,metaKey:t.metaKey,type:t.type};var t}}),u(["gotpointercapture","lostpointercapture","pointercancel","pointerdown","pointerenter","pointerleave","pointermove","pointerout","pointerover","pointerup"],{createEventArgs:e=>{return{...h(t=e),pointerId:t.pointerId,width:t.width,height:t.height,pressure:t.pressure,tiltX:t.tiltX,tiltY:t.tiltY,pointerType:t.pointerType,isPrimary:t.isPrimary};var t}}),u(["wheel","mousewheel"],{createEventArgs:e=>{return{...h(t=e),deltaX:t.deltaX,deltaY:t.deltaY,deltaZ:t.deltaZ,deltaMode:t.deltaMode};var t}}),u(["toggle"],{createEventArgs:()=>({})});const f=["date","datetime-local","month","time","week"],p=new Map;let m,v,g=0;const y={async add(e,t,n){if(!n)throw new Error("initialParameters must be an object, even if empty.");const r="__bl-dynamic-root:"+(++g).toString();p.set(r,e);const o=await E().invokeMethodAsync("AddRootComponent",t,r),a=new w(o,v[t]);return await a.setParameters(n),a}};class b{invoke(e){return this._callback(e)}setCallback(t){this._selfJSObjectReference||(this._selfJSObjectReference=e.createJSObjectReference(this)),this._callback=t}getJSObjectReference(){return this._selfJSObjectReference}dispose(){this._selfJSObjectReference&&e.disposeJSObjectReference(this._selfJSObjectReference)}}class w{constructor(e,t){this._jsEventCallbackWrappers=new Map,this._componentId=e;for(const e of t)"eventcallback"===e.type&&this._jsEventCallbackWrappers.set(e.name.toLowerCase(),new b)}setParameters(e){const t={},n=Object.entries(e||{}),r=n.length;for(const[e,r]of n){const n=this._jsEventCallbackWrappers.get(e.toLowerCase());n&&r?(n.setCallback(r),t[e]=n.getJSObjectReference()):t[e]=r}return E().invokeMethodAsync("SetRootComponentParameters",this._componentId,r,t)}async dispose(){if(null!==this._componentId){await E().invokeMethodAsync("RemoveRootComponent",this._componentId),this._componentId=null;for(const e of this._jsEventCallbackWrappers.values())e.dispose()}}}function E(){if(!m)throw new Error("Dynamic root components have not been enabled in this application.");return m}const S=[];let I;const D=new Promise((e=>{I=e}));function C(e,t,n){return N(e,t.eventHandlerId,(()=>A(e).invokeMethodAsync("DispatchEventAsync",t,n)))}function A(e){const t=S[e];if(!t)throw new Error(`No interop methods are registered for renderer ${e}`);return t}let N=(e,t,n)=>n();const k=x(["abort","blur","canplay","canplaythrough","change","cuechange","durationchange","emptied","ended","error","focus","load","loadeddata","loadedmetadata","loadend","loadstart","mouseenter","mouseleave","pointerenter","pointerleave","pause","play","playing","progress","ratechange","reset","scroll","seeked","seeking","stalled","submit","suspend","timeupdate","toggle","unload","volumechange","waiting","DOMNodeInsertedIntoDocument","DOMNodeRemovedFromDocument"]),T={submit:!0},R=x(["click","dblclick","mousedown","mousemove","mouseup"]);class _{constructor(e){this.browserRendererId=e,this.afterClickCallbacks=[];const t=++_.nextEventDelegatorId;this.eventsCollectionKey=`_blazorEvents_${t}`,this.eventInfoStore=new O(this.onGlobalEvent.bind(this))}setListener(e,t,n,r){const o=this.getEventHandlerInfosForElement(e,!0),a=o.getHandler(t);if(a)this.eventInfoStore.update(a.eventHandlerId,n);else{const a={element:e,eventName:t,eventHandlerId:n,renderingComponentId:r};this.eventInfoStore.add(a),o.setHandler(t,a)}}getHandler(e){return this.eventInfoStore.get(e)}removeListener(e){const t=this.eventInfoStore.remove(e);if(t){const e=t.element,n=this.getEventHandlerInfosForElement(e,!1);n&&n.removeHandler(t.eventName)}}notifyAfterClick(e){this.afterClickCallbacks.push(e),this.eventInfoStore.addGlobalListener("click")}setStopPropagation(e,t,n){this.getEventHandlerInfosForElement(e,!0).stopPropagation(t,n)}setPreventDefault(e,t,n){this.getEventHandlerInfosForElement(e,!0).preventDefault(t,n)}onGlobalEvent(e){if(!(e.target instanceof Element))return;this.dispatchGlobalEventToAllElements(e.type,e);const t=(n=e.type,s.get(n));var n;t&&t.forEach((t=>this.dispatchGlobalEventToAllElements(t,e))),"click"===e.type&&this.afterClickCallbacks.forEach((t=>t(e)))}dispatchGlobalEventToAllElements(e,t){const n=t.composedPath();let r=n.shift(),a=null,s=!1;const i=Object.prototype.hasOwnProperty.call(k,e);let l=!1;for(;r;){const h=r,f=this.getEventHandlerInfosForElement(h,!1);if(f){const n=f.getHandler(e);if(n&&(u=h,d=t.type,!((u instanceof HTMLButtonElement||u instanceof HTMLInputElement||u instanceof HTMLTextAreaElement||u instanceof HTMLSelectElement)&&Object.prototype.hasOwnProperty.call(R,d)&&u.disabled))){if(!s){const n=c(e);a=(null==n?void 0:n.createEventArgs)?n.createEventArgs(t):{},s=!0}Object.prototype.hasOwnProperty.call(T,t.type)&&t.preventDefault(),C(this.browserRendererId,{eventHandlerId:n.eventHandlerId,eventName:e,eventFieldInfo:o.fromEvent(n.renderingComponentId,t)},a)}f.stopPropagation(e)&&(l=!0),f.preventDefault(e)&&t.preventDefault()}r=i||l?void 0:n.shift()}var u,d}getEventHandlerInfosForElement(e,t){return Object.prototype.hasOwnProperty.call(e,this.eventsCollectionKey)?e[this.eventsCollectionKey]:t?e[this.eventsCollectionKey]=new L:null}}_.nextEventDelegatorId=0;class O{constructor(e){this.globalListener=e,this.infosByEventHandlerId={},this.countByEventName={},i.push(this.handleEventNameAliasAdded.bind(this))}add(e){if(this.infosByEventHandlerId[e.eventHandlerId])throw new Error(`Event ${e.eventHandlerId} is already tracked`);this.infosByEventHandlerId[e.eventHandlerId]=e,this.addGlobalListener(e.eventName)}get(e){return this.infosByEventHandlerId[e]}addGlobalListener(e){if(e=l(e),Object.prototype.hasOwnProperty.call(this.countByEventName,e))this.countByEventName[e]++;else{this.countByEventName[e]=1;const t=Object.prototype.hasOwnProperty.call(k,e);document.addEventListener(e,this.globalListener,t)}}update(e,t){if(Object.prototype.hasOwnProperty.call(this.infosByEventHandlerId,t))throw new Error(`Event ${t} is already tracked`);const n=this.infosByEventHandlerId[e];delete this.infosByEventHandlerId[e],n.eventHandlerId=t,this.infosByEventHandlerId[t]=n}remove(e){const t=this.infosByEventHandlerId[e];if(t){delete this.infosByEventHandlerId[e];const n=l(t.eventName);0==--this.countByEventName[n]&&(delete this.countByEventName[n],document.removeEventListener(n,this.globalListener))}return t}handleEventNameAliasAdded(e,t){if(Object.prototype.hasOwnProperty.call(this.countByEventName,e)){const n=this.countByEventName[e];delete this.countByEventName[e],document.removeEventListener(e,this.globalListener),this.addGlobalListener(t),this.countByEventName[t]+=n-1}}}class L{constructor(){this.handlers={},this.preventDefaultFlags=null,this.stopPropagationFlags=null}getHandler(e){return Object.prototype.hasOwnProperty.call(this.handlers,e)?this.handlers[e]:null}setHandler(e,t){this.handlers[e]=t}removeHandler(e){delete this.handlers[e]}preventDefault(e,t){return void 0!==t&&(this.preventDefaultFlags=this.preventDefaultFlags||{},this.preventDefaultFlags[e]=t),!!this.preventDefaultFlags&&this.preventDefaultFlags[e]}stopPropagation(e,t){return void 0!==t&&(this.stopPropagationFlags=this.stopPropagationFlags||{},this.stopPropagationFlags[e]=t),!!this.stopPropagationFlags&&this.stopPropagationFlags[e]}}function x(e){const t={};return e.forEach((e=>{t[e]=!0})),t}const F=q("_blazorLogicalChildren"),P=q("_blazorLogicalParent"),M=q("_blazorLogicalEnd");function B(e,t){if(e.childNodes.length>0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return F in e||(e[F]=[]),e}function j(e,t){const n=document.createComment("!");return H(n,e,t),n}function H(e,t,n){const r=e;if(e instanceof Comment&&K(r)&&K(r).length>0)throw new Error("Not implemented: inserting non-empty logical container");if(U(r))throw new Error("Not implemented: moving existing logical children");const o=K(t);if(n0;)J(n,0)}const r=n;r.parentNode.removeChild(r)}function U(e){return e[P]||null}function $(e,t){return K(e)[t]}function z(e){const t=X(e);return"http://www.w3.org/2000/svg"===t.namespaceURI&&"foreignObject"!==t.tagName}function K(e){return e[F]}function V(e,t){const n=K(e);t.forEach((e=>{e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=G(e.moveRangeStart)})),t.forEach((t=>{const r=document.createComment("marker");t.moveToBeforeMarker=r;const o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):Y(r,e)})),t.forEach((e=>{const t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd;let a=r;for(;a;){const e=a.nextSibling;if(n.insertBefore(a,t),a===o)break;a=e}n.removeChild(t)})),t.forEach((e=>{n[e.toSiblingIndex]=e.moveRangeStart}))}function X(e){if(e instanceof Element||e instanceof DocumentFragment)return e;if(e instanceof Comment)return e.parentNode;throw new Error("Not a valid logical element")}function W(e){const t=K(U(e));return t[Array.prototype.indexOf.call(t,e)+1]||null}function Y(e,t){if(t instanceof Element||t instanceof DocumentFragment)t.appendChild(e);else{if(!(t instanceof Comment))throw new Error(`Cannot append node because the parent is not a valid logical element. Parent: ${t}`);{const n=W(t);n?n.parentNode.insertBefore(e,n):Y(e,U(t))}}}function G(e){if(e instanceof Element||e instanceof DocumentFragment)return e;const t=W(e);if(t)return t.previousSibling;{const t=U(e);return t instanceof Element||t instanceof DocumentFragment?t.lastChild:G(t)}}function q(e){return"function"==typeof Symbol?Symbol():e}function Z(e){return`_bl_${e}`}const Q="__internalId";e.attachReviver(((e,t)=>t&&"object"==typeof t&&Object.prototype.hasOwnProperty.call(t,Q)&&"string"==typeof t[Q]?function(e){const t=`[${Z(e)}]`;return document.querySelector(t)}(t[Q]):t));const ee="_blazorDeferredValue",te=document.createElement("template"),ne=document.createElementNS("http://www.w3.org/2000/svg","g"),re={},oe="__internal_",ae="preventDefault_",se="stopPropagation_";class ie{constructor(e){this.rootComponentIds=new Set,this.childComponentLocations={},this.eventDelegator=new _(e),this.eventDelegator.notifyAfterClick((e=>{if(!me)return;if(0!==e.button||function(e){return e.ctrlKey||e.shiftKey||e.altKey||e.metaKey}(e))return;if(e.defaultPrevented)return;const t=function(e){const t=!window._blazorDisableComposedPath&&e.composedPath&&e.composedPath();if(t){for(let e=0;edocument.baseURI,getLocationHref:()=>location.href,scrollToElement:Ce};function Ce(e){const t=document.getElementById(e);return!!t&&(t.scrollIntoView(),!0)}function Ae(e,t,n=!1){const r=Fe(e);!t.forceLoad&&Me(r)?Ne(r,!1,t.replaceHistoryEntry,t.historyEntryState,n):function(e,t){if(location.href===e){const t=e+"?";history.replaceState(null,"",t),location.replace(e)}else t?location.replace(e):location.href=e}(e,t.replaceHistoryEntry)}async function Ne(e,t,n,r,o=!1){if(Re(),function(e){const t=e.indexOf("#");return t>-1&&location.href.replace(location.hash,"")===e.substring(0,t)}(e))!function(e,t,n){ke(e,t,n);const r=e.indexOf("#");r!==e.length-1&&Ce(e.substring(r+1))}(e,n,r);else{if(!o&&ge&&!await _e(e,r,t))return;pe=!0,ke(e,n,r),await Oe(t)}}function ke(e,t,n){t?history.replaceState({userState:n,_index:ye},"",e):(ye++,history.pushState({userState:n,_index:ye},"",e))}function Te(e){return new Promise((t=>{const n=Se;Se=()=>{Se=n,t()},history.go(e)}))}function Re(){Ie&&(Ie(!1),Ie=null)}function _e(e,t,n){return new Promise((r=>{Re(),Ee?(be++,Ie=r,Ee(be,e,t,n)):r(!1)}))}async function Oe(e){var t;we&&await we(location.href,null===(t=history.state)||void 0===t?void 0:t.userState,e)}async function Le(e){var t,n;Se&&await Se(e),ye=null!==(n=null===(t=history.state)||void 0===t?void 0:t._index)&&void 0!==n?n:0}let xe;function Fe(e){return xe=xe||document.createElement("a"),xe.href=e,xe.href}function Pe(e,t){return e?e.tagName===t?e:Pe(e.parentElement,t):null}function Me(e){const t=(n=document.baseURI).substring(0,n.lastIndexOf("/"));var n;const r=e.charAt(t.length);return e.startsWith(t)&&(""===r||"/"===r||"?"===r||"#"===r)}const Be={focus:function(e,t){if(e instanceof HTMLElement)e.focus({preventScroll:t});else{if(!(e instanceof SVGElement))throw new Error("Unable to focus an invalid element.");if(!e.hasAttribute("tabindex"))throw new Error("Unable to focus an SVG element that does not have a tabindex.");e.focus({preventScroll:t})}},focusBySelector:function(e,t){const n=document.querySelector(e);n&&(n.hasAttribute("tabindex")||(n.tabIndex=-1),n.focus({preventScroll:!0}))}},je={init:function(e,t,n,r=50){const o=Je(t);(o||document.documentElement).style.overflowAnchor="none";const a=document.createRange();u(n.parentElement)&&(t.style.display="table-row",n.style.display="table-row");const s=new IntersectionObserver((function(r){r.forEach((r=>{var o;if(!r.isIntersecting)return;a.setStartAfter(t),a.setEndBefore(n);const s=a.getBoundingClientRect().height,i=null===(o=r.rootBounds)||void 0===o?void 0:o.height;r.target===t?e.invokeMethodAsync("OnSpacerBeforeVisible",r.intersectionRect.top-r.boundingClientRect.top,s,i):r.target===n&&n.offsetHeight>0&&e.invokeMethodAsync("OnSpacerAfterVisible",r.boundingClientRect.bottom-r.intersectionRect.bottom,s,i)}))}),{root:o,rootMargin:`${r}px`});s.observe(t),s.observe(n);const i=l(t),c=l(n);function l(e){const t={attributes:!0},n=new MutationObserver(((n,r)=>{u(e.parentElement)&&(r.disconnect(),e.style.display="table-row",r.observe(e,t)),s.unobserve(e),s.observe(e)}));return n.observe(e,t),n}function u(e){return null!==e&&(e instanceof HTMLTableElement&&""===e.style.display||"table"===e.style.display||e instanceof HTMLTableSectionElement&&""===e.style.display||"table-row-group"===e.style.display)}He[e._id]={intersectionObserver:s,mutationObserverBefore:i,mutationObserverAfter:c}},dispose:function(e){const t=He[e._id];t&&(t.intersectionObserver.disconnect(),t.mutationObserverBefore.disconnect(),t.mutationObserverAfter.disconnect(),e.dispose(),delete He[e._id])}},He={};function Je(e){return e&&e!==document.body&&e!==document.documentElement?"visible"!==getComputedStyle(e).overflowY?e:Je(e.parentElement):null}const Ue={getAndRemoveExistingTitle:function(){var e;const t=document.head?document.head.getElementsByTagName("title"):[];if(0===t.length)return null;let n=null;for(let r=t.length-1;r>=0;r--){const o=t[r],a=o.previousSibling;a instanceof Comment&&null!==U(a)||(null===n&&(n=o.textContent),null===(e=o.parentNode)||void 0===e||e.removeChild(o))}return n}},$e={init:function(e,t){t._blazorInputFileNextFileId=0,t.addEventListener("click",(function(){t.value=""})),t.addEventListener("change",(function(){t._blazorFilesById={};const n=Array.prototype.map.call(t.files,(function(e){const n={id:++t._blazorInputFileNextFileId,lastModified:new Date(e.lastModified).toISOString(),name:e.name,size:e.size,contentType:e.type,readPromise:void 0,arrayBuffer:void 0,blob:e};return t._blazorFilesById[n.id]=n,n}));e.invokeMethodAsync("NotifyChange",n)}))},toImageFile:async function(e,t,n,r,o){const a=ze(e,t),s=await new Promise((function(e){const t=new Image;t.onload=function(){URL.revokeObjectURL(t.src),e(t)},t.onerror=function(){t.onerror=null,URL.revokeObjectURL(t.src)},t.src=URL.createObjectURL(a.blob)})),i=await new Promise((function(e){var t;const a=Math.min(1,r/s.width),i=Math.min(1,o/s.height),c=Math.min(a,i),l=document.createElement("canvas");l.width=Math.round(s.width*c),l.height=Math.round(s.height*c),null===(t=l.getContext("2d"))||void 0===t||t.drawImage(s,0,0,l.width,l.height),l.toBlob(e,n)})),c={id:++e._blazorInputFileNextFileId,lastModified:a.lastModified,name:a.name,size:(null==i?void 0:i.size)||0,contentType:n,blob:i||a.blob};return e._blazorFilesById[c.id]=c,c},readFileData:async function(e,t){return ze(e,t).blob}};function ze(e,t){const n=e._blazorFilesById[t];if(!n)throw new Error(`There is no file with ID ${t}. The file list may have changed. See https://aka.ms/aspnet/blazor-input-file-multiple-selections.`);return n}const Ke=new Set,Ve={enableNavigationPrompt:function(e){0===Ke.size&&window.addEventListener("beforeunload",Xe),Ke.add(e)},disableNavigationPrompt:function(e){Ke.delete(e),0===Ke.size&&window.removeEventListener("beforeunload",Xe)}};function Xe(e){e.preventDefault(),e.returnValue=!0}const We=new Map,Ye={navigateTo:function(e,t,n=!1){Ae(e,t instanceof Object?t:{forceLoad:t,replaceHistoryEntry:n})},registerCustomEventType:function(e,t){if(!t)throw new Error("The options parameter is required.");if(a.has(e))throw new Error(`The event '${e}' is already registered.`);if(t.browserEventName){const n=s.get(t.browserEventName);n?n.push(e):s.set(t.browserEventName,[e]),i.forEach((n=>n(e,t.browserEventName)))}a.set(e,t)},rootComponents:y,_internal:{navigationManager:De,domWrapper:Be,Virtualize:je,PageTitle:Ue,InputFile:$e,NavigationLock:Ve,getJSDataStreamChunk:async function(e,t,n){return e instanceof Blob?await async function(e,t,n){const r=e.slice(t,t+n),o=await r.arrayBuffer();return new Uint8Array(o)}(e,t,n):function(e,t,n){return new Uint8Array(e.buffer,e.byteOffset+t,n)}(e,t,n)},attachWebRendererInterop:function(t,n,r){const o=S.length;return S.push(t),Object.keys(n).length>0&&function(t,n,r){if(m)throw new Error("Dynamic root components have already been enabled.");m=t,v=n;for(const[t,o]of Object.entries(r)){const r=e.findJSFunction(t,0);for(const e of o)r(e,n[e])}}(A(o),n,r),I(),o}}};window.Blazor=Ye;let Ge=!1;const qe="function"==typeof TextDecoder?new TextDecoder("utf-8"):null,Ze=qe?qe.decode.bind(qe):function(e){let t=0;const n=e.length,r=[],o=[];for(;t65535&&(o-=65536,r.push(o>>>10&1023|55296),o=56320|1023&o),r.push(o)}r.length>1024&&(o.push(String.fromCharCode.apply(null,r)),r.length=0)}return o.push(String.fromCharCode.apply(null,r)),o.join("")},Qe=Math.pow(2,32),et=Math.pow(2,21)-1;function tt(e,t){return e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24}function nt(e,t){return e[t]+(e[t+1]<<8)+(e[t+2]<<16)+(e[t+3]<<24>>>0)}function rt(e,t){const n=nt(e,t+4);if(n>et)throw new Error(`Cannot read uint64 with high order part ${n}, because the result would exceed Number.MAX_SAFE_INTEGER.`);return n*Qe+nt(e,t)}class ot{constructor(e){this.batchData=e;const t=new ct(e);this.arrayRangeReader=new lt(e),this.arrayBuilderSegmentReader=new ut(e),this.diffReader=new at(e),this.editReader=new st(e,t),this.frameReader=new it(e,t)}updatedComponents(){return tt(this.batchData,this.batchData.length-20)}referenceFrames(){return tt(this.batchData,this.batchData.length-16)}disposedComponentIds(){return tt(this.batchData,this.batchData.length-12)}disposedEventHandlerIds(){return tt(this.batchData,this.batchData.length-8)}updatedComponentsEntry(e,t){const n=e+4*t;return tt(this.batchData,n)}referenceFramesEntry(e,t){return e+20*t}disposedComponentIdsEntry(e,t){const n=e+4*t;return tt(this.batchData,n)}disposedEventHandlerIdsEntry(e,t){const n=e+8*t;return rt(this.batchData,n)}}class at{constructor(e){this.batchDataUint8=e}componentId(e){return tt(this.batchDataUint8,e)}edits(e){return e+4}editsEntry(e,t){return e+16*t}}class st{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}editType(e){return tt(this.batchDataUint8,e)}siblingIndex(e){return tt(this.batchDataUint8,e+4)}newTreeIndex(e){return tt(this.batchDataUint8,e+8)}moveToSiblingIndex(e){return tt(this.batchDataUint8,e+8)}removedAttributeName(e){const t=tt(this.batchDataUint8,e+12);return this.stringReader.readString(t)}}class it{constructor(e,t){this.batchDataUint8=e,this.stringReader=t}frameType(e){return tt(this.batchDataUint8,e)}subtreeLength(e){return tt(this.batchDataUint8,e+4)}elementReferenceCaptureId(e){const t=tt(this.batchDataUint8,e+4);return this.stringReader.readString(t)}componentId(e){return tt(this.batchDataUint8,e+8)}elementName(e){const t=tt(this.batchDataUint8,e+8);return this.stringReader.readString(t)}textContent(e){const t=tt(this.batchDataUint8,e+4);return this.stringReader.readString(t)}markupContent(e){const t=tt(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeName(e){const t=tt(this.batchDataUint8,e+4);return this.stringReader.readString(t)}attributeValue(e){const t=tt(this.batchDataUint8,e+8);return this.stringReader.readString(t)}attributeEventHandlerId(e){return rt(this.batchDataUint8,e+12)}}class ct{constructor(e){this.batchDataUint8=e,this.stringTableStartIndex=tt(e,e.length-4)}readString(e){if(-1===e)return null;{const n=tt(this.batchDataUint8,this.stringTableStartIndex+4*e),r=function(e,t){let n=0,r=0;for(let o=0;o<4;o++){const a=e[t+o];if(n|=(127&a)<async function(e,n){const r=function(e){const t=document.baseURI;return t.endsWith("/")?`${t}${e}`:`${t}/${e}`}(n),o=await import(r);if(void 0===o)return;const{beforeStart:a,afterStarted:s}=o;return s&&e.afterStartedCallbacks.push(s),a?a(...t):void 0}(this,e))))}async invokeAfterStartedCallbacks(e){await D,await Promise.all(this.afterStartedCallbacks.map((t=>t(e))))}}let It,Dt=!1;async function Ct(){if(Dt)throw new Error("Blazor has already started.");Dt=!0,It=e.attachDispatcher({beginInvokeDotNetFromJS:pt,endInvokeJSFromDotNet:mt,sendByteArray:vt});const t=await async function(){const e=await fetch("_framework/blazor.modules.json",{method:"GET",credentials:"include",cache:"no-cache"}),t=await e.json(),n=new St;return await n.importInitializersAsync(t,[]),n}();(function(){const e={AttachToDocument:(e,t)=>{!function(e,t,n){const r="::after",o="::before";let a=!1;if(e.endsWith(r))e=e.slice(0,-r.length),a=!0;else if(e.endsWith(o))throw new Error(`The '${o}' selector is not supported.`);const s=function(e){const t=p.get(e);if(t)return p.delete(e),t}(e)||document.querySelector(e);if(!s)throw new Error(`Could not find any element matching selector '${e}'.`);!function(e,t,n,r){let o=fe[0];o||(o=new ie(0),fe[0]=o),o.attachRootComponentToLogicalElement(n,t,r)}(0,B(s,!0),t,a)}(t,e)},RenderBatch:(e,t)=>{try{const n=Et(t);(function(e,t){const n=fe[0];if(!n)throw new Error("There is no browser renderer with ID 0.");const r=t.arrayRangeReader,o=t.updatedComponents(),a=r.values(o),s=r.count(o),i=t.referenceFrames(),c=r.values(i),l=t.diffReader;for(let e=0;e{ht=!0,console.error(`${e}\n${t}`),function(){const e=document.querySelector("#blazor-error-ui");e&&(e.style.display="block"),Ge||(Ge=!0,document.querySelectorAll("#blazor-error-ui .reload").forEach((e=>{e.onclick=function(e){location.reload(),e.preventDefault()}})),document.querySelectorAll("#blazor-error-ui .dismiss").forEach((e=>{e.onclick=function(e){const t=document.querySelector("#blazor-error-ui");t&&(t.style.display="none"),e.preventDefault()}})))}()},BeginInvokeJS:It.beginInvokeJSFromDotNet.bind(It),EndInvokeDotNet:It.endInvokeDotNetFromJS.bind(It),SendByteArrayToJS:wt,Navigate:De.navigateTo,SetHasLocationChangingListeners:De.setHasLocationChangingListeners,EndLocationChanging:De.endLocationChanging};window.external.receiveMessage((t=>{const n=function(e){if(ht||!e||!e.startsWith(dt))return null;const t=e.substring(dt.length),[n,...r]=JSON.parse(t);return{messageType:n,args:r}}(t);if(n){if(!Object.prototype.hasOwnProperty.call(e,n.messageType))throw new Error(`Unsupported IPC message type '${n.messageType}'`);e[n.messageType].apply(null,n.args)}}))})(),Ye._internal.receiveWebViewDotNetDataStream=At,De.enableNavigationInterception(),De.listenForNavigationEvents(gt,yt),bt("AttachPage",De.getBaseURI(),De.getLocationHref()),await t.invokeAfterStartedCallbacks(Ye)}function At(e,t,n,r){!function(e,t,n,r,o){let a=We.get(t);if(!a){const n=new ReadableStream({start(e){We.set(t,e),a=e}});e.supplyDotNetStream(t,n)}o?(a.error(o),We.delete(t)):0===r?(a.close(),We.delete(t)):a.enqueue(n.length===r?n:n.subarray(0,r))}(It,e,t,n,r)}Ye.start=Ct,window.DotNet=e,document&&document.currentScript&&"false"!==document.currentScript.getAttribute("autostart")&&Ct()})(); \ No newline at end of file diff --git a/src/Components/Web.JS/src/Boot.WebView.ts b/src/Components/Web.JS/src/Boot.WebView.ts index e9390d010ae8..1812faf8eacb 100644 --- a/src/Components/Web.JS/src/Boot.WebView.ts +++ b/src/Components/Web.JS/src/Boot.WebView.ts @@ -20,16 +20,16 @@ async function boot(): Promise { } started = true; - const jsInitializer = await fetchAndInvokeInitializers(); - - startIpcReceiver(); - dispatcher = DotNet.attachDispatcher({ beginInvokeDotNetFromJS: sendBeginInvokeDotNetFromJS, endInvokeJSFromDotNet: sendEndInvokeJSFromDotNet, sendByteArray: sendByteArray, }); + const jsInitializer = await fetchAndInvokeInitializers(); + + startIpcReceiver(); + Blazor._internal.receiveWebViewDotNetDataStream = receiveWebViewDotNetDataStream; navigationManagerFunctions.enableNavigationInterception(); diff --git a/src/Components/Web.JS/src/Platform/WebView/WebViewIpcReceiver.ts b/src/Components/Web.JS/src/Platform/WebView/WebViewIpcReceiver.ts index 27002cb54760..2849eee20f56 100644 --- a/src/Components/Web.JS/src/Platform/WebView/WebViewIpcReceiver.ts +++ b/src/Components/Web.JS/src/Platform/WebView/WebViewIpcReceiver.ts @@ -33,9 +33,9 @@ export function startIpcReceiver(): void { showErrorNotification(); }, - 'BeginInvokeJS': dispatcher.beginInvokeJSFromDotNet, + 'BeginInvokeJS': dispatcher.beginInvokeJSFromDotNet.bind(dispatcher), - 'EndInvokeDotNet': dispatcher.endInvokeDotNetFromJS, + 'EndInvokeDotNet': dispatcher.endInvokeDotNetFromJS.bind(dispatcher), 'SendByteArrayToJS': receiveBase64ByteArray, diff --git a/src/Components/Web/src/Binding/BindingEditContextExtensions.cs b/src/Components/Web/src/Binding/BindingEditContextExtensions.cs new file mode 100644 index 000000000000..3a430bef7393 --- /dev/null +++ b/src/Components/Web/src/Binding/BindingEditContextExtensions.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components.Forms; + +namespace Microsoft.AspNetCore.Components.Binding; + +internal static class BindingEditContextExtensions +{ + private static readonly object _convertibleTypesKey = new object(); + + public static void SetConvertibleValues( + this EditContext context, + ModelBindingContext binding) + { + context.Properties[_convertibleTypesKey] = (Predicate)binding.CanConvert; + } + + public static Predicate? GetConvertibleValues(this EditContext context) + { + return context.Properties.TryGetValue(_convertibleTypesKey, out var result) ? (Predicate)result : null; + } +} diff --git a/src/Components/Web/src/Forms/EditForm.cs b/src/Components/Web/src/Forms/EditForm.cs index 0eb80df1e40d..0e4a53bfecf5 100644 --- a/src/Components/Web/src/Forms/EditForm.cs +++ b/src/Components/Web/src/Forms/EditForm.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms; @@ -121,6 +122,11 @@ protected override void OnParametersSet() { _editContext = new EditContext(Model!); } + + if (_editContext != null && BindingContext != null) + { + _editContext.SetConvertibleValues(BindingContext); + } } /// diff --git a/src/Components/Web/src/Forms/ExpressionFormatting/ExpressionFormatter.cs b/src/Components/Web/src/Forms/ExpressionFormatting/ExpressionFormatter.cs index e3fc1b199616..eb62618d2391 100644 --- a/src/Components/Web/src/Forms/ExpressionFormatting/ExpressionFormatter.cs +++ b/src/Components/Web/src/Forms/ExpressionFormatting/ExpressionFormatter.cs @@ -23,7 +23,7 @@ public static void ClearCache() s_methodInfoDataCache.Clear(); } - public static string FormatLambda(LambdaExpression expression) + public static string FormatLambda(LambdaExpression expression, Predicate? canConvertDirectly = null) { var builder = new ReverseStringBuilder(stackalloc char[StackAllocBufferSize]); var node = expression.Body; @@ -74,6 +74,13 @@ public static string FormatLambda(LambdaExpression expression) if (nextNode?.NodeType == ExpressionType.Constant) { + // Special case primitive values that are bound directly from the form. + // By convention, the name for the field will be "value". + if (canConvertDirectly?.Invoke(memberExpression.Type) == true && + memberExpression.Member.IsDefined(typeof(SupplyParameterFromFormAttribute), inherit: false)) + { + builder.InsertFront("value"); + } // The next node has a compiler-generated closure type, // which means the current member access is on the captured model. // We don't want to include the model variable name in the generated @@ -165,7 +172,7 @@ static MethodInfoData GetMethodInfoData(MethodInfo methodInfo) return new(IsSingleArgumentIndexer: false); } } - + private static void FormatIndexArgument( Expression indexExpression, ref ReverseStringBuilder builder) diff --git a/src/Components/Web/src/Forms/InputBase.cs b/src/Components/Web/src/Forms/InputBase.cs index 6041ddee9a4c..6a2113eab05c 100644 --- a/src/Components/Web/src/Forms/InputBase.cs +++ b/src/Components/Web/src/Forms/InputBase.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; +using Microsoft.AspNetCore.Components.Binding; namespace Microsoft.AspNetCore.Components.Forms; @@ -205,7 +206,9 @@ protected string NameAttributeValue { if (_formattedValueExpression is null && ValueExpression is not null) { - _formattedValueExpression = ExpressionFormatter.FormatLambda(ValueExpression); + _formattedValueExpression = ExpressionFormatter.FormatLambda( + ValueExpression, + EditContext.GetConvertibleValues()); } return _formattedValueExpression ?? string.Empty; diff --git a/src/Components/Web/src/Forms/InputCheckbox.cs b/src/Components/Web/src/Forms/InputCheckbox.cs index ad78a4ff8074..9538b031960e 100644 --- a/src/Components/Web/src/Forms/InputCheckbox.cs +++ b/src/Components/Web/src/Forms/InputCheckbox.cs @@ -37,9 +37,14 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddAttributeIfNotNullOrEmpty(3, "name", NameAttributeValue); builder.AddAttribute(4, "class", CssClass); builder.AddAttribute(5, "checked", BindConverter.FormatValue(CurrentValue)); - builder.AddAttribute(6, "onchange", EventCallback.Factory.CreateBinder(this, __value => CurrentValue = __value, CurrentValue)); + // Include the "value" attribute so that when this is posted by a form, "true" + // is included in the form fields. That's how works normally. + // It sends the "on" value when the checkbox is checked, and nothing otherwise. + builder.AddAttribute(6, "value", bool.TrueString); + + builder.AddAttribute(7, "onchange", EventCallback.Factory.CreateBinder(this, __value => CurrentValue = __value, CurrentValue)); builder.SetUpdatesAttributeName("checked"); - builder.AddElementReferenceCapture(7, __inputReference => Element = __inputReference); + builder.AddElementReferenceCapture(8, __inputReference => Element = __inputReference); builder.CloseElement(); } diff --git a/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj index eab4198dbc34..ba1fdcd2100d 100644 --- a/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj +++ b/src/Components/Web/src/Microsoft.AspNetCore.Components.Web.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index dbeb2dc911e7..780363a5e104 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -17,6 +17,10 @@ Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer. Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.BeginRenderingComponent(System.Type! componentType, Microsoft.AspNetCore.Components.ParameterView initialParameters) -> Microsoft.AspNetCore.Components.Web.HtmlRendering.HtmlRootComponent Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.StaticHtmlRenderer(System.IServiceProvider! serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Components.RenderTree.WebRenderer.WaitUntilAttachedAsync() -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.get -> string? +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.Name.set -> void +Microsoft.AspNetCore.Components.SupplyParameterFromFormAttribute.SupplyParameterFromFormAttribute() -> void Microsoft.AspNetCore.Components.Web.AutoRenderMode Microsoft.AspNetCore.Components.Web.AutoRenderMode.AutoRenderMode() -> void Microsoft.AspNetCore.Components.Web.AutoRenderMode.AutoRenderMode(bool prerender) -> void diff --git a/src/Components/Web/src/SupplyParameterFromFormAttribute.cs b/src/Components/Web/src/SupplyParameterFromFormAttribute.cs new file mode 100644 index 000000000000..2ae657c95583 --- /dev/null +++ b/src/Components/Web/src/SupplyParameterFromFormAttribute.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +/// +/// Indicates that the value of the associated property should be supplied from +/// the form data for the form with the specified name. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public sealed class SupplyParameterFromFormAttribute : Attribute, IHostEnvironmentCascadingParameter +{ + /// + /// Gets or sets the name for the parameter. The name is used to match + /// the form data and decide whether or not the value needs to be bound. + /// + public string? Name { get; set; } +} diff --git a/src/Components/Web/test/Forms/EditFormTest.cs b/src/Components/Web/test/Forms/EditFormTest.cs index ae08af1ccdf9..59f449e95a21 100644 --- a/src/Components/Web/test/Forms/EditFormTest.cs +++ b/src/Components/Web/test/Forms/EditFormTest.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; @@ -17,6 +19,7 @@ public EditFormTest() { var services = new ServiceCollection(); services.AddSingleton(); + services.AddSingleton(); _testRenderer = new(services.BuildServiceProvider()); } @@ -118,7 +121,7 @@ public async Task FormElementNameAndAction_SetToComponentName_WhenCombiningWithD { Model = model, FormName = "my-form", - BindingContext = new ModelBindingContext("", "") + BindingContext = new ModelBindingContext("", "", t => true) }; // Act @@ -139,7 +142,7 @@ public async Task FormElementNameAndAction_SetToCombinedIdentifier_WhenCombining { Model = model, FormName = "my-form", - BindingContext = new ModelBindingContext("parent-context", "path?handler=parent-context") + BindingContext = new ModelBindingContext("parent-context", "path?handler=parent-context", t => true ) }; // Act @@ -164,7 +167,7 @@ public async Task FormElementNameAndAction_CanBeExplicitlyOverriden() ["name"] = "my-explicit-name", ["action"] = "/somewhere/else", }, - BindingContext = new ModelBindingContext("parent-context", "path?handler=parent-context") + BindingContext = new ModelBindingContext("parent-context", "path?handler=parent-context", t => true) }; // Act @@ -184,7 +187,7 @@ public async Task FormElementNameAndAction_NotSetOnDefaultBindingContext() var rootComponent = new TestEditFormHostComponent { Model = model, - BindingContext = new ModelBindingContext("", ""), + BindingContext = new ModelBindingContext("", "", t => true), SubmitHandler = ctx => { } }; @@ -247,7 +250,7 @@ public async Task EventHandlerName_SetToBindingIdOnDefaultHandler() var rootComponent = new TestEditFormHostComponent { Model = model, - BindingContext = new ModelBindingContext("", "") + BindingContext = new ModelBindingContext("", "", t => true) }; // Act @@ -287,7 +290,7 @@ public async Task EventHandlerName_SetToFormNameWhenParentBindingContextIsDefaul { Model = model, FormName = "my-form", - BindingContext = new ModelBindingContext("", "") + BindingContext = new ModelBindingContext("", "", t => true) }; // Act @@ -307,7 +310,7 @@ public async Task EventHandlerName_SetToCombinedNameWhenParentBindingContextIsNa { Model = model, FormName = "my-form", - BindingContext = new ModelBindingContext("parent-context", "path?handler=parent-context") + BindingContext = new ModelBindingContext("parent-context", "path?handler=parent-context", t => true) }; // Act @@ -440,4 +443,23 @@ public TestNavigationManager() Initialize("https://localhost:85/subdir/", "https://localhost:85/subdir/path?query=value#hash"); } } + + private class TestFormValueSupplier : IFormValueSupplier + { + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool CanConvertSingleValue(Type type) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue) + { + boundValue = null; + return false; + } + } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs index f5d525cbdaf0..3345d113eb4b 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs @@ -262,5 +262,6 @@ internal void InitializeDefaultServices() builder.AddProvider(new WebAssemblyConsoleLoggerProvider(DefaultWebAssemblyJSRuntime.Instance)); }); Services.AddSingleton(); + Services.AddSingleton(); } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyFormValueSupplier.cs b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyFormValueSupplier.cs new file mode 100644 index 000000000000..a982d7d401c1 --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyFormValueSupplier.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; + +namespace Microsoft.AspNetCore.Components.WebAssembly.Services; +internal class WebAssemblyFormValueSupplier : IFormValueSupplier +{ + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool CanConvertSingleValue(Type type) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue) + { + boundValue = null; + return false; + } +} diff --git a/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs b/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs index e6420673fbbf..e522547c6971 100644 --- a/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs +++ b/src/Components/WebView/WebView/src/ComponentsWebViewServiceCollectionExtensions.cs @@ -31,6 +31,8 @@ public static IServiceCollection AddBlazorWebView(this IServiceCollection servic services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); + return services; } } diff --git a/src/Components/WebView/WebView/src/Services/WebViewFormValueSupplier.cs b/src/Components/WebView/WebView/src/Services/WebViewFormValueSupplier.cs new file mode 100644 index 000000000000..7405b541cfe7 --- /dev/null +++ b/src/Components/WebView/WebView/src/Services/WebViewFormValueSupplier.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components.Binding; + +namespace Microsoft.AspNetCore.Components.WebView.Services; + +internal class WebViewFormValueSupplier : IFormValueSupplier +{ + public bool CanBind(string formName, Type valueType) + { + return false; + } + + public bool CanConvertSingleValue(Type type) + { + return false; + } + + public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue) + { + boundValue = null; + return false; + } +} diff --git a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs index 928774c49e10..d0799f139fd4 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs @@ -1,23 +1,22 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using System.Net.Http; +using Components.TestServer.RazorComponents; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; using TestServer; using Xunit.Abstractions; -using OpenQA.Selenium; -using System.Net.Http; -using static System.Net.Mime.MediaTypeNames; -using Components.TestServer.RazorComponents; namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.FormHandlingTests; -public class FormWithParentBindingContextTest : ServerTestBase>> +public class FormWithParentBindingContextTest : ServerTestBase>> { public FormWithParentBindingContextTest( BrowserFixture browserFixture, - BasicTestAppServerSiteFixture> serverFixture, + BasicTestAppServerSiteFixture> serverFixture, ITestOutputHelper output) : base(browserFixture, serverFixture, output) { @@ -51,6 +50,21 @@ public void CanDispatchToTheDefaultFormWithBody() DispatchToFormCore(dispatchToForm); } + [Fact] + public void CanBindParameterToTheDefaultForm() + { + var dispatchToForm = new DispatchToForm(this) + { + Url = "forms/default-form-bound-parameter", + FormCssSelector = "form", + ExpectedActionValue = null, + InputFieldId = "value", + InputFieldCssSelector = "input[name=value]", + InputFieldValue = "stranger", + }; + DispatchToFormCore(dispatchToForm); + } + [Fact] public void CanReadFormValuesDuringOnInitialized() { @@ -76,6 +90,21 @@ public void CanDispatchToNamedForm() DispatchToFormCore(dispatchToForm); } + [Fact] + public void CanBindFormValueFromNamedFormWithBody() + { + var dispatchToForm = new DispatchToForm(this) + { + Url = "forms/named-form-bound-parameter", + FormCssSelector = "form[name=named-form-handler]", + ExpectedActionValue = "forms/named-form-bound-parameter?handler=named-form-handler", + InputFieldId = "value", + InputFieldCssSelector = "input[name=value]", + InputFieldValue = "stranger", + }; + DispatchToFormCore(dispatchToForm); + } + [Fact] public void CanDispatchToNamedFormInNestedContext() { @@ -88,6 +117,21 @@ public void CanDispatchToNamedFormInNestedContext() DispatchToFormCore(dispatchToForm); } + [Fact] + public void CanBindFormValueFromNestedNamedFormWithBody() + { + var dispatchToForm = new DispatchToForm(this) + { + Url = "forms/nested-named-form-bound-parameter", + FormCssSelector = """form[name="parent-context.named-form-handler"]""", + ExpectedActionValue = "forms/nested-named-form-bound-parameter?handler=parent-context.named-form-handler", + InputFieldId = "value", + InputFieldCssSelector = "input[name=value]", + InputFieldValue = "stranger", + }; + DispatchToFormCore(dispatchToForm); + } + [Fact] public void CanDispatchToFormDefinedInNonPageComponent() { @@ -251,7 +295,11 @@ private void DispatchToFormCore(DispatchToForm dispatch) if (dispatch.InputFieldValue != null) { - Browser.Exists(By.Id(dispatch.InputFieldId)).SendKeys(dispatch.InputFieldValue); + var criteria = dispatch.InputFieldCssSelector != null ? + By.CssSelector(dispatch.InputFieldCssSelector) : + By.Id(dispatch.InputFieldId); + + Browser.Exists(criteria).SendKeys(dispatch.InputFieldValue); } Browser.Click(By.Id(dispatch.SubmitButtonId)); @@ -284,6 +332,7 @@ public DispatchToForm(FormWithParentBindingContextTest test) : this() public string SubmitButtonId { get; internal set; } = "send"; public string InputFieldId { get; internal set; } = "firstName"; + public string InputFieldCssSelector { get; internal set; } = null; } private void GoTo(string relativePath) diff --git a/src/Components/test/E2ETest/Tests/InteropTest.cs b/src/Components/test/E2ETest/Tests/InteropTest.cs index 41c5c7fa5e79..bd233b4ffd5f 100644 --- a/src/Components/test/E2ETest/Tests/InteropTest.cs +++ b/src/Components/test/E2ETest/Tests/InteropTest.cs @@ -91,6 +91,7 @@ public void CanInvokeDotNetMethods() ["invokeAsyncThrowsSerializingCircularStructure"] = "Success", ["invokeAsyncThrowsUndefinedJSObjectReference"] = "Success", ["invokeAsyncThrowsNullJSObjectReference"] = "Success", + ["disposeJSObjectReferenceAsync"] = "Success", }; var expectedSyncValues = new Dictionary @@ -143,6 +144,7 @@ public void CanInvokeDotNetMethods() ["genericInstanceMethod"] = @"""Updated value 2""", ["requestDotNetStreamReference"] = @"""Success""", ["requestDotNetStreamWrapperReference"] = @"""Success""", + ["disposeJSInProcessObjectReference"] = "Success", }; // Include the sync assertions only when running under WebAssembly diff --git a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor index fd496a8cd1b5..31d28fd23ffd 100644 --- a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor @@ -221,6 +221,16 @@ JSObjectReferenceInvokeNonFunctionException = e; } + try + { + await jsObjectReference.DisposeAsync(); + ReturnValues["disposeJSObjectReferenceAsync"] = "Success"; + } + catch (Exception ex) + { + ReturnValues["disposeJSObjectReferenceAsync"] = $"Failure: {ex.Message}"; + } + var module = await JSRuntime.InvokeAsync("import", "./js/testmodule.js"); ReturnValues["jsObjectReferenceModule"] = await module.InvokeAsync("identity", "Returned from module!"); @@ -408,6 +418,15 @@ NumberField = 41, }).ToString(); + try + { + jsInProcObjectReference.Dispose(); + ReturnValues["disposeJSInProcessObjectReference"] = "Success"; + } + catch (Exception ex) + { + ReturnValues["disposeJSInProcessObjectReference"] = $"Failure: {ex.Message}"; + } var unmarshalledRuntime = (IJSUnmarshalledRuntime)JSRuntime; var jsUnmarshalledReference = unmarshalledRuntime.InvokeUnmarshalled("returnJSObjectReference"); diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormBoundParameter.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormBoundParameter.razor new file mode 100644 index 000000000000..1b55105e58b4 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormBoundParameter.razor @@ -0,0 +1,17 @@ +@using Microsoft.AspNetCore.Components.Forms + + + + + + +@if (_submitted) +{ +

Hello @Parameter!

+} + +@code{ + bool _submitted = false; + + [SupplyParameterFromForm(Name = "named-form-handler")] public string Parameter { get; set; } = ""; +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithoutBindingContextApp.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithDefaultContextApp.razor similarity index 91% rename from src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithoutBindingContextApp.razor rename to src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithDefaultContextApp.razor index ec4501fa7209..ab400f1f3543 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithoutBindingContextApp.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/FormWithDefaultContextApp.razor @@ -9,7 +9,7 @@ - + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundParameter.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundParameter.razor new file mode 100644 index 000000000000..0cda2a205275 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundParameter.razor @@ -0,0 +1,20 @@ +@page "/forms/default-form-bound-parameter" +@using Microsoft.AspNetCore.Components.Forms + +

Default form with bound parameter

+ + + + + + +@if (_submitted) +{ +

Hello @Parameter!

+} + +@code { + bool _submitted = false; + + [SupplyParameterFromForm] public string Parameter { get; set; } = ""; +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NamedFormBoundParameter.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NamedFormBoundParameter.razor new file mode 100644 index 000000000000..55d1adf585fa --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NamedFormBoundParameter.razor @@ -0,0 +1,20 @@ +@page "/forms/named-form-bound-parameter" +@using Microsoft.AspNetCore.Components.Forms + +

Named form with bound parameter

+ + + + + + +@if (_submitted) +{ +

Hello @Parameter!

+} + +@code{ + bool _submitted = false; + + [SupplyParameterFromForm(Name = "named-form-handler")] public string Parameter { get; set; } = ""; +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NestedNamedFormBoundParameter.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NestedNamedFormBoundParameter.razor new file mode 100644 index 000000000000..29750b239c0d --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/NestedNamedFormBoundParameter.razor @@ -0,0 +1,8 @@ +@page "/forms/nested-named-form-bound-parameter" +@using Microsoft.AspNetCore.Components.Forms + +

Nested named form bound parameter

+ + + + diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.TrimmingTests/Microsoft.AspNetCore.TrimmingTests.proj b/src/DefaultBuilder/test/Microsoft.AspNetCore.TrimmingTests/Microsoft.AspNetCore.TrimmingTests.proj index 5bbb96898742..55686ecdeafd 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.TrimmingTests/Microsoft.AspNetCore.TrimmingTests.proj +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.TrimmingTests/Microsoft.AspNetCore.TrimmingTests.proj @@ -2,11 +2,11 @@ - System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault + System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault;System.Diagnostics.Debugger.IsSupported X509Utilities.cs - System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault + System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault;System.Diagnostics.Debugger.IsSupported X509Utilities.cs diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.TrimmingTests/X509Utilities.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.TrimmingTests/X509Utilities.cs index ac814b6f4eed..c3579ef557d6 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.TrimmingTests/X509Utilities.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.TrimmingTests/X509Utilities.cs @@ -11,18 +11,8 @@ public static class X509Utilities { - public static bool HasCertificateType - { - get - { - var certificateType = GetType("System.Security.Cryptography", "System.Security.Cryptography.X509Certificates.X509Certificate"); - - // We're checking for members, rather than just the presence of the type, - // because Debugger Display types may reference it without actually - // causing a meaningful binary size increase. - return certificateType is not null && GetMembers(certificateType).Any(); - } - } + public static bool HasCertificateType => + GetType("System.Security.Cryptography", "System.Security.Cryptography.X509Certificates.X509Certificate") is not null; [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2057:UnrecognizedReflectionPattern", Justification = "Returning null when the type is unreferenced is desirable")] @@ -30,11 +20,4 @@ public static bool HasCertificateType { return Type.GetType($"{typeName}, {assemblyName}"); } - - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern", - Justification = "Returning null when the type is unreferenced is desirable")] - private static MemberInfo[] GetMembers(Type type) - { - return type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly); - } } \ No newline at end of file diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/RoutePattern/RoutePatternParser.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/RoutePattern/RoutePatternParser.cs index 7155bd076547..7259fcf0e187 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/RoutePattern/RoutePatternParser.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/RoutePattern/RoutePatternParser.cs @@ -373,7 +373,7 @@ private static void ValidateNoConsecutiveSeparators(RoutePatternCompilationUnit } } - private void CollectDiagnostics(RoutePatternNode node, HashSet seenDiagnostics, IList diagnostics) + private static void CollectDiagnostics(RoutePatternNode node, HashSet seenDiagnostics, IList diagnostics) { foreach (var child in node) { diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs index 708c6f5baf4d..40f7e607ef3c 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs @@ -455,30 +455,6 @@ private static void ProvideCompletions(EmbeddedCompletionContext context, Syntax } } - private (RoutePatternNode parent, RoutePatternToken Token)? FindToken(RoutePatternNode parent, VirtualChar ch) - { - foreach (var child in parent) - { - if (child.IsNode) - { - var result = FindToken(child.Node, ch); - if (result != null) - { - return result; - } - } - else - { - if (child.Token.VirtualChars.Contains(ch)) - { - return (parent, child.Token); - } - } - } - - return null; - } - private readonly struct RoutePatternItem { public readonly string DisplayText; diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs index a1e27c3c9010..26afcc85d64b 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs @@ -163,7 +163,7 @@ not CompletionTriggerKind.InvokeAndCommitIfUnique and context.IsExclusive = true; } - private void ProvideCompletions(EmbeddedCompletionContext context) + private static void ProvideCompletions(EmbeddedCompletionContext context) { var result = GetCurrentToken(context); if (result == null) @@ -216,7 +216,7 @@ private void ProvideCompletions(EmbeddedCompletionContext context) } } - private (RoutePatternNode Parent, RoutePatternToken Token)? GetCurrentToken(EmbeddedCompletionContext context) + private static (RoutePatternNode Parent, RoutePatternToken Token)? GetCurrentToken(EmbeddedCompletionContext context) { var previousVirtualCharOpt = context.RouteUsage.RoutePattern.Text.Find(context.Position - 1); if (previousVirtualCharOpt == null) @@ -279,7 +279,7 @@ private static void ProvidePolicyNameCompletions(EmbeddedCompletionContext conte } } - private (RoutePatternNode Parent, RoutePatternToken Token)? FindToken(RoutePatternNode parent, VirtualChar ch) + private static (RoutePatternNode Parent, RoutePatternToken Token)? FindToken(RoutePatternNode parent, VirtualChar ch) { foreach (var child in parent) { diff --git a/src/Framework/test/TestData.cs b/src/Framework/test/TestData.cs index 98de16bb3587..5e665cc06516 100644 --- a/src/Framework/test/TestData.cs +++ b/src/Framework/test/TestData.cs @@ -119,6 +119,8 @@ static TestData() "Microsoft.Extensions.Configuration.Xml", "Microsoft.Extensions.DependencyInjection", "Microsoft.Extensions.DependencyInjection.Abstractions", + "Microsoft.Extensions.Diagnostics", + "Microsoft.Extensions.Diagnostics.Abstractions", "Microsoft.Extensions.Diagnostics.HealthChecks", "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions", "Microsoft.Extensions.FileProviders.Abstractions", @@ -268,6 +270,8 @@ static TestData() { "Microsoft.Extensions.Configuration" }, { "Microsoft.Extensions.DependencyInjection.Abstractions" }, { "Microsoft.Extensions.DependencyInjection" }, + { "Microsoft.Extensions.Diagnostics.Abstractions" }, + { "Microsoft.Extensions.Diagnostics" }, { "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" }, { "Microsoft.Extensions.Diagnostics.HealthChecks" }, { "Microsoft.Extensions.Features" }, diff --git a/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs b/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs index c4c5488e958e..45c7a0215aa6 100644 --- a/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs +++ b/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Hosting; namespace Microsoft.AspNetCore.Hosting; diff --git a/src/Hosting/Hosting/src/GenericHost/SlimWebHostBuilder.cs b/src/Hosting/Hosting/src/GenericHost/SlimWebHostBuilder.cs index ff99aed202a7..693f39a497c7 100644 --- a/src/Hosting/Hosting/src/GenericHost/SlimWebHostBuilder.cs +++ b/src/Hosting/Hosting/src/GenericHost/SlimWebHostBuilder.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Hosting; namespace Microsoft.AspNetCore.Hosting; diff --git a/src/Hosting/Hosting/src/Internal/HostingMetrics.cs b/src/Hosting/Hosting/src/Internal/HostingMetrics.cs index 87d3ddf796e5..5ab7ed20737e 100644 --- a/src/Hosting/Hosting/src/Internal/HostingMetrics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingMetrics.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics; namespace Microsoft.AspNetCore.Hosting; @@ -19,7 +19,7 @@ internal sealed class HostingMetrics : IDisposable public HostingMetrics(IMeterFactory meterFactory) { - _meter = meterFactory.CreateMeter(MeterName); + _meter = meterFactory.Create(MeterName); _currentRequestsCounter = _meter.CreateUpDownCounter( "current-requests", diff --git a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj index d981ca6501fb..49974f5104af 100644 --- a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj +++ b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj @@ -25,6 +25,7 @@ + diff --git a/src/Hosting/Hosting/src/WebHostBuilder.cs b/src/Hosting/Hosting/src/WebHostBuilder.cs index f21d3593697e..aff8235a7b80 100644 --- a/src/Hosting/Hosting/src/WebHostBuilder.cs +++ b/src/Hosting/Hosting/src/WebHostBuilder.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; diff --git a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs index b42bb72e1bce..e58eda72de6b 100644 --- a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs @@ -2,13 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Diagnostics.Tracing; using System.Reflection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Metrics; using Moq; namespace Microsoft.AspNetCore.Hosting.Tests; @@ -44,17 +46,15 @@ public async Task EventCountersAndMetricsValues() }); var testMeterFactory1 = new TestMeterFactory(); - var testMeterRegister1 = new TestMeterRegistry(testMeterFactory1.Meters); var testMeterFactory2 = new TestMeterFactory(); - var testMeterRegister2 = new TestMeterRegistry(testMeterFactory2.Meters); var hostingApplication1 = CreateApplication(out var features1, eventSource: hostingEventSource, meterFactory: testMeterFactory1); var hostingApplication2 = CreateApplication(out var features2, eventSource: hostingEventSource, meterFactory: testMeterFactory2); - using var currentRequestsRecorder1 = new InstrumentRecorder(testMeterRegister1, HostingMetrics.MeterName, "current-requests"); - using var currentRequestsRecorder2 = new InstrumentRecorder(testMeterRegister2, HostingMetrics.MeterName, "current-requests"); - using var requestDurationRecorder1 = new InstrumentRecorder(testMeterRegister1, HostingMetrics.MeterName, "request-duration"); - using var requestDurationRecorder2 = new InstrumentRecorder(testMeterRegister2, HostingMetrics.MeterName, "request-duration"); + using var currentRequestsRecorder1 = new InstrumentRecorder(testMeterFactory1, HostingMetrics.MeterName, "current-requests"); + using var currentRequestsRecorder2 = new InstrumentRecorder(testMeterFactory2, HostingMetrics.MeterName, "current-requests"); + using var requestDurationRecorder1 = new InstrumentRecorder(testMeterFactory1, HostingMetrics.MeterName, "request-duration"); + using var requestDurationRecorder2 = new InstrumentRecorder(testMeterFactory2, HostingMetrics.MeterName, "request-duration"); // Act/Assert 1 var context1 = hostingApplication1.CreateContext(features1); diff --git a/src/Hosting/Hosting/test/HostingApplicationTests.cs b/src/Hosting/Hosting/test/HostingApplicationTests.cs index f3882d917f9e..37a0fe85ca15 100644 --- a/src/Hosting/Hosting/test/HostingApplicationTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationTests.cs @@ -2,17 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; -using System.Collections.ObjectModel; using System.Diagnostics; -using System.Diagnostics.Metrics; -using Microsoft.AspNetCore.Hosting.Fakes; using Microsoft.AspNetCore.Hosting.Server.Abstractions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Metrics; using Moq; using static Microsoft.AspNetCore.Hosting.HostingApplication; diff --git a/src/Hosting/Hosting/test/HostingMetricsTests.cs b/src/Hosting/Hosting/test/HostingMetricsTests.cs index 72877540bce7..bbea161096ca 100644 --- a/src/Hosting/Hosting/test/HostingMetricsTests.cs +++ b/src/Hosting/Hosting/test/HostingMetricsTests.cs @@ -9,8 +9,8 @@ using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Metrics; namespace Microsoft.AspNetCore.Hosting.Tests; @@ -21,13 +21,12 @@ public void MultipleRequests() { // Arrange var meterFactory = new TestMeterFactory(); - var meterRegistry = new TestMeterRegistry(meterFactory.Meters); var hostingApplication = CreateApplication(meterFactory: meterFactory); var httpContext = new DefaultHttpContext(); var meter = meterFactory.Meters.Single(); - using var requestDurationRecorder = new InstrumentRecorder(meterRegistry, HostingMetrics.MeterName, "request-duration"); - using var currentRequestsRecorder = new InstrumentRecorder(meterRegistry, HostingMetrics.MeterName, "current-requests"); + using var requestDurationRecorder = new InstrumentRecorder(meterFactory, HostingMetrics.MeterName, "request-duration"); + using var currentRequestsRecorder = new InstrumentRecorder(meterFactory, HostingMetrics.MeterName, "current-requests"); // Act/Assert Assert.Equal(HostingMetrics.MeterName, meter.Name); @@ -111,7 +110,6 @@ public async Task StartListeningDuringRequest_NotMeasured() // Arrange var syncPoint = new SyncPoint(); var meterFactory = new TestMeterFactory(); - var meterRegistry = new TestMeterRegistry(meterFactory.Meters); var hostingApplication = CreateApplication(meterFactory: meterFactory, requestDelegate: async ctx => { await syncPoint.WaitToContinue(); @@ -130,8 +128,8 @@ public async Task StartListeningDuringRequest_NotMeasured() await syncPoint.WaitForSyncPoint().DefaultTimeout(); - using var requestDurationRecorder = new InstrumentRecorder(meterRegistry, HostingMetrics.MeterName, "request-duration"); - using var currentRequestsRecorder = new InstrumentRecorder(meterRegistry, HostingMetrics.MeterName, "current-requests"); + using var requestDurationRecorder = new InstrumentRecorder(meterFactory, HostingMetrics.MeterName, "request-duration"); + using var currentRequestsRecorder = new InstrumentRecorder(meterFactory, HostingMetrics.MeterName, "current-requests"); context1.HttpContext.Response.StatusCode = StatusCodes.Status200OK; syncPoint.Continue(); @@ -148,13 +146,12 @@ public void IHttpMetricsTagsFeatureNotUsedFromFeatureCollection() { // Arrange var meterFactory = new TestMeterFactory(); - var meterRegistry = new TestMeterRegistry(meterFactory.Meters); var hostingApplication = CreateApplication(meterFactory: meterFactory); var httpContext = new DefaultHttpContext(); var meter = meterFactory.Meters.Single(); - using var requestDurationRecorder = new InstrumentRecorder(meterRegistry, HostingMetrics.MeterName, "request-duration"); - using var currentRequestsRecorder = new InstrumentRecorder(meterRegistry, HostingMetrics.MeterName, "current-requests"); + using var requestDurationRecorder = new InstrumentRecorder(meterFactory, HostingMetrics.MeterName, "request-duration"); + using var currentRequestsRecorder = new InstrumentRecorder(meterFactory, HostingMetrics.MeterName, "current-requests"); // Act/Assert Assert.Equal(HostingMetrics.MeterName, meter.Name); diff --git a/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj b/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj index 25a94cfc96a3..0fc87b4ba5c3 100644 --- a/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj +++ b/src/Hosting/Hosting/test/Microsoft.AspNetCore.Hosting.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Hosting/Server.IntegrationTesting/src/ApplicationPublisher.cs b/src/Hosting/Server.IntegrationTesting/src/ApplicationPublisher.cs index e8c64592f24f..aacd3acbe67c 100644 --- a/src/Hosting/Server.IntegrationTesting/src/ApplicationPublisher.cs +++ b/src/Hosting/Server.IntegrationTesting/src/ApplicationPublisher.cs @@ -38,7 +38,7 @@ public virtual Task Publish(DeploymentParameters deploymen if (deploymentParameters.ApplicationType == ApplicationType.Standalone) { - parameters += $" --runtime {GetRuntimeIdentifier(deploymentParameters)}"; + parameters += $" --self-contained --runtime {GetRuntimeIdentifier(deploymentParameters)}"; } else { diff --git a/src/Http/Http.Abstractions/src/ConnectionInfo.cs b/src/Http/Http.Abstractions/src/ConnectionInfo.cs index eb63133a4b9a..41a407836554 100644 --- a/src/Http/Http.Abstractions/src/ConnectionInfo.cs +++ b/src/Http/Http.Abstractions/src/ConnectionInfo.cs @@ -64,7 +64,13 @@ private string DebuggerToString() { var remoteEndpoint = RemoteIpAddress == null ? "(null)" : new IPEndPoint(RemoteIpAddress, RemotePort).ToString(); var localEndpoint = LocalIpAddress == null ? "(null)" : new IPEndPoint(LocalIpAddress, LocalPort).ToString(); - return $"Id = {Id ?? "(null)"}, Remote = {remoteEndpoint}, Local = {localEndpoint}, ClientCertificate = {ClientCertificate?.Subject ?? "(null)"}"; + + var s = $"Id = {Id ?? "(null)"}, Remote = {remoteEndpoint}, Local = {localEndpoint}"; + if (ClientCertificate != null) + { + s += $", ClientCertificate = {ClientCertificate.Subject}"; + } + return s; } private sealed class ConnectionInfoDebugView(ConnectionInfo info) diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index da6077afa1e8..6c763eeb07f6 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -54,29 +54,6 @@ Microsoft.AspNetCore.Http.HttpResponse - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs index 469f96a7a164..e313821d576e 100644 --- a/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs +++ b/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetailsContext.cs @@ -29,7 +29,7 @@ public sealed class ProblemDetailsContext public ProblemDetails ProblemDetails { get => _problemDetails ??= new ProblemDetails(); - init => _problemDetails = value; + set => _problemDetails = value; } /// diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 39a447908233..2c46af234a03 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -10,6 +10,8 @@ Microsoft.AspNetCore.Http.Metadata.IRouteDiagnosticsMetadata Microsoft.AspNetCore.Http.Metadata.IRouteDiagnosticsMetadata.Route.get -> string! Microsoft.AspNetCore.Http.ProblemDetailsContext.Exception.get -> System.Exception? Microsoft.AspNetCore.Http.ProblemDetailsContext.Exception.init -> void +*REMOVED*Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.init -> void +Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.set -> void Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.set -> void static Microsoft.AspNetCore.Http.EndpointFilterInvocationContext.Create(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> Microsoft.AspNetCore.Http.EndpointFilterInvocationContext! static Microsoft.AspNetCore.Http.EndpointFilterInvocationContext.Create(Microsoft.AspNetCore.Http.HttpContext! httpContext, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) -> Microsoft.AspNetCore.Http.EndpointFilterInvocationContext! diff --git a/src/Http/Http.Abstractions/src/WebSocketManager.cs b/src/Http/Http.Abstractions/src/WebSocketManager.cs index d927c3f04d9c..335028183906 100644 --- a/src/Http/Http.Abstractions/src/WebSocketManager.cs +++ b/src/Http/Http.Abstractions/src/WebSocketManager.cs @@ -50,8 +50,8 @@ private string DebuggerToString() { return IsWebSocketRequest switch { - false => "IsWebSocketRequest = False", - true => $"IsWebSocketRequest = True, RequestedProtocols = {string.Join(",", WebSocketRequestedProtocols)}", + false => "IsWebSocketRequest = false", + true => $"IsWebSocketRequest = true, RequestedProtocols = {string.Join(",", WebSocketRequestedProtocols)}", }; } diff --git a/src/Http/Http.Abstractions/test/ProblemDetailsContextTests.cs b/src/Http/Http.Abstractions/test/ProblemDetailsContextTests.cs new file mode 100644 index 000000000000..453852ee997b --- /dev/null +++ b/src/Http/Http.Abstractions/test/ProblemDetailsContextTests.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.AspNetCore.Http.Abstractions.Tests; + +public class ProblemDetailsContextTests +{ + [Fact] + public void ProblemDetailsPropertySetter_Should_SetProblemDetails() + { + // Arrange + ProblemDetailsContext context = new() { HttpContext = new DefaultHttpContext() }; + ProblemDetails problemDetails = new(); + + // Act + context.ProblemDetails = problemDetails; + + // Assert + Assert.Equal(problemDetails, context.ProblemDetails); + } +} diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index 028c498ef51e..ca4e7cc17dcd 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -99,7 +99,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) codeWriter.StartBlock(); codeWriter.WriteLine("if (ic.HttpContext.Response.StatusCode == 400)"); codeWriter.StartBlock(); - codeWriter.WriteLine("return ValueTask.FromResult(Results.Empty);"); + codeWriter.WriteLine(endpoint.Response?.IsAwaitable == true + ? "return (object?)Results.Empty;" + : "return ValueTask.FromResult(Results.Empty);"); codeWriter.EndBlock(); endpoint.EmitFilteredInvocation(codeWriter); codeWriter.EndBlockWithComma(); diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs index bd537439d423..699b9421e269 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs @@ -101,6 +101,8 @@ private static void PopulateMetadataForParameter(ParameterInfo parameter, End private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -112,21 +114,8 @@ private static void PopulateMetadataForParameter(ParameterInfo parameter, End } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -147,12 +136,22 @@ private static void PopulateMetadataForParameter(ParameterInfo parameter, End return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } """; diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs index e4039273aa04..8eec1bda9909 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs @@ -75,7 +75,7 @@ internal static void EmitFormParameterPreparation(this EndpointParameter endpoin } codeWriter.WriteLine($"var {endpointParameter.EmitAssigningCodeResult()} = {endpointParameter.AssigningCode};"); - if (!endpointParameter.IsOptional) + if (!endpointParameter.IsOptional && !endpointParameter.IsArray) { codeWriter.WriteLine($"if ({endpointParameter.EmitAssigningCodeResult()} == null)"); codeWriter.StartBlock(); @@ -262,8 +262,16 @@ internal static void EmitJsonBodyOrQueryParameterPreparationString(this Endpoint internal static void EmitBindAsyncPreparation(this EndpointParameter endpointParameter, CodeWriter codeWriter) { + // Invoke the `BindAsync` method on an interface if it is the target receiver. + var receiverType = endpointParameter.BindableMethodSymbol?.ReceiverType is { TypeKind: TypeKind.Interface } targetType + ? targetType + : endpointParameter.Type; + var bindMethodReceiverType = receiverType?.UnwrapTypeSymbol(unwrapNullable: true); + var bindMethodReceiverTypeString = bindMethodReceiverType?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var unwrappedType = endpointParameter.Type.UnwrapTypeSymbol(unwrapNullable: true); - var unwrappedTypeString = unwrappedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var unwrappedTypeString = unwrappedType?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var resolveParameterInfo = endpointParameter.IsProperty ? endpointParameter.PropertyAsParameterInfoConstruction : $"parameters[{endpointParameter.Ordinal}]"; @@ -274,10 +282,10 @@ internal static void EmitBindAsyncPreparation(this EndpointParameter endpointPar codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = await BindAsync<{unwrappedTypeString}>(httpContext, {resolveParameterInfo});"); break; case BindabilityMethod.BindAsyncWithParameter: - codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = await {unwrappedTypeString}.BindAsync(httpContext, {resolveParameterInfo});"); + codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = await {bindMethodReceiverTypeString}.BindAsync(httpContext, {resolveParameterInfo});"); break; case BindabilityMethod.BindAsync: - codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = await {unwrappedTypeString}.BindAsync(httpContext);"); + codeWriter.WriteLine($"var {endpointParameter.EmitTempArgument()} = await {bindMethodReceiverTypeString}.BindAsync(httpContext);"); break; default: throw new NotImplementedException($"Unreachable! Unexpected {nameof(BindabilityMethod)}: {endpointParameter.BindMethod}"); diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs index 1d205605fc88..bd2aee82f3b1 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointParameter.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Analyzers.Infrastructure; using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; using Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel.Emitters; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using WellKnownType = Microsoft.AspNetCore.App.Analyzers.Infrastructure.WellKnownTypeData.WellKnownType; @@ -106,7 +107,9 @@ private void ProcessEndpointParameterSource(Endpoint endpoint, ISymbol symbol, I } else { - AssigningCode = $"(string?)httpContext.Request.Form[{SymbolDisplay.FormatLiteral(LookupName, true)}]"; + AssigningCode = !IsArray + ? $"(string?)httpContext.Request.Form[{SymbolDisplay.FormatLiteral(LookupName, true)}]" + : $"httpContext.Request.Form[{SymbolDisplay.FormatLiteral(LookupName, true)}].ToArray()"; IsParsable = TryGetParsability(Type, wellKnownTypes, out var parsingBlockEmitter); ParsingBlockEmitter = parsingBlockEmitter; } @@ -190,12 +193,13 @@ Type is not INamedTypeSymbol namedTypeSymbol || LookupName = symbol.Name; AssigningCode = "httpContext.Request.Form"; } - else if (HasBindAsync(Type, wellKnownTypes, out var bindMethod)) + else if (HasBindAsync(Type, wellKnownTypes, out var bindMethod, out var bindMethodSymbol)) { endpoint.IsAwaitable = true; endpoint.EmitterContext.RequiresPropertyAsParameterInfo = IsProperty && bindMethod is BindabilityMethod.BindAsyncWithParameter or BindabilityMethod.IBindableFromHttpContext; Source = EndpointParameterSource.BindAsync; BindMethod = bindMethod; + BindableMethodSymbol = bindMethodSymbol; } else if (Type.SpecialType == SpecialType.System_String) { @@ -264,11 +268,12 @@ private static bool ImplementsIEndpointParameterMetadataProvider(ITypeSymbol typ public bool IsStringValues { get; set; } public BindabilityMethod? BindMethod { get; set; } + public IMethodSymbol? BindableMethodSymbol { get; set; } - private static bool HasBindAsync(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out BindabilityMethod? bindMethod) + private static bool HasBindAsync(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out BindabilityMethod? bindMethod, [NotNullWhen(true)] out IMethodSymbol? bindMethodSymbol) { var parameterType = typeSymbol.UnwrapTypeSymbol(unwrapArray: true, unwrapNullable: true); - return ParsabilityHelper.GetBindability(parameterType, wellKnownTypes, out bindMethod) == Bindability.Bindable; + return ParsabilityHelper.GetBindability(parameterType, wellKnownTypes, out bindMethod, out bindMethodSymbol) == Bindability.Bindable; } private static bool TryGetArrayElementType(ITypeSymbol type, [NotNullWhen(true)]out ITypeSymbol elementType) @@ -343,7 +348,7 @@ private bool TryGetParsability(ITypeSymbol typeSymbol, WellKnownTypes wellKnownT { parsingBlockEmitter = (writer, inputArgument, outputArgument) => { - writer.WriteLine($"""{typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {outputArgument} = default;"""); + writer.WriteLine($"""{typeSymbol.ToDisplayString(EmitterConstants.DisplayFormat)} {outputArgument} = default;"""); writer.WriteLine($$"""if ({{preferredTryParseInvocation(inputArgument, $"{inputArgument}_parsed_non_nullable")}})"""); writer.StartBlock(); writer.WriteLine($$"""{{outputArgument}} = {{$"{inputArgument}_parsed_non_nullable"}};"""); diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs index 4b6c84954241..6641ef3b832f 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs @@ -338,7 +338,9 @@ public static void EmitFilteredInvocation(this Endpoint endpoint, CodeWriter cod codeWriter.WriteLine(endpoint.Response?.IsAwaitable == true ? $"await handler({endpoint.EmitFilteredArgumentList()});" : $"handler({endpoint.EmitFilteredArgumentList()});"); - codeWriter.WriteLine("return ValueTask.FromResult(Results.Empty);"); + codeWriter.WriteLine(endpoint.Response?.IsAwaitable == true + ? "return (object?)Results.Empty;" + : "return ValueTask.FromResult(Results.Empty);"); } else if (endpoint.Response?.IsAwaitable == true) { diff --git a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs index a70fec428d17..5bd053f13f7d 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsDefaultWriterTest.cs @@ -54,6 +54,45 @@ public async Task WriteAsync_Works() Assert.Equal(expectedProblem.Instance, problemDetails.Instance); } + [Fact] + public async Task WriteAsync_Works_WhenReplacingProblemDetailsUsingSetter() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = CreateContext(stream); + var originalProblemDetails = new ProblemDetails(); + + var expectedProblem = new ProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom", + Title = "Custom Bad Request", + }; + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = originalProblemDetails + }; + + problemDetailsContext.ProblemDetails = expectedProblem; + + //Act + await writer.WriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream, SerializerOptions); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + } + [Fact] public async Task WriteAsync_Works_WithJsonContext() { diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 3aaf4752e597..38878e04ad3a 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -573,26 +573,6 @@ private record MyBindAsyncFromInterfaceRecord(Uri uri) : IBindAsync - { - ["tryParsable"] = queryValues - }); - - var factoryResult = RequestDelegateFactory.Create(action, new() { DisableInferBodyFromParameters = true }); - - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - Assert.NotEmpty(httpContext.Items); - Assert.Equal(expectedParameterValue, httpContext.Items["tryParsable"]); - } - [Theory] [MemberData(nameof(TryParsableArrayParameters))] public async Task RequestDelegateHandlesDoesNotHandleArraysFromQueryStringWhenBodyIsInferred(Delegate action, string[]? queryValues, object? expectedParameterValue) @@ -640,76 +620,6 @@ static void StoreNullableIntArray(HttpContext httpContext, int?[]? tryParsable) Assert.Null(httpContext.Items["tryParsable"]); } - [Fact] - public async Task RequestDelegateHandlesArraysFromExplicitQueryStringSource() - { - var httpContext = CreateHttpContext(); - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["a"] = new(new[] { "1", "2", "3" }) - }); - - httpContext.Request.Headers["Custom"] = new(new[] { "4", "5", "6" }); - - httpContext.Request.Form = new FormCollection(new Dictionary - { - ["form"] = new(new[] { "7", "8", "9" }) - }); - - var factoryResult = RequestDelegateFactory.Create((HttpContext context, - [FromHeader(Name = "Custom")] int[] headerValues, - [FromQuery(Name = "a")] int[] queryValues, - [FromForm(Name = "form")] int[] formValues) => - { - context.Items["headers"] = headerValues; - context.Items["query"] = queryValues; - context.Items["form"] = formValues; - }); - - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - Assert.Equal(new[] { 1, 2, 3 }, (int[])httpContext.Items["query"]!); - Assert.Equal(new[] { 4, 5, 6 }, (int[])httpContext.Items["headers"]!); - Assert.Equal(new[] { 7, 8, 9 }, (int[])httpContext.Items["form"]!); - } - - [Fact] - public async Task RequestDelegateHandlesStringValuesFromExplicitQueryStringSource() - { - var httpContext = CreateHttpContext(); - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["a"] = new(new[] { "1", "2", "3" }) - }); - - httpContext.Request.Headers["Custom"] = new(new[] { "4", "5", "6" }); - - httpContext.Request.Form = new FormCollection(new Dictionary - { - ["form"] = new(new[] { "7", "8", "9" }) - }); - - var factoryResult = RequestDelegateFactory.Create((HttpContext context, - [FromHeader(Name = "Custom")] StringValues headerValues, - [FromQuery(Name = "a")] StringValues queryValues, - [FromForm(Name = "form")] StringValues formValues) => - { - context.Items["headers"] = headerValues; - context.Items["query"] = queryValues; - context.Items["form"] = formValues; - }); - - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - Assert.Equal(new StringValues(new[] { "1", "2", "3" }), httpContext.Items["query"]); - Assert.Equal(new StringValues(new[] { "4", "5", "6" }), httpContext.Items["headers"]); - Assert.Equal(new StringValues(new[] { "7", "8", "9" }), httpContext.Items["form"]!); - } - [Fact] public async Task RequestDelegateHandlesNullableStringValuesFromExplicitQueryStringSource() { @@ -773,26 +683,6 @@ public async Task RequestDelegateHandlesNullableStringValuesFromExplicitQueryStr Assert.Null(httpContext.Items["form"]); } - [Fact] - public async Task RequestDelegateUsesTryParseOverBindAsyncGivenExplicitAttribute() - { - var fromRouteFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromRoute] MyBindAsyncRecord myBindAsyncRecord) => { }); - var fromQueryFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromQuery] MyBindAsyncRecord myBindAsyncRecord) => { }); - - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues["myBindAsyncRecord"] = "foo"; - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["myBindAsyncRecord"] = "foo" - }); - - var fromRouteRequestDelegate = fromRouteFactoryResult.RequestDelegate; - var fromQueryRequestDelegate = fromQueryFactoryResult.RequestDelegate; - - await Assert.ThrowsAsync(() => fromRouteRequestDelegate(httpContext)); - await Assert.ThrowsAsync(() => fromQueryRequestDelegate(httpContext)); - } - [Fact] public async Task RequestDelegateCanAwaitValueTasksThatAreNotImmediatelyCompleted() { @@ -814,25 +704,6 @@ public async Task RequestDelegateCanAwaitValueTasksThatAreNotImmediatelyComplete Assert.Equal(new MyAwaitedBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myAwaitedBindAsyncStruct"]); } - [Fact] - public async Task RequestDelegateUsesBindAsyncFromImplementedInterface() - { - var httpContext = CreateHttpContext(); - - httpContext.Request.Headers.Referer = "https://example.org"; - - var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncFromInterfaceRecord myBindAsyncRecord) => - { - httpContext.Items["myBindAsyncFromInterfaceRecord"] = myBindAsyncRecord; - }); - - var requestDelegate = resultFactory.RequestDelegate; - - await requestDelegate(httpContext); - - Assert.Equal(new MyBindAsyncFromInterfaceRecord(new Uri("https://example.org")), httpContext.Items["myBindAsyncFromInterfaceRecord"]); - } - public static object[][] DelegatesWithAttributesOnNotTryParsableParameters { get @@ -902,118 +773,6 @@ void StoreNullableIntArray(HttpContext httpContext, int?[] values) Assert.Equal(400, badHttpRequestException.StatusCode); } - [Fact] - public async Task BindAsyncWithBodyArgument() - { - Todo originalTodo = new() - { - Name = "Write more tests!" - }; - - var httpContext = CreateHttpContext(); - - var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); - var stream = new MemoryStream(requestBodyBytes); - httpContext.Request.Body = stream; - - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - - var jsonOptions = new JsonOptions(); - jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); - - var mock = new Mock(); - mock.Setup(m => m.GetService(It.IsAny())).Returns(t => - { - if (t == typeof(IOptions)) - { - return Options.Create(jsonOptions); - } - return null; - }); - - httpContext.RequestServices = mock.Object; - httpContext.Request.Headers.Referer = "https://example.org"; - - var invoked = false; - - var factoryResult = RequestDelegateFactory.Create((HttpContext context, MyBindAsyncRecord myBindAsyncRecord, Todo todo) => - { - invoked = true; - context.Items[nameof(myBindAsyncRecord)] = myBindAsyncRecord; - context.Items[nameof(todo)] = todo; - }); - - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - Assert.True(invoked); - var arg = httpContext.Items["myBindAsyncRecord"] as MyBindAsyncRecord; - Assert.NotNull(arg); - Assert.Equal("https://example.org/", arg!.Uri.ToString()); - var todo = httpContext.Items["todo"] as Todo; - Assert.NotNull(todo); - Assert.Equal("Write more tests!", todo!.Name); - } - - [Fact] - public async Task BindAsyncRunsBeforeBodyBinding() - { - Todo originalTodo = new() - { - Name = "Write more tests!" - }; - - var httpContext = CreateHttpContext(); - - var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); - var stream = new MemoryStream(requestBodyBytes); - httpContext.Request.Body = stream; - - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - - var jsonOptions = new JsonOptions(); - jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); - - var mock = new Mock(); - mock.Setup(m => m.GetService(It.IsAny())).Returns(t => - { - if (t == typeof(IOptions)) - { - return Options.Create(jsonOptions); - } - return null; - }); - - httpContext.RequestServices = mock.Object; - httpContext.Request.Headers.Referer = "https://example.org"; - - var invoked = false; - - var factoryResult = RequestDelegateFactory.Create((HttpContext context, CustomTodo customTodo, Todo todo) => - { - invoked = true; - context.Items[nameof(customTodo)] = customTodo; - context.Items[nameof(todo)] = todo; - }); - - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - Assert.True(invoked); - var todo0 = httpContext.Items["customTodo"] as Todo; - Assert.NotNull(todo0); - Assert.Equal("Write more tests!", todo0!.Name); - var todo1 = httpContext.Items["todo"] as Todo; - Assert.NotNull(todo1); - Assert.Equal("Write more tests!", todo1!.Name); - } - private record ParametersListWithImplictFromBody(HttpContext HttpContext, TodoStruct Todo); private record ParametersListWithExplictFromBody(HttpContext HttpContext, [FromBody] Todo Todo); @@ -1136,27 +895,6 @@ public async Task RequestDelegateRejectsEmptyBodyGivenExplicitFromBodyParameter( Assert.Equal(400, httpContext.Response.StatusCode); } - [Theory] - [MemberData(nameof(ImplicitFromBodyActions))] - public async Task RequestDelegateRejectsEmptyBodyGivenImplicitFromBodyParameter(Delegate action) - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = "0"; - httpContext.Features.Set(new RequestBodyDetectionFeature(false)); - - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - - var factoryResult = RequestDelegateFactory.Create(action, new RequestDelegateFactoryOptions() { ThrowOnBadRequest = true }); - var requestDelegate = factoryResult.RequestDelegate; - - var ex = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); - Assert.StartsWith("Implicit body inferred for parameter", ex.Message); - Assert.EndsWith("but no body was provided. Did you mean to use a Service instead?", ex.Message); - } - [Fact] public void RequestDelegateFactoryThrowsForByRefReturnTypes() { @@ -1450,230 +1188,52 @@ public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAtt Assert.Same(myOriginalService, httpContext.Items["service"]); } - [Fact] - public async Task RequestDelegatePopulatesHttpContextParameterWithoutAttribute() + public static IEnumerable ChildResult { - HttpContext? httpContextArgument = null; - - void TestAction(HttpContext httpContext) + get { - httpContextArgument = httpContext; - } + TodoChild originalTodo = new() + { + Name = "Write even more tests!", + Child = "With type hierarchies!", + }; - var httpContext = CreateHttpContext(); + Todo TestAction() => originalTodo; - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + Task TaskTestAction() => Task.FromResult(originalTodo); + async Task TaskTestActionAwaited() + { + await Task.Yield(); + return originalTodo; + } - await requestDelegate(httpContext); + ValueTask ValueTaskTestAction() => ValueTask.FromResult(originalTodo); + async ValueTask ValueTaskTestActionAwaited() + { + await Task.Yield(); + return originalTodo; + } - Assert.Same(httpContext, httpContextArgument); + return new List + { + new object[] { (Func)TestAction }, + new object[] { (Func>)TaskTestAction}, + new object[] { (Func>)TaskTestActionAwaited}, + new object[] { (Func>)ValueTaskTestAction}, + new object[] { (Func>)ValueTaskTestActionAwaited}, + }; + } } - [Fact] - public async Task RequestDelegatePassHttpContextRequestAbortedAsCancellationToken() + [Theory] + [MemberData(nameof(ChildResult))] + public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody(Delegate @delegate) { - CancellationToken? cancellationTokenArgument = null; - - void TestAction(CancellationToken cancellationToken) - { - cancellationTokenArgument = cancellationToken; - } - - using var cts = new CancellationTokenSource(); var httpContext = CreateHttpContext(); - // Reset back to default HttpRequestLifetimeFeature that implements a setter for RequestAborted. - httpContext.Features.Set(new HttpRequestLifetimeFeature()); - httpContext.RequestAborted = cts.Token; + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - Assert.Equal(httpContext.RequestAborted, cancellationTokenArgument); - } - - [Fact] - public async Task RequestDelegatePassHttpContextUserAsClaimsPrincipal() - { - ClaimsPrincipal? userArgument = null; - - void TestAction(ClaimsPrincipal user) - { - userArgument = user; - } - - var httpContext = CreateHttpContext(); - httpContext.User = new ClaimsPrincipal(); - - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - Assert.Equal(httpContext.User, userArgument); - } - - [Fact] - public async Task RequestDelegatePassHttpContextRequestAsHttpRequest() - { - HttpRequest? httpRequestArgument = null; - - void TestAction(HttpRequest httpRequest) - { - httpRequestArgument = httpRequest; - } - - var httpContext = CreateHttpContext(); - - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - Assert.Equal(httpContext.Request, httpRequestArgument); - } - - [Fact] - public async Task RequestDelegatePassesHttpContextRresponseAsHttpResponse() - { - HttpResponse? httpResponseArgument = null; - - void TestAction(HttpResponse httpResponse) - { - httpResponseArgument = httpResponse; - } - - var httpContext = CreateHttpContext(); - - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - Assert.Equal(httpContext.Response, httpResponseArgument); - } - - public static IEnumerable ComplexResult - { - get - { - Todo originalTodo = new() - { - Name = "Write even more tests!" - }; - - Todo TestAction() => originalTodo; - Task TaskTestAction() => Task.FromResult(originalTodo); - ValueTask ValueTaskTestAction() => ValueTask.FromResult(originalTodo); - - static Todo StaticTestAction() => new Todo { Name = "Write even more tests!" }; - static Task StaticTaskTestAction() => Task.FromResult(new Todo { Name = "Write even more tests!" }); - static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(new Todo { Name = "Write even more tests!" }); - - return new List - { - new object[] { (Func)TestAction }, - new object[] { (Func>)TaskTestAction}, - new object[] { (Func>)ValueTaskTestAction}, - new object[] { (Func)StaticTestAction}, - new object[] { (Func>)StaticTaskTestAction}, - new object[] { (Func>)StaticValueTaskTestAction}, - }; - } - } - - [Theory] - [MemberData(nameof(ComplexResult))] - public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody(Delegate @delegate) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - var deserializedResponseBody = JsonSerializer.Deserialize(responseBodyStream.ToArray(), new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); - - Assert.NotNull(deserializedResponseBody); - Assert.Equal("Write even more tests!", deserializedResponseBody!.Name); - } - - [Fact] - public async Task RequestDelegateWritesComplexStructReturnValueAsJsonResponseBody() - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - - var factoryResult = RequestDelegateFactory.Create(() => new TodoStruct(42, "Bob", true)); - var requestDelegate = factoryResult.RequestDelegate; - - await requestDelegate(httpContext); - - var deserializedResponseBody = JsonSerializer.Deserialize(responseBodyStream.ToArray(), new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); - - Assert.Equal(42, deserializedResponseBody.Id); - Assert.Equal("Bob", deserializedResponseBody.Name); - Assert.True(deserializedResponseBody.IsComplete); - } - - public static IEnumerable ChildResult - { - get - { - TodoChild originalTodo = new() - { - Name = "Write even more tests!", - Child = "With type hierarchies!", - }; - - Todo TestAction() => originalTodo; - - Task TaskTestAction() => Task.FromResult(originalTodo); - async Task TaskTestActionAwaited() - { - await Task.Yield(); - return originalTodo; - } - - ValueTask ValueTaskTestAction() => ValueTask.FromResult(originalTodo); - async ValueTask ValueTaskTestActionAwaited() - { - await Task.Yield(); - return originalTodo; - } - - return new List - { - new object[] { (Func)TestAction }, - new object[] { (Func>)TaskTestAction}, - new object[] { (Func>)TaskTestActionAwaited}, - new object[] { (Func>)ValueTaskTestAction}, - new object[] { (Func>)ValueTaskTestActionAwaited}, - }; - } - } - - [Theory] - [MemberData(nameof(ChildResult))] - public async Task RequestDelegateWritesMembersFromChildTypesToJsonResponseBody(Delegate @delegate) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - - var factoryResult = RequestDelegateFactory.Create(@delegate); + var factoryResult = RequestDelegateFactory.Create(@delegate); var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); @@ -2683,668 +2243,102 @@ public void CreateThrowsNotSupportedExceptionIfIFormCollectionHasMetadataParamet { IFormCollection? formArgument = null; - void TestAction([FromForm(Name = "foo")] IFormCollection formCollection) - { - formArgument = formCollection; - } - - var nse = Assert.Throws(() => RequestDelegateFactory.Create(TestAction)); - Assert.Equal("Assigning a value to the IFromFormMetadata.Name property is not supported for parameters of type IFormCollection.", nse.Message); - } - - public static object[][] NullableFromParameterListActions - { - get - { - void TestParameterListRecordStruct([AsParameters] ParameterListRecordStruct? args) - { } - - void TestParameterListRecordClass([AsParameters] ParameterListRecordClass? args) - { } - - void TestParameterListStruct([AsParameters] ParameterListStruct? args) - { } - - void TestParameterListClass([AsParameters] ParameterListClass? args) - { } - - return new[] - { - new object[] { (Action)TestParameterListRecordStruct }, - new object[] { (Action)TestParameterListRecordClass }, - new object[] { (Action)TestParameterListStruct }, - new object[] { (Action)TestParameterListClass }, - }; - } - } - - [Theory] - [MemberData(nameof(NullableFromParameterListActions))] - public void RequestDelegateThrowsWhenNullableParameterList(Delegate action) - { - var parameter = action.Method.GetParameters()[0]; - var httpContext = CreateHttpContext(); - - var exception = Assert.Throws(() => RequestDelegateFactory.Create(action)); - Assert.Contains($"The nullable type '{TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false)}' is not supported, mark the parameter as non-nullable.", exception.Message); - } - - [Fact] - public void RequestDelegateThrowsWhenParameterNameConflicts() - { - void TestAction(HttpContext context, [AsParameters] SampleParameterList args, [AsParameters] SampleParameterList args2) - { - context.Items.Add("foo", args.Foo); - } - var httpContext = CreateHttpContext(); - - var exception = Assert.Throws(() => RequestDelegateFactory.Create(TestAction)); - Assert.Contains("An item with the same key has already been added. Key: Foo", exception.Message); - } - - private class ParameterListWithReadOnlyProperties - { - public ParameterListWithReadOnlyProperties() - { - ReadOnlyValue = 1; - } - - public int Value { get; set; } - - public int ConstantValue => 1; - - public int ReadOnlyValue { get; } - } - - [Fact] - public async Task RequestDelegateFactory_InvokesFiltersButNotHandler_OnArgumentError() - { - var invoked = false; - // Arrange - string HelloName(string name) - { - invoked = true; - return $"Hello, {name}!"; - }; - - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - - // Act - var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() - { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => async (context) => - { - context.Arguments[0] = context.Arguments[0] != null ? $"{((string)context.Arguments[0]!)}Prefix" : "NULL"; - return await next(context); - } - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); - - // Assert - Assert.False(invoked); - Assert.Equal(400, httpContext.Response.StatusCode); - Assert.Equal(0, responseBodyStream.Position); - } - - [Fact] - public async Task RequestDelegateFactory_InvokesFilters_OnDelegateWithTarget() - { - // Arrange - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["name"] = "TestName" - }); - - // Act - var factoryResult = RequestDelegateFactory.Create((string name) => $"Hello, {name}!", new RequestDelegateFactoryOptions() - { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => async (context) => - { - return await next(context); - } - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); - - // Assert - - Assert.Equal(200, httpContext.Response.StatusCode); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("Hello, TestName!", decodedResponseBody); - } - - string GetString(string name) - { - return $"Hello, {name}!"; - } - - [Fact] - public async Task RequestDelegateFactory_InvokesFilters_OnMethodInfoWithNullTargetFactory() - { - // Arrange - var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( - nameof(GetString), - BindingFlags.NonPublic | BindingFlags.Instance, - new[] { typeof(string) }); - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["name"] = "TestName" - }); - - // Act - var factoryResult = RequestDelegateFactory.Create(methodInfo!, null, new RequestDelegateFactoryOptions() - { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => async (context) => - { - return await next(context); - } - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); - - // Assert - Assert.Equal(200, httpContext.Response.StatusCode); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("Hello, TestName!", decodedResponseBody); - } - - [Fact] - public async Task RequestDelegateFactory_InvokesFilters_OnMethodInfoWithProvidedTargetFactory() - { - // Arrange - var invoked = false; - var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( - nameof(GetString), - BindingFlags.NonPublic | BindingFlags.Instance, - new[] { typeof(string) }); - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["name"] = "TestName" - }); - - // Act - Func targetFactory = (context) => - { - invoked = true; - context.Items["invoked"] = true; - return Activator.CreateInstance(methodInfo!.DeclaringType!)!; - }; - var factoryResult = RequestDelegateFactory.Create(methodInfo!, targetFactory, new RequestDelegateFactoryOptions() - { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => async (context) => - { - return await next(context); - } - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); - - // Assert - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.True(invoked); - var invokedInContext = Assert.IsType(httpContext.Items["invoked"]); - Assert.True(invokedInContext); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("Hello, TestName!", decodedResponseBody); - } - - [Fact] - public async Task RequestDelegateFactory_CanInvokeSingleEndpointFilter_ThatProvidesCustomErrorMessage() - { - // Arrange - string HelloName(string name) - { - return $"Hello, {name}!"; - }; - - var httpContext = CreateHttpContext(); - - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - - // Act - var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() - { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { - (routeHandlerContext, next) => async (context) => - { - if (context.HttpContext.Response.StatusCode == 400) - { - return Results.Problem("New response", statusCode: 400); - } - return await next(context); - } - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); - - // Assert - var decodedResponseBody = JsonSerializer.Deserialize(responseBodyStream.ToArray(), JsonOptions.DefaultSerializerOptions); - Assert.Equal(400, httpContext.Response.StatusCode); - Assert.Equal("New response", decodedResponseBody!.Detail); - } - - [Fact] - public async Task RequestDelegateFactory_CanInvokeMultipleEndpointFilters_ThatTouchArguments() - { - // Arrange - string HelloName(string name, int age) - { - return $"Hello, {name}! You are {age} years old."; - }; - - var loggerInvoked = 0; - void Log(string arg) => loggerInvoked++; - - var httpContext = CreateHttpContext(); - - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["name"] = "TestName", - ["age"] = "25" - }); - - // Act - var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() - { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => async (context) => - { - context.Arguments[1] = ((int)context.Arguments[1]!) + 2; - return await next(context); - }, - (routeHandlerContext, next) => async (context) => - { - foreach (var parameter in context.Arguments) - { - Log(parameter!.ToString() ?? "no arg"); - } - return await next(context); - } - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); - - // Assert - var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("Hello, TestName! You are 27 years old.", responseBody); - Assert.Equal(2, loggerInvoked); - } - - [Fact] - public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatUsesMethodInfo() - { - // Arrange - string HelloName(string name) - { - return $"Hello, {name}!."; - }; - - var httpContext = CreateHttpContext(); - - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["name"] = "TestName" - }); - - // Act - var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() - { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => - { - var parameters = routeHandlerContext.MethodInfo.GetParameters(); - var isInt = parameters.Length == 2 && parameters[1].ParameterType == typeof(int); - return async (context) => - { - if (isInt) - { - context.Arguments[1] = ((int)context.Arguments[1]!) + 2; - return await next(context); - } - return "Is not an int."; - }; - }, - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); - - // Assert - var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("Is not an int.", responseBody); - } - - [Fact] - public async Task RequestDelegateFactory_CanInvokeEndpointFilter_ThatReadsEndpointMetadata() - { - // Arrange - string HelloName(IFormFileCollection formFiles) - { - return $"Got {formFiles.Count} files."; - }; - - var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream"); - var form = new MultipartFormDataContent("some-boundary"); - form.Add(fileContent, "file", "file.txt"); - - var stream = new MemoryStream(); - await form.CopyToAsync(stream); - - stream.Seek(0, SeekOrigin.Begin); - - var httpContext = CreateHttpContext(); - httpContext.Request.Body = stream; - httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary"; - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - - // Act - var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() - { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => - { - string? contentType = null; - - return async (context) => - { - contentType ??= context.HttpContext.GetEndpoint()?.Metadata.GetMetadata()?.ContentTypes.SingleOrDefault(); - - if (contentType == "multipart/form-data") - { - return "I see you expect a form."; - } - - return await next(context); - }; - }, - }), - }); - - var builder = new RouteEndpointBuilder(factoryResult.RequestDelegate, RoutePatternFactory.Parse("/"), order: 0); - ((List)builder.Metadata).AddRange(factoryResult.EndpointMetadata); - httpContext.Features.Set(new EndpointFeature { Endpoint = builder.Build() }); - - await factoryResult.RequestDelegate(httpContext); - - // Assert - var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("I see you expect a form.", responseBody); - } - - [Fact] - public async Task RequestDelegateFactory_CanInvokeSingleEndpointFilter_ThatModifiesBodyParameter() - { - // Arrange - Todo todo = new Todo() { Name = "Write tests", IsComplete = true }; - string PrintTodo(Todo todo) - { - return $"{todo.Name} is {(todo.IsComplete ? "done" : "not done")}."; - }; - - var httpContext = CreateHttpContext(); - - var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(todo); - var stream = new MemoryStream(requestBodyBytes); - httpContext.Request.Body = stream; - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - - // Act - var factoryResult = RequestDelegateFactory.Create(PrintTodo, new RequestDelegateFactoryOptions() - { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => async (context) => - { - Todo originalTodo = (Todo)context.Arguments[0]!; - originalTodo!.IsComplete = !originalTodo.IsComplete; - context.Arguments[0] = originalTodo; - return await next(context); - } - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); - - // Assert - var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("Write tests is not done.", responseBody); - } - - [Fact] - public async Task RequestDelegateFactory_CanInvokeSingleEndpointFilter_ThatModifiesResult() - { - // Arrange - string HelloName(string name) - { - return $"Hello, {name}!"; - }; - - var httpContext = CreateHttpContext(); - - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["name"] = "TestName" - }); - - // Act - var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() - { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => async (context) => - { - var previousResult = await next(context); - if (previousResult is string stringResult) - { - return stringResult.ToUpperInvariant(); - } - return previousResult; - } - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); - - // Assert - var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("HELLO, TESTNAME!", responseBody); - } - - [Fact] - public async Task RequestDelegateFactory_CanInvokeMultipleEndpointFilters_ThatModifyArgumentsAndResult() - { - // Arrange - string HelloName(string name) - { - return $"Hello, {name}!"; - }; - - var httpContext = CreateHttpContext(); - - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; - - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["name"] = "TestName" - }); - - // Act - var factoryResult = RequestDelegateFactory.Create(HelloName, new RequestDelegateFactoryOptions() + void TestAction([FromForm(Name = "foo")] IFormCollection formCollection) { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => async (context) => - { - var previousResult = await next(context); - if (previousResult is string stringResult) - { - return stringResult.ToUpperInvariant(); - } - return previousResult; - }, - (RouteHandlerContext, next) => async (context) => - { - var newValue = $"{context.GetArgument(0)}Prefix"; - context.Arguments[0] = newValue; - return await next(context); - } - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + formArgument = formCollection; + } - // Assert - var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("HELLO, TESTNAMEPREFIX!", responseBody); + var nse = Assert.Throws(() => RequestDelegateFactory.Create(TestAction)); + Assert.Equal("Assigning a value to the IFromFormMetadata.Name property is not supported for parameters of type IFormCollection.", nse.Message); } - public static object[][] TaskOfTMethods + public static object[][] NullableFromParameterListActions { get { - Task TaskOfTMethod() - { - return Task.FromResult("foo"); - } + void TestParameterListRecordStruct([AsParameters] ParameterListRecordStruct? args) + { } - async Task TaskOfTWithYieldMethod() - { - await Task.Yield(); - return "foo"; - } + void TestParameterListRecordClass([AsParameters] ParameterListRecordClass? args) + { } - async Task TaskOfObjectWithYieldMethod() - { - await Task.Yield(); - return "foo"; - } + void TestParameterListStruct([AsParameters] ParameterListStruct? args) + { } - return new object[][] + void TestParameterListClass([AsParameters] ParameterListClass? args) + { } + + return new[] { - new object[] { (Func>)TaskOfTMethod }, - new object[] { (Func>)TaskOfTWithYieldMethod }, - new object[] { (Func>)TaskOfObjectWithYieldMethod } + new object[] { (Action)TestParameterListRecordStruct }, + new object[] { (Action)TestParameterListRecordClass }, + new object[] { (Action)TestParameterListStruct }, + new object[] { (Action)TestParameterListClass }, }; } } [Theory] - [MemberData(nameof(TaskOfTMethods))] - public async Task CanInvokeFilter_OnTaskOfTReturningHandler(Delegate @delegate) + [MemberData(nameof(NullableFromParameterListActions))] + public void RequestDelegateThrowsWhenNullableParameterList(Delegate action) { - // Arrange - var responseBodyStream = new MemoryStream(); + var parameter = action.Method.GetParameters()[0]; var httpContext = CreateHttpContext(); - httpContext.Response.Body = responseBodyStream; - // Act - var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() + var exception = Assert.Throws(() => RequestDelegateFactory.Create(action)); + Assert.Contains($"The nullable type '{TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false)}' is not supported, mark the parameter as non-nullable.", exception.Message); + } + + [Fact] + public void RequestDelegateThrowsWhenParameterNameConflicts() + { + void TestAction(HttpContext context, [AsParameters] SampleParameterList args, [AsParameters] SampleParameterList args2) { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => async (context) => - { - return await next(context); - } - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + context.Items.Add("foo", args.Foo); + } + var httpContext = CreateHttpContext(); - Assert.Equal(200, httpContext.Response.StatusCode); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("foo", decodedResponseBody); + var exception = Assert.Throws(() => RequestDelegateFactory.Create(TestAction)); + Assert.Contains("An item with the same key has already been added. Key: Foo", exception.Message); } - public static object[][] ValueTaskOfTMethods + private class ParameterListWithReadOnlyProperties { - get + public ParameterListWithReadOnlyProperties() { - ValueTask ValueTaskOfTMethod() - { - return ValueTask.FromResult("foo"); - } + ReadOnlyValue = 1; + } - async ValueTask ValueTaskOfTWithYieldMethod() - { - await Task.Yield(); - return "foo"; - } + public int Value { get; set; } - async ValueTask ValueTaskOfObjectWithYield() - { - await Task.Yield(); - return "foo"; - } + public int ConstantValue => 1; - return new object[][] - { - new object[] { (Func>)ValueTaskOfTMethod }, - new object[] { (Func>)ValueTaskOfTWithYieldMethod }, - new object[] { (Func>)ValueTaskOfObjectWithYield } - }; - } + public int ReadOnlyValue { get; } } - [Theory] - [MemberData(nameof(ValueTaskOfTMethods))] - public async Task CanInvokeFilter_OnValueTaskOfTReturningHandler(Delegate @delegate) + string GetString(string name) + { + return $"Hello, {name}!"; + } + + [Fact] + public async Task RequestDelegateFactory_InvokesFilters_OnMethodInfoWithNullTargetFactory() { // Arrange - var responseBodyStream = new MemoryStream(); + var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( + nameof(GetString), + BindingFlags.NonPublic | BindingFlags.Instance, + new[] { typeof(string) }); var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["name"] = "TestName" + }); // Act - var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() + var factoryResult = RequestDelegateFactory.Create(methodInfo!, null, new RequestDelegateFactoryOptions() { EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { @@ -3357,59 +2351,37 @@ public async Task CanInvokeFilter_OnValueTaskOfTReturningHandler(Delegate @deleg var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); + // Assert Assert.Equal(200, httpContext.Response.StatusCode); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("foo", decodedResponseBody); - } - - public static object[][] VoidReturningMethods - { - get - { - void VoidMethod() { } - - ValueTask ValueTaskMethod() - { - return ValueTask.CompletedTask; - } - - Task TaskMethod() - { - return Task.CompletedTask; - } - - async ValueTask ValueTaskWithYieldMethod() - { - await Task.Yield(); - } - - async Task TaskWithYieldMethod() - { - await Task.Yield(); - } - - return new object[][] - { - new object[] { (Action)VoidMethod }, - new object[] { (Func)ValueTaskMethod }, - new object[] { (Func)TaskMethod }, - new object[] { (Func)ValueTaskWithYieldMethod }, - new object[] { (Func)TaskWithYieldMethod} - }; - } + Assert.Equal("Hello, TestName!", decodedResponseBody); } - [Theory] - [MemberData(nameof(VoidReturningMethods))] - public async Task CanInvokeFilter_OnVoidReturningHandler(Delegate @delegate) + [Fact] + public async Task RequestDelegateFactory_InvokesFilters_OnMethodInfoWithProvidedTargetFactory() { // Arrange - var responseBodyStream = new MemoryStream(); + var invoked = false; + var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( + nameof(GetString), + BindingFlags.NonPublic | BindingFlags.Instance, + new[] { typeof(string) }); var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); httpContext.Response.Body = responseBodyStream; + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["name"] = "TestName" + }); // Act - var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() + Func targetFactory = (context) => + { + invoked = true; + context.Items["invoked"] = true; + return Activator.CreateInstance(methodInfo!.DeclaringType!)!; + }; + var factoryResult = RequestDelegateFactory.Create(methodInfo!, targetFactory, new RequestDelegateFactoryOptions() { EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() { @@ -3422,9 +2394,13 @@ public async Task CanInvokeFilter_OnVoidReturningHandler(Delegate @delegate) var requestDelegate = factoryResult.RequestDelegate; await requestDelegate(httpContext); + // Assert Assert.Equal(200, httpContext.Response.StatusCode); + Assert.True(invoked); + var invokedInContext = Assert.IsType(httpContext.Items["invoked"]); + Assert.True(invokedInContext); var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(String.Empty, decodedResponseBody); + Assert.Equal("Hello, TestName!", decodedResponseBody); } [Fact] @@ -3464,73 +2440,6 @@ async Task HandlerWithTaskAwait(HttpContext c) Assert.Equal(string.Empty, decodedResponseBody); } - public static object[][] TasksOfTypesMethods - { - get - { - ValueTask ValueTaskOfStructMethod() - { - return ValueTask.FromResult(new TodoStruct { Name = "Test todo" }); - } - - async ValueTask ValueTaskOfStructWithYieldMethod() - { - await Task.Yield(); - return new TodoStruct { Name = "Test todo" }; - } - - Task TaskOfStructMethod() - { - return Task.FromResult(new TodoStruct { Name = "Test todo" }); - } - - async Task TaskOfStructWithYieldMethod() - { - await Task.Yield(); - return new TodoStruct { Name = "Test todo" }; - } - - return new object[][] - { - new object[] { (Func>)ValueTaskOfStructMethod }, - new object[] { (Func>)ValueTaskOfStructWithYieldMethod }, - new object[] { (Func>)TaskOfStructMethod }, - new object[] { (Func>)TaskOfStructWithYieldMethod } - }; - } - } - - [Theory] - [MemberData(nameof(TasksOfTypesMethods))] - public async Task CanInvokeFilter_OnHandlerReturningTasksOfStruct(Delegate @delegate) - { - // Arrange - var responseBodyStream = new MemoryStream(); - var httpContext = CreateHttpContext(); - httpContext.Response.Body = responseBodyStream; - - // Act - var factoryResult = RequestDelegateFactory.Create(@delegate, new RequestDelegateFactoryOptions() - { - EndpointBuilder = CreateEndpointBuilderFromFilterFactories(new List>() - { - (routeHandlerContext, next) => async (context) => - { - return await next(context); - } - }), - }); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); - - Assert.Equal(200, httpContext.Response.StatusCode); - var deserializedResponseBody = JsonSerializer.Deserialize(responseBodyStream.ToArray(), new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); - Assert.Equal("Test todo", deserializedResponseBody.Name); - } - [Fact] public void Create_DoesNotAddDelegateMethodInfo_AsMetadata() { @@ -4520,19 +3429,6 @@ private class JsonTodoChild : JsonTodo public string? Child { get; set; } } - private class CustomTodo : Todo - { - public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - Assert.Equal(typeof(CustomTodo), parameter.ParameterType); - Assert.Equal("customTodo", parameter.Name); - - var body = await context.Request.ReadFromJsonAsync(); - context.Request.Body.Position = 0; - return body; - } - } - [JsonPolymorphic] [JsonDerivedType(typeof(JsonTodoChild), nameof(JsonTodoChild))] private class JsonTodo : Todo @@ -4548,53 +3444,6 @@ private class JsonTodo : Todo private record struct TodoStruct(int Id, string? Name, bool IsComplete) : ITodo; - private interface ITodo - { - public int Id { get; } - public string? Name { get; } - public bool IsComplete { get; } - } - - class TodoJsonConverter : JsonConverter - { - public override ITodo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var todo = new Todo(); - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndObject) - { - break; - } - - var property = reader.GetString()!; - reader.Read(); - - switch (property.ToLowerInvariant()) - { - case "id": - todo.Id = reader.GetInt32(); - break; - case "name": - todo.Name = reader.GetString(); - break; - case "iscomplete": - todo.IsComplete = reader.GetBoolean(); - break; - default: - break; - } - } - - return todo; - } - - public override void Write(Utf8JsonWriter writer, ITodo value, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - } - private class FromRouteAttribute : Attribute, IFromRouteMetadata { public string? Name { get; set; } @@ -4848,11 +3697,6 @@ public TlsConnectionFeature(X509Certificate2 clientCertificate) throw new NotImplementedException(); } } - - private class EndpointFeature : IEndpointFeature - { - public Endpoint? Endpoint { get; set; } - } } internal static class TestExtensionResults diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt index b7effbfa9d86..741c5461ffe3 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_BindAsync_Snapshot.generated.txt @@ -307,6 +307,36 @@ namespace Microsoft.AspNetCore.Builder filePath, lineNumber); } + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::System.Func handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + GetVerb, + filePath, + lineNumber); + } + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::System.Func handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + GetVerb, + filePath, + lineNumber); + } } } @@ -1777,6 +1807,164 @@ namespace Microsoft.AspNetCore.Http.Generated await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); } + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + [(@"TestMapActions.cs", 43)] = ( + (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 43)); + options.EndpointBuilder.Metadata.Add(new GeneratedProducesResponseTypeMetadata(type: null, statusCode: StatusCodes.Status200OK, contentTypes: GeneratedMetadataConstants.PlaintextContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = (System.Func)del; + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!, ic.GetArgument(1)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + async Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + var httpContext_local = httpContext; + var myBindAsyncParam_temp = await global::Microsoft.AspNetCore.Http.Generators.Tests.IBindAsync.BindAsync(httpContext); + global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncFromInterfaceRecord myBindAsyncParam_local; + if ((object?)myBindAsyncParam_temp == null) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided("MyBindAsyncFromInterfaceRecord", "myBindAsyncParam", "MyBindAsyncFromInterfaceRecord.BindAsync(HttpContext)"); + wasParamCheckFailure = true; + myBindAsyncParam_local = default!; + } + else + { + myBindAsyncParam_local = (global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncFromInterfaceRecord)myBindAsyncParam_temp; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return; + } + httpContext.Response.ContentType ??= "text/plain"; + var result = handler(httpContext_local, myBindAsyncParam_local); + await httpContext.Response.WriteAsync(result); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + var httpContext_local = httpContext; + var myBindAsyncParam_temp = await global::Microsoft.AspNetCore.Http.Generators.Tests.IBindAsync.BindAsync(httpContext); + global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncFromInterfaceRecord myBindAsyncParam_local; + if ((object?)myBindAsyncParam_temp == null) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided("MyBindAsyncFromInterfaceRecord", "myBindAsyncParam", "MyBindAsyncFromInterfaceRecord.BindAsync(HttpContext)"); + wasParamCheckFailure = true; + myBindAsyncParam_local = default!; + } + else + { + myBindAsyncParam_local = (global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncFromInterfaceRecord)myBindAsyncParam_temp; + } + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, httpContext_local, myBindAsyncParam_local)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + [(@"TestMapActions.cs", 44)] = ( + (methodInfo, options) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 44)); + options.EndpointBuilder.Metadata.Add(new GeneratedProducesResponseTypeMetadata(type: null, statusCode: StatusCodes.Status200OK, contentTypes: GeneratedMetadataConstants.PlaintextContentType)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + Debug.Assert(options != null, "RequestDelegateFactoryOptions not found."); + Debug.Assert(options.EndpointBuilder != null, "EndpointBuilder not found."); + Debug.Assert(options.EndpointBuilder.ApplicationServices != null, "ApplicationServices not found."); + Debug.Assert(options.EndpointBuilder.FilterFactories != null, "FilterFactories not found."); + var handler = (System.Func)del; + EndpointFilterDelegate? filteredInvocation = null; + var serviceProvider = options.ServiceProvider ?? options.EndpointBuilder.ApplicationServices; + var logOrThrowExceptionHelper = new LogOrThrowExceptionHelper(serviceProvider, options); + + if (options.EndpointBuilder.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler(ic.GetArgument(0)!)); + }, + options.EndpointBuilder, + handler.Method); + } + + async Task RequestHandler(HttpContext httpContext) + { + var wasParamCheckFailure = false; + var myBindAsyncParam_temp = await global::Microsoft.AspNetCore.Http.Generators.Tests.IBindAsync.BindAsync(httpContext); + var myBindAsyncParam_local = (global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncFromInterfaceRecord?)myBindAsyncParam_temp; + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + return; + } + httpContext.Response.ContentType ??= "text/plain"; + var result = handler(myBindAsyncParam_local); + await httpContext.Response.WriteAsync(result); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var wasParamCheckFailure = false; + var myBindAsyncParam_temp = await global::Microsoft.AspNetCore.Http.Generators.Tests.IBindAsync.BindAsync(httpContext); + var myBindAsyncParam_local = (global::Microsoft.AspNetCore.Http.Generators.Tests.MyBindAsyncFromInterfaceRecord?)myBindAsyncParam_temp; + + if (wasParamCheckFailure) + { + httpContext.Response.StatusCode = 400; + } + var result = await filteredInvocation(EndpointFilterInvocationContext.Create(httpContext, myBindAsyncParam_local)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; return new RequestDelegateResult(targetDelegate, metadata); diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt index 880960617026..915af282c6dd 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt @@ -302,6 +302,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -313,21 +315,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -348,12 +337,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } private static Task ExecuteAsyncExplicit(IResult result, HttpContext httpContext) => result.ExecuteAsync(httpContext); diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt index ffdc340a342d..bd9f1e167d9d 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam.generated.txt @@ -257,6 +257,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -268,21 +270,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -303,12 +292,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt index ffdc340a342d..bd9f1e167d9d 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_EmptyQueryValues.generated.txt @@ -257,6 +257,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -268,21 +270,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -303,12 +292,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt index ffdc340a342d..bd9f1e167d9d 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_NullableStringArrayParam_QueryNotPresent.generated.txt @@ -257,6 +257,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -268,21 +270,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -303,12 +292,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt index 69b95f2a4274..9dae0f01030c 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ImplicitQuery_StringArrayParam.generated.txt @@ -257,6 +257,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -268,21 +270,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -303,12 +292,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt index e8457967ab56..6e883c316f43 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_JsonBodyOrService_HandlesBothJsonAndService.generated.txt @@ -255,6 +255,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -266,21 +268,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -301,12 +290,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } private static Func> ResolveJsonBodyOrService(LogOrThrowExceptionHelper logOrThrowExceptionHelper, string parameterTypeName, string parameterName, IServiceProviderIsService? serviceProviderIsService = null) { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_TakesCustomMetadataEmitter_Has_Metadata.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_TakesCustomMetadataEmitter_Has_Metadata.generated.txt index d8182248d678..43f428d4bc62 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_TakesCustomMetadataEmitter_Has_Metadata.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_TakesCustomMetadataEmitter_Has_Metadata.generated.txt @@ -241,6 +241,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -252,21 +254,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -287,12 +276,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } private static Func> ResolveJsonBodyOrService(LogOrThrowExceptionHelper logOrThrowExceptionHelper, string parameterTypeName, string parameterName, IServiceProviderIsService? serviceProviderIsService = null) { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Get_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Get_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt index fe4aa112ab43..0998446b3aab 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Get_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Get_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt @@ -256,6 +256,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -267,21 +269,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -302,12 +291,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndGet_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndGet_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt index fe4aa112ab43..0998446b3aab 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndGet_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndGet_WithArrayQueryString_AndBody_ShouldUseQueryString.generated.txt @@ -256,6 +256,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -267,21 +269,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -302,12 +291,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndPut_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndPut_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt index fe4aa112ab43..0998446b3aab 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndPut_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_PostAndPut_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt @@ -256,6 +256,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -267,21 +269,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -302,12 +291,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Post_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Post_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt index fe4aa112ab43..0998446b3aab 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Post_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapMethods_Post_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt @@ -256,6 +256,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -267,21 +269,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -302,12 +291,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt index db3800726df9..cc014855753a 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_AndBody_ShouldUseBody.generated.txt @@ -255,6 +255,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -266,21 +268,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -301,12 +290,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt index 113689f6870e..88bfda511859 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapPost_WithArrayQueryString_ShouldFail.generated.txt @@ -257,6 +257,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -268,21 +270,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -303,12 +292,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt index 5a01c21f25d1..80e87ca14b97 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt @@ -266,7 +266,7 @@ namespace Microsoft.AspNetCore.Http.Generated { if (ic.HttpContext.Response.StatusCode == 400) { - return ValueTask.FromResult(Results.Empty); + return (object?)Results.Empty; } var result = await handler(); return (object?)result; @@ -327,7 +327,7 @@ namespace Microsoft.AspNetCore.Http.Generated { if (ic.HttpContext.Response.StatusCode == 400) { - return ValueTask.FromResult(Results.Empty); + return (object?)Results.Empty; } var result = await handler(); return (object?)result; diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/VerifyAsParametersBaseline.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/VerifyAsParametersBaseline.generated.txt index 78eaba52fe76..640e8b993da4 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/VerifyAsParametersBaseline.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/VerifyAsParametersBaseline.generated.txt @@ -198,7 +198,7 @@ namespace Microsoft.AspNetCore.Http.Generated } var Value_raw = (string?)httpContext.Request.RouteValues["Value"]; var Value_temp = (string?)Value_raw; - int Value_parsed_temp = default; + global::System.Int32 Value_parsed_temp = default; if (GeneratedRouteBuilderExtensionsCore.TryParseExplicit(Value_temp!, CultureInfo.InvariantCulture, out var Value_temp_parsed_non_nullable)) { Value_parsed_temp = Value_temp_parsed_non_nullable; @@ -236,7 +236,7 @@ namespace Microsoft.AspNetCore.Http.Generated } var Value_raw = (string?)httpContext.Request.RouteValues["Value"]; var Value_temp = (string?)Value_raw; - int Value_parsed_temp = default; + global::System.Int32 Value_parsed_temp = default; if (GeneratedRouteBuilderExtensionsCore.TryParseExplicit(Value_temp!, CultureInfo.InvariantCulture, out var Value_temp_parsed_non_nullable)) { Value_parsed_temp = Value_temp_parsed_non_nullable; @@ -688,6 +688,8 @@ namespace Microsoft.AspNetCore.Http.Generated private static async ValueTask<(bool, T?)> TryResolveBodyAsync(HttpContext httpContext, LogOrThrowExceptionHelper logOrThrowExceptionHelper, bool allowEmpty, string parameterTypeName, string parameterName, bool isInferred = false) { var feature = httpContext.Features.Get(); + T? bodyValue = default; + var bodyValueSet = false; if (feature?.CanHaveBody == true) { @@ -699,21 +701,8 @@ namespace Microsoft.AspNetCore.Http.Generated } try { - var bodyValue = await httpContext.Request.ReadFromJsonAsync(); - if (!allowEmpty && bodyValue == null) - { - if (!isInferred) - { - logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); - } - else - { - logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); - } - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return (false, bodyValue); - } - return (true, bodyValue); + bodyValue = await httpContext.Request.ReadFromJsonAsync(); + bodyValueSet = bodyValue != null; } catch (BadHttpRequestException badHttpRequestException) { @@ -734,12 +723,22 @@ namespace Microsoft.AspNetCore.Http.Generated return (false, default); } } - else if (!allowEmpty) + + if (!allowEmpty && !bodyValueSet) { + if (!isInferred) + { + logOrThrowExceptionHelper.RequiredParameterNotProvided(parameterTypeName, parameterName, "body"); + } + else + { + logOrThrowExceptionHelper.ImplicitBodyNotProvided(parameterName); + } httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return (false, bodyValue); } - return (allowEmpty, default); + return (true, bodyValue); } private static Func> ResolveJsonBodyOrService(LogOrThrowExceptionHelper logOrThrowExceptionHelper, string parameterTypeName, string parameterName, IServiceProviderIsService? serviceProviderIsService = null) { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs index 41a2745b728d..e5fcf36b6385 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTestBase.cs @@ -10,7 +10,6 @@ using System.Text.Json; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Http.RequestDelegateGenerator; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Testing; @@ -21,14 +20,13 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyModel; using Microsoft.Extensions.DependencyModel.Resolution; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Http.Generators.Tests; public abstract class RequestDelegateCreationTestBase : LoggedTest { // Change this to true and run tests in development to regenerate baseline files. - public bool RegenerateBaselines ; + public bool RegenerateBaselines; protected abstract bool IsGeneratorEnabled { get; } @@ -218,12 +216,29 @@ internal HttpContext CreateHttpContextWithBody(Todo requestData, IServiceProvide return httpContext; } - internal static async Task VerifyResponseBodyAsync(HttpContext httpContext, string expectedBody, int expectedStatusCode = 200) + internal static async Task GetResponseBodyAsync(HttpContext httpContext) { var httpResponse = httpContext.Response; httpResponse.Body.Seek(0, SeekOrigin.Begin); var streamReader = new StreamReader(httpResponse.Body); - var body = await streamReader.ReadToEndAsync(); + return await streamReader.ReadToEndAsync(); + } + + internal static async Task VerifyResponseJsonBodyAsync(HttpContext httpContext, Action check, int expectedStatusCode = 200) + { + var body = await GetResponseBodyAsync(httpContext); + var deserializedObject = JsonSerializer.Deserialize(body, new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true + }); + + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + check(deserializedObject); + } + + internal static async Task VerifyResponseBodyAsync(HttpContext httpContext, string expectedBody, int expectedStatusCode = 200) + { + var body = await GetResponseBodyAsync(httpContext); Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); Assert.Equal(expectedBody, body); } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs index f3f2715b52f7..d7bf9bdef497 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs @@ -480,4 +480,40 @@ public async Task MapMethods_PostAndPut_WithArrayQueryString_AndBody_ShouldUseBo await VerifyResponseBodyAsync(httpContext, "ValueFromBody"); await VerifyAgainstBaselineUsingFile(compilation); } + + public async Task RequestDelegateHandlesArraysFromExplicitQueryStringSource() + { + var source = """ +app.MapPost("/", (HttpContext context, + [FromHeader(Name = "Custom")] int[] headerValues, + [FromQuery(Name = "a")] int[] queryValues, + [FromForm(Name = "form")] int[] formValues) => +{ + context.Items["headers"] = headerValues; + context.Items["query"] = queryValues; + context.Items["form"] = formValues; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["a"] = new(new[] { "1", "2", "3" }) + }); + + httpContext.Request.Headers["Custom"] = new(new[] { "4", "5", "6" }); + + httpContext.Request.Form = new FormCollection(new Dictionary + { + ["form"] = new(new[] { "7", "8", "9" }) + }); + + await endpoint.RequestDelegate(httpContext); + + Assert.Equal(new[] { 1, 2, 3 }, (int[])httpContext.Items["query"]!); + Assert.Equal(new[] { 4, 5, 6 }, (int[])httpContext.Items["headers"]!); + Assert.Equal(new[] { 7, 8, 9 }, (int[])httpContext.Items["form"]!); + } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.AsParameters.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.AsParameters.cs index 8aaa0f446ee1..5e69f4724921 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.AsParameters.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.AsParameters.cs @@ -88,34 +88,6 @@ static void TestAction([AsParameters] ParameterListFromHeader args) Assert.Equal(originalHeaderParam, httpContext.Items["input"]); } - [Fact] - public async Task RequestDelegatePopulatesHttpContextParametersWithoutAttribute_FromParameterList() - { - var source = """ -static void TestAction([AsParameters] ParametersListWithHttpContext args) -{ - args.HttpContext.Items.Add("input", args.HttpContext); - args.HttpContext.Items.Add("user", args.User); - args.HttpContext.Items.Add("request", args.Request); - args.HttpContext.Items.Add("response", args.Response); -} -app.MapGet("/", TestAction); -"""; - - var (_, compilation) = await RunGeneratorAsync(source); - var endpoint = GetEndpointFromCompilation(compilation); - - var httpContext = CreateHttpContext(); - httpContext.User = new ClaimsPrincipal(); - - await endpoint.RequestDelegate(httpContext); - - Assert.Same(httpContext, httpContext.Items["input"]); - Assert.Same(httpContext.User, httpContext.Items["user"]); - Assert.Same(httpContext.Request, httpContext.Items["request"]); - Assert.Same(httpContext.Response, httpContext.Items["response"]); - } - public static object[][] FromParameterListActions { get diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.BindAsync.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.BindAsync.cs index c63979916189..d941bd4e52bb 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.BindAsync.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.BindAsync.cs @@ -11,10 +11,13 @@ using System.Text.Encodings.Web; using System.Text.Json; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; +using Moq; namespace Microsoft.AspNetCore.Http.Generators.Tests; @@ -31,8 +34,7 @@ public abstract partial class RequestDelegateCreationTests : RequestDelegateCrea new object[] { "BindAsyncFromImplicitStaticAbstractInterface", false }, new object[] { "InheritBindAsync", false }, new object[] { "BindAsyncFromExplicitStaticAbstractInterface", false }, - // TODO: Fix this - //new object[] { "MyBindAsyncFromInterfaceRecord", false }, + new object[] { "MyBindAsyncFromInterfaceRecord", false }, }; public static IEnumerable BindAsyncUriTypes => @@ -140,7 +142,7 @@ public async Task MapAction_BindAsync_NonOptional_NotProvided(string bindAsyncTy var log = Assert.Single(TestSink.Writes); Assert.Equal(LogLevel.Debug, log.LogLevel); Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); - var parameters = bindAsyncType is "MySimpleBindAsyncRecord" || bindAsyncType is "InheritBindAsync" + var parameters = bindAsyncType is "MySimpleBindAsyncRecord" || bindAsyncType is "InheritBindAsync" || bindAsyncType is "MyBindAsyncFromInterfaceRecord" ? "(HttpContext)" : "(HttpContext, ParameterInfo)"; Assert.Equal($@"Required parameter ""{bindAsyncType} myBindAsyncParam"" was not provided from {bindAsyncType}.BindAsync{parameters}.", log.Message); @@ -195,4 +197,116 @@ public async Task MapAction_BindAsync_ExceptionsAreUncaught() var ex = await Assert.ThrowsAsync(() => endpoint.RequestDelegate(httpContext)); Assert.Equal("BindAsync failed", ex.Message); } + + [Fact] + public async Task BindAsyncWithBodyArgument() + { + Todo originalTodo = new() + { + Name = "Write more tests!" + }; + + var httpContext = CreateHttpContext(); + + var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); + var stream = new MemoryStream(requestBodyBytes); + httpContext.Request.Body = stream; + + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + + var jsonOptions = new JsonOptions(); + jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); + + var mock = new Mock(); + mock.Setup(m => m.GetService(It.IsAny())).Returns(t => + { + if (t == typeof(IOptions)) + { + return Options.Create(jsonOptions); + } + return null; + }); + + httpContext.RequestServices = mock.Object; + httpContext.Request.Headers.Referer = "https://example.org"; + + var source = """ +app.MapPost("/", (HttpContext context, MyBindAsyncRecord myBindAsyncParam, Todo todo) => +{ + context.Items["invoked"] = true; + context.Items[nameof(myBindAsyncParam)] = myBindAsyncParam; + context.Items[nameof(todo)] = todo; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + await endpoint.RequestDelegate(httpContext); + + Assert.True(httpContext.Items["invoked"] as bool?); + var arg = httpContext.Items["myBindAsyncParam"] as MyBindAsyncRecord; + Assert.NotNull(arg); + Assert.Equal("https://example.org/", arg!.Uri.ToString()); + var todo = httpContext.Items["todo"] as Todo; + Assert.NotNull(todo); + Assert.Equal("Write more tests!", todo!.Name); + } + + [Fact] + public async Task BindAsyncRunsBeforeBodyBinding() + { + Todo originalTodo = new() + { + Name = "Write more tests!" + }; + + var httpContext = CreateHttpContext(); + + var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); + var stream = new MemoryStream(requestBodyBytes); + httpContext.Request.Body = stream; + + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + + var jsonOptions = new JsonOptions(); + jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); + + var mock = new Mock(); + mock.Setup(m => m.GetService(It.IsAny())).Returns(t => + { + if (t == typeof(IOptions)) + { + return Options.Create(jsonOptions); + } + return null; + }); + + httpContext.RequestServices = mock.Object; + httpContext.Request.Headers.Referer = "https://example.org"; + + var source = """ +app.MapPost("/", (HttpContext context, CustomTodo customTodo, Todo todo) => +{ + context.Items["invoked"] = true; + context.Items[nameof(customTodo)] = customTodo; + context.Items[nameof(todo)] = todo; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + await endpoint.RequestDelegate(httpContext); + + Assert.True(httpContext.Items["invoked"] as bool?); + var todo0 = httpContext.Items["customTodo"] as Todo; + Assert.NotNull(todo0); + Assert.Equal("Write more tests!", todo0!.Name); + var todo1 = httpContext.Items["todo"] as Todo; + Assert.NotNull(todo1); + Assert.Equal("Write more tests!", todo1!.Name); + } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Filters.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Filters.cs new file mode 100644 index 000000000000..f3b78393160d --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Filters.cs @@ -0,0 +1,631 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using System.Text; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http.Generators.Tests; + +public abstract partial class RequestDelegateCreationTests : RequestDelegateCreationTestBase +{ + [Fact] + public async Task RequestDelegateInvokesFiltersButNotHandler_OnArgumentError() + { + var source = """ +app.MapGet("/", (HttpContext httpContext, string name) => +{ + httpContext.Items["invoked"] = true; + return $"Hello, {name}!"; +}) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + context.HttpContext.Items["filterInvoked"] = true; + context.Arguments[1] = context.Arguments[1] != null ? $"{((string)context.Arguments[1]!)}Prefix" : "NULL"; + return await next(context); +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + + // Assert + Assert.Null(httpContext.Items["invoked"]); + Assert.True(httpContext.Items["filterInvoked"] as bool?); + await VerifyResponseBodyAsync(httpContext, string.Empty, StatusCodes.Status400BadRequest); + } + + [Fact] + public async Task RequestDelegateInvokesFilters_OnDelegateWithTarget() + { + // Arrange + var source = """ +app.MapGet("/", (HttpContext httpContext, string name) => +{ + httpContext.Items["invoked"] = true; + return $"Hello, {name}!"; +}) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + context.HttpContext.Items["filterInvoked"] = true; + return await next(context); +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + var httpContext = CreateHttpContext(); + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["name"] = "TestName" + }); + + // Act + await endpoint.RequestDelegate(httpContext); + + // Assert + Assert.True(httpContext.Items["invoked"] as bool?); + Assert.True(httpContext.Items["filterInvoked"] as bool?); + await VerifyResponseBodyAsync(httpContext, "Hello, TestName!"); + } + + [Fact] + public async Task RequestDelegateCanInvokeSingleEndpointFilter_ThatProvidesCustomErrorMessage() + { + // Arrange + var source = """ +app.MapGet("/", (HttpContext httpContext, string name) => +{ + httpContext.Items["invoked"] = true; + return $"Hello, {name}!"; +}) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + if (context.HttpContext.Response.StatusCode == 400) + { + return Results.Problem("New response", statusCode: 400); + } + return await next(context); +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + // Act + await endpoint.RequestDelegate(httpContext); + + // Assert + await VerifyResponseJsonBodyAsync(httpContext, (problemDetails) => + { + Assert.Equal("New response", problemDetails.Detail); + }, StatusCodes.Status400BadRequest); + } + + [Fact] + public async Task RequestDelegateCanInvokeMultipleEndpointFilters_ThatTouchArguments() + { + // Arrange + var source = """ +void Log(HttpContext context, string arg) +{ + int loggerInvoked = (int?)context.Items["loggerInvoked"] ?? 0; + context.Items["loggerInvoked"] = loggerInvoked + 1; +} +app.MapGet("/", (string name, int age) => +{ + return $"Hello, {name}! You are {age} years old."; +}) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + context.Arguments[1] = ((int)context.Arguments[1]!) + 2; + return await next(context); +}) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + foreach (var parameter in context.Arguments) + { + Log(context.HttpContext, parameter!.ToString() ?? "no arg"); + } + return await next(context); +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["name"] = "TestName", + ["age"] = "25" + }); + + // Act + await endpoint.RequestDelegate(httpContext); + + // Assert + await VerifyResponseBodyAsync(httpContext, "Hello, TestName! You are 27 years old."); + Assert.Equal(2, httpContext.Items["loggerInvoked"]); + } + + [Fact] + public async Task RequestDelegateCanInvokeEndpointFilter_ThatUsesMethodInfo() + { + // Arrange + var source = """ +app.MapGet("/", (string name) => +{ + return $"Hello, {name}!"; +}) +.AddEndpointFilterFactory((routeHandlerContext, next) => +{ + var parameters = routeHandlerContext.MethodInfo.GetParameters(); + var isInt = parameters.Length == 2 && parameters[1].ParameterType == typeof(int); + return async (context) => + { + if (isInt) + { + context.Arguments[1] = ((int)context.Arguments[1]!) + 2; + return await next(context); + } + return "Is not an int."; + }; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["name"] = "TestName" + }); + + // Act + await endpoint.RequestDelegate(httpContext); + + // Assert + await VerifyResponseBodyAsync(httpContext, "Is not an int."); + } + + [Fact] + public async Task RequestDelegateCanInvokeEndpointFilter_ThatReadsEndpointMetadata() + { + // Arrange + var source = """ +app.MapGet("/", (IFormFileCollection formFiles) => +{ + return $"Got {formFiles.Count} files."; +}) +.AddEndpointFilterFactory((routeHandlerContext, next) => +{ + string? contentType = null; + + return async (context) => + { + contentType ??= context.HttpContext.GetEndpoint()?.Metadata.GetMetadata()?.ContentTypes.SingleOrDefault(); + + if (contentType == "multipart/form-data") + { + return "I see you expect a form."; + } + + return await next(context); + }; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream"); + var form = new MultipartFormDataContent("some-boundary"); + form.Add(fileContent, "file", "file.txt"); + + var stream = new MemoryStream(); + await form.CopyToAsync(stream); + + stream.Seek(0, SeekOrigin.Begin); + + httpContext.Request.Body = stream; + httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary"; + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + + // Act + httpContext.Features.Set(new EndpointFeature { Endpoint = endpoint }); + + await endpoint.RequestDelegate(httpContext); + + // Assert + await VerifyResponseBodyAsync(httpContext, "I see you expect a form."); + } + + [Fact] + public async Task RequestDelegateCanInvokeSingleEndpointFilter_ThatModifiesBodyParameter() + { + // Arrange + var source = """ +string PrintTodo(Todo todo) +{ + return $"{todo.Name} is {(todo.IsComplete ? "done" : "not done")}."; +}; +app.MapPost("/", PrintTodo) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + Todo originalTodo = (Todo)context.Arguments[0]!; + originalTodo!.IsComplete = !originalTodo.IsComplete; + context.Arguments[0] = originalTodo; + return await next(context); +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContextWithBody(new Todo { Name = "Write tests", IsComplete = true }); + + // Act + await endpoint.RequestDelegate(httpContext); + + // Assert + await VerifyResponseBodyAsync(httpContext, "Write tests is not done."); + } + + [Fact] + public async Task RequestDelegateCanInvokeSingleEndpointFilter_ThatModifiesResult() + { + // Arrange + var source = """ +app.MapPost("/", (string name) => +{ + return $"Hello, {name}!"; +}) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + var previousResult = await next(context); + if (previousResult is string stringResult) + { + return stringResult.ToUpperInvariant(); + } + return previousResult; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["name"] = "TestName" + }); + + // Act + await endpoint.RequestDelegate(httpContext); + + // Assert + await VerifyResponseBodyAsync(httpContext, "HELLO, TESTNAME!"); + } + + [Fact] + public async Task RequestDelegateCanInvokeMultipleEndpointFilters_ThatModifyArgumentsAndResult() + { + // Arrange + var source = """ +app.MapPost("/", (string name) => +{ + return $"Hello, {name}!"; +}) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + var previousResult = await next(context); + if (previousResult is string stringResult) + { + return stringResult.ToUpperInvariant(); + } + return previousResult; +}) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + var newValue = $"{context.GetArgument(0)}Prefix"; + context.Arguments[0] = newValue; + return await next(context); +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["name"] = "TestName" + }); + + // Act + await endpoint.RequestDelegate(httpContext); + + // Assert + await VerifyResponseBodyAsync(httpContext, "HELLO, TESTNAMEPREFIX!"); + } + + public static object[][] TaskOfTMethods + { + get + { + var taskOfTMethod = """ +Task TestAction() +{ + return Task.FromResult("foo"); +} +"""; + var taskOfTWithYieldMethod = """ +async Task TestAction() +{ + await Task.Yield(); + return "foo"; +} +"""; + var taskOfObjectWithYieldMethod = """ +async Task TestAction() +{ + await Task.Yield(); + return "foo"; +} +"""; + + return new object[][] + { + new object[] { taskOfTMethod }, + new object[] { taskOfTWithYieldMethod }, + new object[] { taskOfObjectWithYieldMethod } + }; + } + } + + [Theory] + [MemberData(nameof(TaskOfTMethods))] + public async Task CanInvokeFilter_OnTaskOfTReturningHandler(string innerSource) + { + // Arrange + var source = $$""" +{{innerSource}} +app.MapGet("/", TestAction) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + return await next(context); +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + // Act + await endpoint.RequestDelegate(httpContext); + + await VerifyResponseBodyAsync(httpContext, "foo"); + } + + public static object[][] ValueTaskOfTMethods + { + get + { + var taskOfTMethod = """ +ValueTask TestAction() +{ + return ValueTask.FromResult("foo"); +} +"""; + var taskOfTWithYieldMethod = """ +async ValueTask TestAction() +{ + await Task.Yield(); + return "foo"; +} +"""; + var taskOfObjectWithYieldMethod = """ +async ValueTask TestAction() +{ + await Task.Yield(); + return "foo"; +} +"""; + + return new object[][] + { + new object[] { taskOfTMethod }, + new object[] { taskOfTWithYieldMethod }, + new object[] { taskOfObjectWithYieldMethod } + }; + } + } + + [Theory] + [MemberData(nameof(ValueTaskOfTMethods))] + public async Task CanInvokeFilter_OnValueTaskOfTReturningHandler(string innerSource) + { + // Arrange + var source = $$""" +{{innerSource}} +app.MapGet("/", TestAction) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + return await next(context); +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + // Act + await endpoint.RequestDelegate(httpContext); + + await VerifyResponseBodyAsync(httpContext, "foo"); + } + + public static object[][] VoidReturningMethods + { + get + { + var voidMethod = "void TestAction() { }"; + + var valueTaskMethod = """ +ValueTask TestAction() +{ + return ValueTask.CompletedTask; +} +"""; + var taskMethod = """ +Task TestAction() +{ + return Task.CompletedTask; +} +"""; + var valueTaskWithYieldMethod = """ +async ValueTask TestAction() +{ + await Task.Yield(); +} +"""; + + var taskWithYieldMethod = """ +async Task TestAction() +{ + await Task.Yield(); +} +"""; + + return new object[][] + { + new object[] { voidMethod }, + new object[] { valueTaskMethod }, + new object[] { taskMethod }, + new object[] { valueTaskWithYieldMethod }, + new object[] { taskWithYieldMethod} + }; + } + } + + [Theory] + [MemberData(nameof(VoidReturningMethods))] + public async Task CanInvokeFilter_OnVoidReturningHandler(string innerSource) + { + // Arrange + var source = $$""" +{{innerSource}} +app.MapGet("/", TestAction) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + return await next(context); +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + // Act + await endpoint.RequestDelegate(httpContext); + + await VerifyResponseBodyAsync(httpContext, string.Empty); + } + + [Theory] + [MemberData(nameof(VoidReturningMethods))] + public async Task CanInvokeFilter_OnVoidReturningHandler_WithModifyingResult(string innerSource) + { + // Arrange + var source = $$""" +{{innerSource}} +app.MapGet("/", TestAction) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + var result = await next(context); + return $"Filtered: {result}"; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + // Act + await endpoint.RequestDelegate(httpContext); + + await VerifyResponseBodyAsync(httpContext, "Filtered: Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult"); + } + + public static object[][] TasksOfTypesMethods + { + get + { + var valueTaskOfStructMethod = """ +ValueTask TestAction() +{ + return ValueTask.FromResult(new TodoStruct { Name = "Test todo" }); +} +"""; + + var valueTaskOfStructWithYieldMethod = """ +async ValueTask TestAction() +{ + await Task.Yield(); + return new TodoStruct { Name = "Test todo" }; +} +"""; + + var taskOfStructMethod = """ +Task TestAction() +{ + return Task.FromResult(new TodoStruct { Name = "Test todo" }); +} +"""; + + var taskOfStructWithYieldMethod = """ +async Task TestAction() +{ + await Task.Yield(); + return new TodoStruct { Name = "Test todo" }; +} +"""; + + return new object[][] + { + new object[] { valueTaskOfStructMethod }, + new object[] { valueTaskOfStructWithYieldMethod }, + new object[] { taskOfStructMethod }, + new object[] { taskOfStructWithYieldMethod } + }; + } + } + + [Theory] + [MemberData(nameof(TasksOfTypesMethods))] + public async Task CanInvokeFilter_OnHandlerReturningTasksOfStruct(string innerSource) + { + // Arrange + var source = $$""" +{{innerSource}} +app.MapGet("/", TestAction) +.AddEndpointFilterFactory((routeHandlerContext, next) => async (context) => +{ + return await next(context); +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + // Act + await endpoint.RequestDelegate(httpContext); + + // Assert + await VerifyResponseJsonBodyAsync(httpContext, (todo) => + { + Assert.Equal("Test todo", todo.Name); + }); + } + + private class EndpointFeature : IEndpointFeature + { + public Endpoint Endpoint { get; set; } + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBodyOrService.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBodyOrService.cs index 3d364b4514ed..3bb25dd2980d 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBodyOrService.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.JsonBodyOrService.cs @@ -4,6 +4,7 @@ using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -132,4 +133,69 @@ public async Task RequestDelegateHandlesBodyParamOptionality(string innerSource, await VerifyResponseBodyAsync(httpContext, expectedBody); } } + + public static object[][] ImplicitFromBodyActions + { + get + { + var testImpliedFromBody = """ +void TestAction(HttpContext httpContext, Todo todo) +{ + httpContext.Items.Add("body", todo); +} +"""; + var testImpliedFromBodyInterface = """ +void TestAction(HttpContext httpContext, ITodo todo) +{ + httpContext.Items.Add("body", todo); +} +"""; + var testImpliedFromBodyStruct = """ +void TestAction(HttpContext httpContext, TodoStruct todo) +{ + httpContext.Items.Add("body", todo); +} +"""; + + var testImpliedFromBodyStructParameterList = """ +void TestAction([AsParameters] ParametersListWithImplicitFromBody args) +{ + args.HttpContext.Items.Add("body", args.Todo); +} +"""; + + return new[] + { + new object[] { testImpliedFromBody }, + new object[] { testImpliedFromBodyInterface }, + new object[] { testImpliedFromBodyStruct }, + new object[] { testImpliedFromBodyStructParameterList }, + }; + } + } + + [Theory] + [MemberData(nameof(ImplicitFromBodyActions))] + public async Task RequestDelegateRejectsEmptyBodyGivenImplicitFromBodyParameter(string innerSource) + { + var source = $""" +{innerSource} +app.MapPost("/", TestAction); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var serviceProvider = CreateServiceProvider(serviceCollection => + { + serviceCollection.Configure(options => options.ThrowOnBadRequest = true); + }); + var endpoint = GetEndpointFromCompilation(compilation, serviceProvider: serviceProvider); + + var httpContext = CreateHttpContext(serviceProvider); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "0"; + httpContext.Features.Set(new RequestBodyDetectionFeature(false)); + + var ex = await Assert.ThrowsAsync(() => endpoint.RequestDelegate(httpContext)); + Assert.StartsWith("Implicit body inferred for parameter", ex.Message); + Assert.EndsWith("but no body was provided. Did you mean to use a Service instead?", ex.Message); + } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Responses.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Responses.cs index 374538e3305c..954cb8886fac 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Responses.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Responses.cs @@ -357,4 +357,88 @@ public async Task SupportsIResultWithExplicitInterfaceImplementation() await VerifyResponseBodyAsync(httpContext, "Already gone!", StatusCodes.Status410Gone); } + + public static IEnumerable ComplexResult + { + get + { + var testAction = """ +app.MapPost("/", () => new Todo() { Name = "Write even more tests!" }); +"""; + + var taskTestAction = """ +app.MapPost("/", () => Task.FromResult(new Todo() { Name = "Write even more tests!" })); +"""; + + var valueTaskTestAction = """ +app.MapPost("/", () => ValueTask.FromResult(new Todo() { Name = "Write even more tests!" })); +"""; + + var staticTestAction = """ +app.MapPost("/", StaticTestAction); +static Todo StaticTestAction() => new Todo() { Name = "Write even more tests!" }; +"""; + + var staticTaskTestAction = """ +app.MapPost("/", StaticTaskTestAction); +static Task StaticTaskTestAction() => Task.FromResult(new Todo() { Name = "Write even more tests!" }); +"""; + + var staticValueTaskTestAction = """ +app.MapPost("/", StaticValueTaskTestAction); +static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(new Todo() { Name = "Write even more tests!" }); +"""; + + return new List + { + new object[] { testAction }, + new object[] { taskTestAction }, + new object[] { valueTaskTestAction }, + new object[] { staticTestAction }, + new object[] { staticTaskTestAction }, + new object[] { staticValueTaskTestAction } + }; + } + } + + [Theory] + [MemberData(nameof(ComplexResult))] + public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody(string source) + { + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + + await VerifyResponseJsonBodyAsync(httpContext, (todo) => + { + Assert.NotNull(todo); + Assert.Equal("Write even more tests!", todo!.Name); + }); + } + + [Fact] + public async Task RequestDelegateWritesComplexStructReturnValueAsJsonResponseBody() + { + var source = """ +app.MapPost("/", () => new TodoStruct(42, "Bob", true, TodoStatus.Done)); +"""; + + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + + await VerifyResponseJsonBodyAsync(httpContext, (todo) => + { + Assert.Equal(42, todo.Id); + Assert.Equal("Bob", todo.Name); + Assert.True(todo.IsComplete); + Assert.Equal(TodoStatus.Done, todo.Status); + }); + } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.SpecialTypes.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.SpecialTypes.cs new file mode 100644 index 000000000000..f74c5e367c1a --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.SpecialTypes.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using System.Numerics; +using System.Reflection; +using System.Reflection.Metadata; +using System.Security.Claims; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.RequestDelegateGenerator.StaticRouteHandlerModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http.Generators.Tests; + +public abstract partial class RequestDelegateCreationTests : RequestDelegateCreationTestBase +{ + [Fact] + public async Task RequestDelegatePopulatesHttpContextParameterWithoutAttribute() + { + var source = """ +app.MapGet("/", (HttpContext httpContext) => +{ + httpContext.Items["arg"] = httpContext; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + + Assert.Same(httpContext, httpContext.Items["arg"]); + } + + [Fact] + public async Task RequestDelegatePassHttpContextRequestAbortedAsCancellationToken() + { + var source = """ +app.MapGet("/", (HttpContext httpContext, System.Threading.CancellationToken token) => +{ + httpContext.Items["arg"] = token; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + using var cts = new CancellationTokenSource(); + var httpContext = CreateHttpContext(); + // Reset back to default HttpRequestLifetimeFeature that implements a setter for RequestAborted. + httpContext.Features.Set(new HttpRequestLifetimeFeature()); + httpContext.RequestAborted = cts.Token; + + await endpoint.RequestDelegate(httpContext); + + Assert.Equal(httpContext.RequestAborted, httpContext.Items["arg"]); + } + + [Fact] + public async Task RequestDelegatePassHttpContextUserAsClaimsPrincipal() + { + var source = """ +app.MapGet("/", (HttpContext httpContext, System.Security.Claims.ClaimsPrincipal user) => +{ + httpContext.Items["arg"] = user; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + var user = new ClaimsPrincipal(); + var httpContext = CreateHttpContext(); + httpContext.User = user; + + await endpoint.RequestDelegate(httpContext); + + Assert.Same(user, httpContext.Items["arg"]); + } + + [Fact] + public async Task RequestDelegatePassHttpContextRequestAsHttpRequest() + { + var source = """ +app.MapGet("/", (HttpContext httpContext, HttpRequest request) => +{ + httpContext.Items["arg"] = request; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + + Assert.Equal(httpContext.Request, httpContext.Items["arg"]); + } + + [Fact] + public async Task RequestDelegatePassesHttpContextResponseAsHttpResponse() + { + var source = """ +app.MapGet("/", (HttpContext httpContext, HttpResponse response) => +{ + httpContext.Items["arg"] = response; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + var httpContext = CreateHttpContext(); + + await endpoint.RequestDelegate(httpContext); + + Assert.Equal(httpContext.Response, httpContext.Items["arg"]); + } + + [Fact] + public async Task RequestDelegatePopulatesHttpContextParametersWithoutAttribute_FromParameterList() + { + var source = """ +static void TestAction([AsParameters] ParametersListWithHttpContext args) +{ + args.HttpContext.Items.Add("input", args.HttpContext); + args.HttpContext.Items.Add("user", args.User); + args.HttpContext.Items.Add("request", args.Request); + args.HttpContext.Items.Add("response", args.Response); +} +app.MapGet("/", TestAction); +"""; + + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + + var httpContext = CreateHttpContext(); + httpContext.User = new ClaimsPrincipal(); + + await endpoint.RequestDelegate(httpContext); + + Assert.Same(httpContext, httpContext.Items["input"]); + Assert.Same(httpContext.User, httpContext.Items["user"]); + Assert.Same(httpContext.Request, httpContext.Items["request"]); + Assert.Same(httpContext.Response, httpContext.Items["response"]); + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.TryParse.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.TryParse.cs index 6392dc8a758f..9213f23b869c 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.TryParse.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.TryParse.cs @@ -21,7 +21,7 @@ public static object[][] TryParsableParameters return new[] { - //// string is not technically "TryParsable", but it's the special case. + // string is not technically "TryParsable", but it's the special case. new object[] { "string", "plain string", "plain string" }, new object[] { "int", "-42", -42 }, new object[] { "uint", "42", 42U }, @@ -37,6 +37,8 @@ public static object[][] TryParsableParameters new object[] { "Half", "0.5", (Half)0.5f }, new object[] { "decimal", "0.5", 0.5m }, new object[] { "Uri", "https://example.org", new Uri("https://example.org") }, + new object[] { "Uri?", "https://example.org", new Uri("https://example.org") }, + new object[] { "Uri?", null, null }, new object[] { "DateTime", now.ToString("o"), now.ToUniversalTime() }, new object[] { "DateTimeOffset", "1970-01-01T00:00:00.0000000+00:00", DateTimeOffset.UnixEpoch }, new object[] { "TimeSpan", "00:00:42", TimeSpan.FromSeconds(42) }, @@ -242,4 +244,25 @@ public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromR Assert.Equal(expectedParameterValue, httpContext.Items["tryParsable"]); } + + [Theory] + [InlineData("void TestAction(HttpContext httpContext, [FromRoute] MyBindAsyncRecord myBindAsyncRecord) { }")] + [InlineData("void TestAction(HttpContext httpContext, [FromQuery] MyBindAsyncRecord myBindAsyncRecord) { }")] + public async Task RequestDelegateUsesTryParseOverBindAsyncGivenExplicitAttribute(string source) + { + var (_, compilation) = await RunGeneratorAsync($$""" +{{source}} +app.MapGet("/{myBindAsyncRecord}", TestAction); +"""); + var endpoint = GetEndpointFromCompilation(compilation); + + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues["myBindAsyncRecord"] = "foo"; + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["myBindAsyncRecord"] = "foo" + }); + + await Assert.ThrowsAsync(async () => await endpoint.RequestDelegate(httpContext)); + } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.cs index 0028f7b0572f..8823f162b56b 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.cs @@ -20,13 +20,8 @@ namespace Microsoft.AspNetCore.Http.Generators.Tests; public abstract partial class RequestDelegateCreationTests : RequestDelegateCreationTestBase { [Theory] - [InlineData("HttpContext")] - [InlineData("HttpRequest")] - [InlineData("HttpResponse")] [InlineData("System.IO.Pipelines.PipeReader")] [InlineData("System.IO.Stream")] - [InlineData("System.Security.Claims.ClaimsPrincipal")] - [InlineData("System.Threading.CancellationToken")] [InlineData("[FromBody] System.IO.Pipelines.PipeReader")] [InlineData("[FromBody] System.IO.Stream")] public async Task MapAction_SingleSpecialTypeParam_StringReturn(string parameterType) @@ -567,4 +562,41 @@ public async Task RequestDelegateCreation_SupportsMapFallback_NoRoute() Assert.Equal(42, httpContext.Items["id"]); Assert.Equal(200, httpContext.Response.StatusCode); } + + [Fact] + public async Task RequestDelegateHandlesStringValuesFromExplicitQueryStringSource() + { + var source = """ +app.MapPost("/", (HttpContext context, + [FromHeader(Name = "Custom")] int[] headerValues, + [FromQuery(Name = "a")] int[] queryValues, + [FromForm(Name = "form")] int[] formValues) => +{ + context.Items["headers"] = headerValues; + context.Items["query"] = queryValues; + context.Items["form"] = formValues; +}); +"""; + var (_, compilation) = await RunGeneratorAsync(source); + var endpoint = GetEndpointFromCompilation(compilation); + var httpContext = CreateHttpContext(); + + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["a"] = new(new[] { "1", "2", "3" }) + }); + + httpContext.Request.Headers["Custom"] = new(new[] { "4", "5", "6" }); + + httpContext.Request.Form = new FormCollection(new Dictionary + { + ["form"] = new(new[] { "7", "8", "9" }) + }); + + await endpoint.RequestDelegate(httpContext); + + Assert.Equal(new[] { 1, 2, 3 }, (int[])httpContext.Items["query"]!); + Assert.Equal(new[] { 4, 5, 6 }, (int[])httpContext.Items["headers"]!); + Assert.Equal(new[] { 7, 8, 9 }, (int[])httpContext.Items["form"]!); + } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/SharedTypes.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/SharedTypes.cs index ed050c310da4..ce70a1f826d4 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/SharedTypes.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/SharedTypes.cs @@ -46,7 +46,14 @@ public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) } } -public class Todo +public interface ITodo +{ + public int Id { get; } + public string? Name { get; } + public bool IsComplete { get; } +} + +public class Todo : ITodo { public int Id { get; set; } public string? Name { get; set; } = "Todo"; @@ -111,14 +118,6 @@ public enum TodoStatus NotDone } -public interface ITodo -{ - public int Id { get; } - public string? Name { get; } - public bool IsComplete { get; } - public TodoStatus Status { get; } -} - public class PrecedenceCheckTodo { public PrecedenceCheckTodo(int magicValue) @@ -184,9 +183,9 @@ public static bool TryParse(string? input, IFormatProvider? provider, out Parsab { result = new ParsableTodo { - Id = 1, - Name = "Knit kitten mittens.", - IsComplete = false + Id = 1, + Name = "Knit kitten mittens.", + IsComplete = false }; return true; } @@ -198,6 +197,19 @@ public static bool TryParse(string? input, IFormatProvider? provider, out Parsab } } +public class CustomTodo : Todo +{ + public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + Assert.Equal(typeof(CustomTodo), parameter.ParameterType); + Assert.Equal("customTodo", parameter.Name); + + var body = await context.Request.ReadFromJsonAsync(); + context.Request.Body.Position = 0; + return body; + } +} + public record MyBindAsyncRecord(Uri Uri) { public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) @@ -206,8 +218,7 @@ public record MyBindAsyncRecord(Uri Uri) { throw new UnreachableException($"Unexpected parameter type: {parameter.ParameterType}"); } - if (parameter.Name?.StartsWith("myBindAsyncParam", StringComparison.OrdinalIgnoreCase - ) == false) + if (parameter.Name?.StartsWith("myBindAsyncParam", StringComparison.OrdinalIgnoreCase) == false) { throw new UnreachableException("Unexpected parameter name"); } @@ -956,3 +967,43 @@ Task IResult.ExecuteAsync(HttpContext httpContext) return Task.CompletedTask; } } + +public class TodoJsonConverter : JsonConverter +{ + public override ITodo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var todo = new Todo(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + var property = reader.GetString()!; + reader.Read(); + + switch (property.ToLowerInvariant()) + { + case "id": + todo.Id = reader.GetInt32(); + break; + case "name": + todo.Name = reader.GetString(); + break; + case "iscomplete": + todo.IsComplete = reader.GetBoolean(); + break; + default: + break; + } + } + + return todo; + } + + public override void Write(Utf8JsonWriter writer, ITodo value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } +} diff --git a/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs b/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs index 8d517606bbee..f24b9a315cf4 100644 --- a/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs +++ b/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs @@ -104,7 +104,7 @@ public IList GetMatches(RouteValueDictionary values, RouteV // match. // // The decision tree uses a tree data structure to execute these rules across all candidates at once. - private void Walk( + private static void Walk( List results, RouteValueDictionary values, RouteValueDictionary ambientValues, @@ -233,7 +233,7 @@ internal string DebuggerDisplayString } } - private void FlattenTree(Stack branchStack, StringBuilder sb, DecisionTreeNode node) + private static void FlattenTree(Stack branchStack, StringBuilder sb, DecisionTreeNode node) { // leaf node if (node.Criteria.Count == 0) diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts index 33e0e790aae9..4a237e391c9a 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -555,7 +555,7 @@ export module DotNet { throw new Error(`JS object instance with ID ${targetInstanceId} does not exist (has it been disposed?).`); } - function disposeJSObjectReferenceById(id: number) { + export function disposeJSObjectReferenceById(id: number) { delete cachedJSObjectsById[id]; } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs index f5c4b500b951..46e4af57b4d7 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSInProcessObjectReference.cs @@ -44,6 +44,6 @@ public void Dispose() } } - [JSImport("DotNet.jsCallDispatcher.disposeJSObjectReferenceById", "blazor-internal")] + [JSImport("globalThis.DotNet.disposeJSObjectReferenceById")] private static partial void DisposeJSObjectReferenceById([JSMarshalAs] long id); } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs index ca5d958ad335..8661dd4b5ad8 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSObjectReference.cs @@ -21,7 +21,7 @@ public class JSObjectReference : IJSObjectReference protected internal long Id { get; } /// - /// Inititializes a new instance. + /// Initializes a new instance. /// /// The used for invoking JS interop calls. /// The unique identifier. @@ -55,7 +55,7 @@ public async ValueTask DisposeAsync() { Disposed = true; - await _jsRuntime.InvokeVoidAsync("DotNet.jsCallDispatcher.disposeJSObjectReferenceById", Id); + await _jsRuntime.InvokeVoidAsync("DotNet.disposeJSObjectReferenceById", Id); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSStreamReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSStreamReference.cs index be28cc17f282..5a99f85075cd 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSStreamReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSStreamReference.cs @@ -14,7 +14,7 @@ public sealed class JSStreamReference : JSObjectReference, IJSStreamReference public long Length { get; } /// - /// Inititializes a new instance. + /// Initializes a new instance. /// /// The used for invoking JS interop calls. /// The unique identifier. diff --git a/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs index ba3f469038e0..24a1ced4f4a5 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -14,8 +15,8 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Metrics; namespace Microsoft.AspNetCore.Diagnostics; @@ -540,9 +541,9 @@ public async Task UnhandledError_ExceptionNameTagAdded() var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var meterFactory = new TestMeterFactory(); - var meterRegistry = new TestMeterRegistry(meterFactory.Meters); - var instrumentRecorder = new InstrumentRecorder(meterRegistry, "Microsoft.AspNetCore.Hosting", "request-duration"); - instrumentRecorder.Register(m => + using var instrumentRecorder = new InstrumentRecorder(meterFactory, "Microsoft.AspNetCore.Hosting", "request-duration"); + using var measurementReporter = new MeasurementReporter(meterFactory, "Microsoft.AspNetCore.Hosting", "request-duration"); + measurementReporter.Register(m => { tcs.SetResult(); }); diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs index 07097d9b3576..0b6dc831ff20 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs @@ -45,7 +45,7 @@ public async Task ExceptionIsSetOnProblemDetailsContext() .UseTestServer() .Configure(app => { - app.UseDeveloperExceptionPage(); + app.UseExceptionHandler(); app.Run(context => { throw new Exception("Test exception"); diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs index 6b9cc6f9e42b..725343f8c0b3 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Net; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -9,10 +10,10 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; -using Microsoft.Extensions.Metrics; namespace Microsoft.AspNetCore.Diagnostics; @@ -917,9 +918,9 @@ public async Task UnhandledError_ExceptionNameTagAdded() var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var meterFactory = new TestMeterFactory(); - var meterRegistry = new TestMeterRegistry(meterFactory.Meters); - var instrumentRecorder = new InstrumentRecorder(meterRegistry, "Microsoft.AspNetCore.Hosting", "request-duration"); - instrumentRecorder.Register(m => + using var instrumentRecorder = new InstrumentRecorder(meterFactory, "Microsoft.AspNetCore.Hosting", "request-duration"); + using var measurementReporter = new MeasurementReporter(meterFactory, "Microsoft.AspNetCore.Hosting", "request-duration"); + measurementReporter.Register(m => { tcs.SetResult(); }); diff --git a/src/Middleware/Diagnostics/test/UnitTests/Microsoft.AspNetCore.Diagnostics.Tests.csproj b/src/Middleware/Diagnostics/test/UnitTests/Microsoft.AspNetCore.Diagnostics.Tests.csproj index b0a4f171d50f..d01bdacc7058 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/Microsoft.AspNetCore.Diagnostics.Tests.csproj +++ b/src/Middleware/Diagnostics/test/UnitTests/Microsoft.AspNetCore.Diagnostics.Tests.csproj @@ -11,6 +11,8 @@ + + diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index b745adae97dc..f684898e6d42 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -3,6 +3,7 @@ #nullable enable using System; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Text; using Microsoft.AspNetCore.Builder; @@ -29,7 +30,7 @@ static TestUtils() StreamUtilities.BodySegmentSize = 10; } - private static bool TestRequestDelegate(HttpContext context, string guid) + private static bool TestRequestDelegate(HttpContext context, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string guid) { var headers = context.Response.GetTypedHeaders(); headers.Date = DateTimeOffset.UtcNow; diff --git a/src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj b/src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj index 27ac36e46a85..a15b1fe9bf8c 100644 --- a/src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj +++ b/src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Middleware/RateLimiting/src/RateLimiterServiceCollectionExtensions.cs b/src/Middleware/RateLimiting/src/RateLimiterServiceCollectionExtensions.cs index 09f6f7ba7c5c..404f6a5e9b8c 100644 --- a/src/Middleware/RateLimiting/src/RateLimiterServiceCollectionExtensions.cs +++ b/src/Middleware/RateLimiting/src/RateLimiterServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; namespace Microsoft.AspNetCore.Builder; diff --git a/src/Middleware/RateLimiting/src/RateLimitingMetrics.cs b/src/Middleware/RateLimiting/src/RateLimitingMetrics.cs index 96d69256eea3..e06d8c405487 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMetrics.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMetrics.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Diagnostics.Metrics; using System.Runtime.CompilerServices; -using Microsoft.Extensions.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics; namespace Microsoft.AspNetCore.RateLimiting; @@ -21,7 +21,7 @@ internal sealed class RateLimitingMetrics : IDisposable public RateLimitingMetrics(IMeterFactory meterFactory) { - _meter = meterFactory.CreateMeter(MeterName); + _meter = meterFactory.Create(MeterName); _currentLeasedRequestsCounter = _meter.CreateUpDownCounter( "current-leased-requests", diff --git a/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj b/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj index 22890a28eae9..7a307f93db64 100644 --- a/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj +++ b/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj @@ -9,6 +9,7 @@ - + + diff --git a/src/Middleware/RateLimiting/test/RateLimitingMetricsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMetricsTests.cs index 9f5f7388daf9..03b054d50725 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMetricsTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMetricsTests.cs @@ -10,9 +10,9 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Metrics; using Microsoft.Extensions.Options; using Moq; @@ -25,7 +25,6 @@ public async Task Metrics_Rejected() { // Arrange var meterFactory = new TestMeterFactory(); - var meterRegistry = new TestMeterRegistry(meterFactory.Meters); var options = CreateOptionsAccessor(); options.Value.GlobalLimiter = new TestPartitionedRateLimiter(new TestRateLimiter(false)); @@ -35,11 +34,11 @@ public async Task Metrics_Rejected() var context = new DefaultHttpContext(); - using var leaseRequestDurationRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "leased-request-duration"); - using var currentLeaseRequestsRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "current-leased-requests"); - using var currentRequestsQueuedRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "current-queued-requests"); - using var queuedRequestDurationRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "queued-request-duration"); - using var leaseFailedRequestsRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "lease-failed-requests"); + using var leaseRequestDurationRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "leased-request-duration"); + using var currentLeaseRequestsRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "current-leased-requests"); + using var currentRequestsQueuedRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "current-queued-requests"); + using var queuedRequestDurationRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "queued-request-duration"); + using var leaseFailedRequestsRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "lease-failed-requests"); // Act await middleware.Invoke(context).DefaultTimeout(); @@ -66,7 +65,6 @@ public async Task Metrics_Success() var syncPoint = new SyncPoint(); var meterFactory = new TestMeterFactory(); - var meterRegistry = new TestMeterRegistry(meterFactory.Meters); var options = CreateOptionsAccessor(); options.Value.GlobalLimiter = new TestPartitionedRateLimiter(new TestRateLimiter(true)); @@ -83,11 +81,11 @@ public async Task Metrics_Success() var context = new DefaultHttpContext(); context.Request.Method = "GET"; - using var leaseRequestDurationRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "leased-request-duration"); - using var currentLeaseRequestsRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "current-leased-requests"); - using var currentRequestsQueuedRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "current-queued-requests"); - using var queuedRequestDurationRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "queued-request-duration"); - using var leaseFailedRequestsRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "lease-failed-requests"); + using var leaseRequestDurationRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "leased-request-duration"); + using var currentLeaseRequestsRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "current-leased-requests"); + using var currentRequestsQueuedRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "current-queued-requests"); + using var queuedRequestDurationRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "queued-request-duration"); + using var leaseFailedRequestsRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "lease-failed-requests"); // Act var middlewareTask = middleware.Invoke(context); @@ -122,7 +120,6 @@ public async Task Metrics_ListenInMiddleOfRequest_CurrentLeasesNotDecreased() var syncPoint = new SyncPoint(); var meterFactory = new TestMeterFactory(); - var meterRegistry = new TestMeterRegistry(meterFactory.Meters); var options = CreateOptionsAccessor(); options.Value.GlobalLimiter = new TestPartitionedRateLimiter(new TestRateLimiter(true)); @@ -144,11 +141,11 @@ public async Task Metrics_ListenInMiddleOfRequest_CurrentLeasesNotDecreased() await syncPoint.WaitForSyncPoint().DefaultTimeout(); - using var leaseRequestDurationRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "leased-request-duration"); - using var currentLeaseRequestsRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "current-leased-requests"); - using var currentRequestsQueuedRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "current-queued-requests"); - using var queuedRequestDurationRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "queued-request-duration"); - using var leaseFailedRequestsRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "lease-failed-requests"); + using var leaseRequestDurationRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "leased-request-duration"); + using var currentLeaseRequestsRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "current-leased-requests"); + using var currentRequestsQueuedRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "current-queued-requests"); + using var queuedRequestDurationRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "queued-request-duration"); + using var leaseFailedRequestsRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "lease-failed-requests"); syncPoint.Continue(); @@ -169,7 +166,6 @@ public async Task Metrics_Queued() var syncPoint = new SyncPoint(); var meterFactory = new TestMeterFactory(); - var meterRegistry = new TestMeterRegistry(meterFactory.Meters); var services = new ServiceCollection(); @@ -196,11 +192,11 @@ public async Task Metrics_Queued() routeEndpointBuilder.Metadata.Add(new EnableRateLimitingAttribute("concurrencyPolicy")); var endpoint = routeEndpointBuilder.Build(); - using var leaseRequestDurationRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "leased-request-duration"); - using var currentLeaseRequestsRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "current-leased-requests"); - using var currentRequestsQueuedRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "current-queued-requests"); - using var queuedRequestDurationRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "queued-request-duration"); - using var leaseFailedRequestsRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "lease-failed-requests"); + using var leaseRequestDurationRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "leased-request-duration"); + using var currentLeaseRequestsRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "current-leased-requests"); + using var currentRequestsQueuedRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "current-queued-requests"); + using var queuedRequestDurationRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "queued-request-duration"); + using var leaseFailedRequestsRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "lease-failed-requests"); // Act var context1 = new DefaultHttpContext(); @@ -241,7 +237,6 @@ public async Task Metrics_ListenInMiddleOfQueued_CurrentQueueNotDecreased() var syncPoint = new SyncPoint(); var meterFactory = new TestMeterFactory(); - var meterRegistry = new TestMeterRegistry(meterFactory.Meters); var services = new ServiceCollection(); @@ -284,11 +279,11 @@ public async Task Metrics_ListenInMiddleOfQueued_CurrentQueueNotDecreased() // Start listening while the second request is queued. - using var leaseRequestDurationRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "leased-request-duration"); - using var currentLeaseRequestsRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "current-leased-requests"); - using var currentRequestsQueuedRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "current-queued-requests"); - using var queuedRequestDurationRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "queued-request-duration"); - using var leaseFailedRequestsRecorder = new InstrumentRecorder(meterRegistry, RateLimitingMetrics.MeterName, "lease-failed-requests"); + using var leaseRequestDurationRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "leased-request-duration"); + using var currentLeaseRequestsRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "current-leased-requests"); + using var currentRequestsQueuedRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "current-queued-requests"); + using var queuedRequestDurationRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "queued-request-duration"); + using var leaseFailedRequestsRecorder = new InstrumentRecorder(meterFactory, RateLimitingMetrics.MeterName, "lease-failed-requests"); Assert.Empty(currentRequestsQueuedRecorder.GetMeasurements()); Assert.Empty(queuedRequestDurationRecorder.GetMeasurements()); diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index b64c7a258195..3ad783a694e9 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; -using Microsoft.Extensions.Metrics; using Microsoft.Extensions.Options; using Moq; diff --git a/src/Middleware/ResponseCaching/test/TestUtils.cs b/src/Middleware/ResponseCaching/test/TestUtils.cs index a3a64267e550..64094bcf051f 100644 --- a/src/Middleware/ResponseCaching/test/TestUtils.cs +++ b/src/Middleware/ResponseCaching/test/TestUtils.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net.Http; using System.Text; @@ -29,7 +30,7 @@ static TestUtils() StreamUtilities.BodySegmentSize = 10; } - private static bool TestRequestDelegate(HttpContext context, string guid) + private static bool TestRequestDelegate(HttpContext context, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string guid) { var headers = context.Response.GetTypedHeaders(); diff --git a/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs b/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs index e0e2bed844f5..5524a957151f 100644 --- a/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs +++ b/src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs @@ -74,15 +74,15 @@ void AnalyzeResponseExpression(IReturnOperation returnOperation) var statementReturnType = returnedValue.Type; - if (!symbolCache.IActionResult.IsAssignableFrom(statementReturnType)) + if (statementReturnType is not null && !symbolCache.IActionResult.IsAssignableFrom(statementReturnType)) { // Return expression is not an instance of IActionResult. Must be returning the "model". return new ActualApiResponseMetadata(returnOperation, statementReturnType); } var defaultStatusCodeAttribute = statementReturnType - .GetAttributes(defaultStatusCodeAttributeSymbol, inherit: true) - .FirstOrDefault(); + ?.GetAttributes(defaultStatusCodeAttributeSymbol, inherit: true) + ?.FirstOrDefault(); // If the type is not annotated with a default status code, then examine // the attributes on any invoked method returning the type. @@ -225,7 +225,7 @@ private static bool TryGetStatusCode( return false; } - internal static int? GetDefaultStatusCode(AttributeData attribute) + internal static int? GetDefaultStatusCode(AttributeData? attribute) { if (attribute != null && attribute.ConstructorArguments.Length == 1 && diff --git a/src/Mvc/Mvc.Api.Analyzers/test/ApiConventionAnalyzerIntegrationTest.cs b/src/Mvc/Mvc.Api.Analyzers/test/ApiConventionAnalyzerIntegrationTest.cs index caeefe7af91e..632f67e1da77 100644 --- a/src/Mvc/Mvc.Api.Analyzers/test/ApiConventionAnalyzerIntegrationTest.cs +++ b/src/Mvc/Mvc.Api.Analyzers/test/ApiConventionAnalyzerIntegrationTest.cs @@ -40,6 +40,10 @@ public Task NoDiagnosticsAreReturned_ForReturnStatementsInLambdas() public Task NoDiagnosticsAreReturned_ForReturnStatementsInLocalFunctions() => RunNoDiagnosticsAreReturned(); + [Fact] + public Task NoDiagnosticsAreReturned_ForApiController_WhenMethodNeverReturns() + => RunNoDiagnosticsAreReturned(); + [Fact] public async Task DiagnosticsAreReturned_ForIncompleteActionResults() { diff --git a/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/NoDiagnosticsAreReturned_ForApiController_WhenMethodNeverReturns.cs b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/NoDiagnosticsAreReturned_ForApiController_WhenMethodNeverReturns.cs new file mode 100644 index 000000000000..900d178a5623 --- /dev/null +++ b/src/Mvc/Mvc.Api.Analyzers/test/TestFiles/ApiConventionAnalyzerIntegrationTest/NoDiagnosticsAreReturned_ForApiController_WhenMethodNeverReturns.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers; + +using System; + +[ApiController] +[Route("[controller]/[action]")] +public class NoDiagnosticsAreReturned_ForApiController_WhenMethodNeverReturns : ControllerBase +{ + public IActionResult GetItem() => throw new NotImplementedException(); +} diff --git a/src/Mvc/Mvc.Core/src/Routing/ActionConstraintMatcherPolicy.cs b/src/Mvc/Mvc.Core/src/Routing/ActionConstraintMatcherPolicy.cs index 8383b317291a..5fe5152b29bb 100644 --- a/src/Mvc/Mvc.Core/src/Routing/ActionConstraintMatcherPolicy.cs +++ b/src/Mvc/Mvc.Core/src/Routing/ActionConstraintMatcherPolicy.cs @@ -151,7 +151,7 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidateSet) return EvaluateActionConstraintsCore(httpContext, candidateSet, items, startingOrder: null); } - private IReadOnlyList<(int index, ActionSelectorCandidate candidate)>? EvaluateActionConstraintsCore( + private static IReadOnlyList<(int index, ActionSelectorCandidate candidate)>? EvaluateActionConstraintsCore( HttpContext httpContext, CandidateSet candidateSet, IReadOnlyList<(int index, ActionSelectorCandidate candidate)> items, diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs index 648e2fa0d73b..ae8fc3a945f1 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/DefaultApiProblemDetailsWriterTest.cs @@ -51,6 +51,45 @@ public async Task WriteAsync_Works() Assert.Equal(expectedProblem.Instance, problemDetails.Instance); } + [Fact] + public async Task WriteAsync_Works_WhenReplacingProblemDetailsUsingSetter() + { + // Arrange + var writer = GetWriter(); + var stream = new MemoryStream(); + var context = CreateContext(stream); + var originalProblemDetails = new ProblemDetails(); + + var expectedProblem = new ProblemDetails() + { + Detail = "Custom Bad Request", + Instance = "Custom Bad Request", + Status = StatusCodes.Status400BadRequest, + Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1-custom", + Title = "Custom Bad Request", + }; + var problemDetailsContext = new ProblemDetailsContext() + { + HttpContext = context, + ProblemDetails = originalProblemDetails + }; + + problemDetailsContext.ProblemDetails = expectedProblem; + + //Act + await writer.WriteAsync(problemDetailsContext); + + //Assert + stream.Position = 0; + var problemDetails = await JsonSerializer.DeserializeAsync(stream, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + Assert.NotNull(problemDetails); + Assert.Equal(expectedProblem.Status, problemDetails.Status); + Assert.Equal(expectedProblem.Type, problemDetails.Type); + Assert.Equal(expectedProblem.Title, problemDetails.Title); + Assert.Equal(expectedProblem.Detail, problemDetails.Detail); + Assert.Equal(expectedProblem.Instance, problemDetails.Instance); + } + [Fact] public async Task WriteAsync_AddExtensions() { diff --git a/src/Mvc/test/Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs b/src/Mvc/test/Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs index ecc52726c805..1b0f3cb2dd83 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs @@ -39,7 +39,8 @@ public async Task Renders_BasicComponent() public async Task Renders_RoutingComponent() { // Arrange & Act - var client = CreateClient(Factory.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddServerSideBlazor()))); + var client = CreateClient(Factory.WithWebHostBuilder(builder => + builder.ConfigureServices(services => services.AddRazorComponents().AddServerComponents()))); var response = await client.GetAsync("http://localhost/components/routable"); @@ -69,8 +70,8 @@ public async Task Redirects_Navigation_Component() public async Task Renders_RoutingComponent_UsingRazorComponents_Prerenderer() { // Arrange & Act - var client = CreateClient(Factory - .WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddServerSideBlazor()))); + var client = CreateClient(Factory.WithWebHostBuilder(builder => + builder.ConfigureServices(services => services.AddRazorComponents().AddServerComponents()))); var response = await client.GetAsync("http://localhost/components/routable"); @@ -85,7 +86,8 @@ public async Task Renders_RoutingComponent_UsingRazorComponents_Prerenderer() public async Task Renders_ThrowingComponent_UsingRazorComponents_Prerenderer() { // Arrange & Act - var client = CreateClient(Factory.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddServerSideBlazor()))); + var client = CreateClient(Factory.WithWebHostBuilder(builder => + builder.ConfigureServices(services => services.AddRazorComponents().AddServerComponents()))); var response = await client.GetAsync("http://localhost/components/throws"); diff --git a/src/ProjectTemplates/Web.ProjectTemplates/Api-CSharp.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/Api-CSharp.csproj.in index ae3d10f13686..29f63b2c6e5b 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/Api-CSharp.csproj.in +++ b/src/ProjectTemplates/Web.ProjectTemplates/Api-CSharp.csproj.in @@ -7,8 +7,8 @@ true Company.WebApplication1 false - true + true true diff --git a/src/ProjectTemplates/Web.ProjectTemplates/GrpcService-CSharp.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/GrpcService-CSharp.csproj.in index f676e5c9f08d..6c6f9d130021 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/GrpcService-CSharp.csproj.in +++ b/src/ProjectTemplates/Web.ProjectTemplates/GrpcService-CSharp.csproj.in @@ -4,8 +4,8 @@ ${DefaultNetCoreTargetFramework} enable enable - true + true true diff --git a/src/ProjectTemplates/Web.ProjectTemplates/Worker-CSharp.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/Worker-CSharp.csproj.in index 718f5ad3a152..e16c7d8505a1 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/Worker-CSharp.csproj.in +++ b/src/ProjectTemplates/Web.ProjectTemplates/Worker-CSharp.csproj.in @@ -4,8 +4,8 @@ ${DefaultNetCoreTargetFramework} enable enable - true + true true dotnet-Company.Application1-53bc9b9d-9d6a-45d4-8429-2a2761773502 diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.IndividualB2CAuth.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.IndividualB2CAuth.razor index 4b78bfa1fba8..878bd8633615 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.IndividualB2CAuth.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.IndividualB2CAuth.razor @@ -1,5 +1,6 @@ @using Microsoft.Identity.Web @using Microsoft.Extensions.Options +@using Microsoft.AspNetCore.Authentication.OpenIdConnect; @inject IOptionsMonitor microsoftIdentityOptions @@ -23,8 +24,8 @@ private bool canEditProfile; protected override void OnInitialized() - { - var options = microsoftIdentityOptions.CurrentValue; + { + var options = microsoftIdentityOptions.Get(OpenIdConnectDefaults.AuthenticationScheme); canEditProfile = !string.IsNullOrEmpty(options.EditProfilePolicyId); } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.cs.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.cs.json index e3bc4df88b51..910a08cb2766 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.cs.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.cs.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "Webová aplikace Blazor", + "description": "Šablona projektu pro vytvoření webové aplikace Blazor, která podporuje vykreslování na straně serveru i interaktivitu klienta. Tato šablona se dá použít pro webové aplikace s bohatými dynamickými uživatelskými rozhraními (UI).", + "symbols/Framework/description": "Cílová architektura pro projekt", + "symbols/Framework/choices/net8.0/description": "Cílový net8.0", + "symbols/skipRestore/description": "Pokud se tato možnost zadá, přeskočí automatické obnovení projektu při vytvoření.", + "symbols/ExcludeLaunchSettings/description": "Určuje, jestli se má z vygenerované šablony vyloučit soubor launchSettings.json.", + "symbols/kestrelHttpPort/description": "Číslo portu, který se má použít pro koncový bod HTTP v souboru launchSettings.json.", + "symbols/kestrelHttpsPort/description": "Číslo portu, který se má použít pro koncový bod HTTPS v souboru launchSettings.json. Tato možnost se dá použít jenom v případě, že se nepoužije parametr no-https (no-https se bude ignorovat, pokud se použije IndividualAuth nebo OrganizationalAuth).", + "symbols/iisHttpPort/description": "Číslo portu, který se má použít pro koncový bod IIS Express HTTP v souboru launchSettings.json.", + "symbols/iisHttpsPort/description": "Číslo portu, který se má použít pro koncový bod IIS Express HTTPS v souboru launchSettings.json. Tato možnost se dá použít jenom v případě, že se nepoužije parametr no-https (no-https se bude ignorovat, pokud se použije IndividualAuth nebo OrganizationalAuth).", + "symbols/UseWebAssembly/displayName": "_Použít interaktivní komponenty WebAssembly", + "symbols/UseWebAssembly/description": "Pokud je zadáno, nakonfiguruje projekt tak, aby vykresloval komponenty interaktivně v prohlížeči pomocí WebAssembly.", + "symbols/UseServer/displayName": "_Použít interaktivní serverové komponenty", + "symbols/UseServer/description": "Pokud je zadáno, nakonfiguruje projekt tak, aby vykresloval komponenty interaktivně na serveru.", + "symbols/PWA/displayName": "_Progresivní webová aplikace", + "symbols/PWA/description": "Pokud je tato možnost zadaná, vytvoří progresivní webovou aplikaci (PWA), která podporuje instalaci a offline použití.", + "symbols/NoHttps/description": "Určuje, jestli se má protokol HTTPS vypnout. Tato možnost platí jenom v případě, že se pro --auth nepoužívají Individual, IndividualB2C, SingleOrg ani MultiOrg.", + "symbols/UseProgramMain/displayName": "Nepoužívat _příkazy nejvyšší úrovně", + "symbols/UseProgramMain/description": "Určuje, jestli se má místo příkazů nejvyšší úrovně generovat explicitní třída Program a metoda Main.", + "postActions/restore/description": "Obnoví balíčky NuGet vyžadované tímto projektem.", + "postActions/restore/manualInstructions/default/text": "Spustit dotnet restore" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.de.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.de.json index e3bc4df88b51..fbc11154db98 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.de.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.de.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "Blazor-Web-App", + "description": "Eine Projektvorlage zum Erstellen einer Blazor-Web-App, die sowohl serverseitiges Rendering als auch Clientinteraktivität unterstützt. Diese Vorlage kann für Web-Apps mit umfangreichen dynamischen Benutzeroberflächen (UIs) verwendet werden.", + "symbols/Framework/description": "Das Zielframework für das Projekt.", + "symbols/Framework/choices/net8.0/description": "Ziel net8.0", + "symbols/skipRestore/description": "Wenn angegeben, wird die automatische Wiederherstellung des Projekts beim Erstellen übersprungen.", + "symbols/ExcludeLaunchSettings/description": "Ob launchSettings.json aus der generierten Vorlage ausgeschlossen werden soll.", + "symbols/kestrelHttpPort/description": "Portnummer, die für den HTTP Endpunkt in launchSettings.json verwendet werden soll.", + "symbols/kestrelHttpsPort/description": "Portnummer, die für den HTTPS Endpunkt in launchSettings.json verwendet werden soll. Diese Option ist nur anwendbar, wenn der Parameter no-https nicht verwendet wird (no-https wird ignoriert, wenn entweder IndividualAuth oder OrganizationalAuth verwendet wird).", + "symbols/iisHttpPort/description": "Portnummer, die für den IIS Express HTTP Endpunkt in launchSettings.json verwendet werden soll.", + "symbols/iisHttpsPort/description": "Portnummer, die für den IIS Express HTTPS Endpunkt in launchSettings.json verwendet werden soll. Diese Option ist nur anwendbar, wenn der Parameter no-https nicht verwendet wird (no-https wird ignoriert, wenn entweder IndividualAuth oder OrganizationalAuth verwendet wird).", + "symbols/UseWebAssembly/displayName": "_Interaktive WebAssembly-Komponenten verwenden", + "symbols/UseWebAssembly/description": "Bei Angabe dieser Option wird das Projekt so konfiguriert, dass Komponenten interaktiv mithilfe von WebAssembly im Browser gerendert werden.", + "symbols/UseServer/displayName": "_Interaktive Serverkomponenten verwenden", + "symbols/UseServer/description": "Bei Angabe dieser Option wird das Projekt so konfiguriert, dass Komponenten interaktiv auf dem Server gerendert werden.", + "symbols/PWA/displayName": "_Progressive Webanwendung", + "symbols/PWA/description": "Wenn angegeben, wird eine Progressive Web Application (PWA) erstellt, die die Installation und Offlineverwendung unterstützt.", + "symbols/NoHttps/description": "Ob HTTPS deaktiviert werden soll. Diese Option gilt nur, wenn Individual, IndividualB2C, SingleOrg oder MultiOrg nicht für --auth verwendet werden.", + "symbols/UseProgramMain/displayName": "Keine Anweisungen_der obersten Ebene verwenden", + "symbols/UseProgramMain/description": "Gibt an, ob anstelle von Anweisungen der obersten Ebene eine explizite Programmklasse und eine Main-Methode generiert werden soll.", + "postActions/restore/description": "„NuGet-Pakete“ wiederherstellen, die für dieses Projekt erforderlich sind.", + "postActions/restore/manualInstructions/default/text": "„dotnet restore“ ausführen" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.en.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.en.json index e3bc4df88b51..6458afa6727f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.en.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.en.json @@ -1,7 +1,7 @@ { "author": "Microsoft", "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", + "description": "A project template for creating a Blazor web app that supports both server-side rendering and client interactivity. This template can be used for web apps with rich dynamic user interfaces (UIs).", "symbols/Framework/description": "The target framework for the project.", "symbols/Framework/choices/net8.0/description": "Target net8.0", "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", @@ -21,4 +21,4 @@ "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", "postActions/restore/description": "Restore NuGet packages required by this project.", "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.es.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.es.json index e3bc4df88b51..927cd06690ac 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.es.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.es.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "Aplicación web Blazor", + "description": "Plantilla de proyecto para crear una aplicación web de Blazor que admita tanto la representación del lado del servidor como la interactividad del cliente. Esta plantilla se puede usar para las aplicaciones web con interfaces de usuario dinámicas enriquecidas.", + "symbols/Framework/description": "Marco de destino del proyecto.", + "symbols/Framework/choices/net8.0/description": "net8.0 de destino", + "symbols/skipRestore/description": "Si se especifica, se omite la restauración automática del proyecto durante la creación.", + "symbols/ExcludeLaunchSettings/description": "Indica si se va a excluir launchSettings.json de la plantilla generada.", + "symbols/kestrelHttpPort/description": "Número de puerto que se va a usar para el punto de conexión HTTP en launchSettings.json.", + "symbols/kestrelHttpsPort/description": "Número de puerto que se va a usar para el punto de conexión HTTPS en launchSettings.json. Esta opción solo es aplicable cuando no se usa el parámetro no-https (no-https se omitirá si se usa IndividualAuth o OrganizationalAuth).", + "symbols/iisHttpPort/description": "Número de puerto que se va a usar para el punto de conexión HTTP de IIS Express en launchSettings.json.", + "symbols/iisHttpsPort/description": "Número de puerto que se va a usar para el punto de conexión HTTPS de IIS Express en launchSettings.json. Esta opción solo es aplicable cuando no se usa el parámetro no-https (no-https se omitirá si se usa IndividualAuth o OrganizationalAuth).", + "symbols/UseWebAssembly/displayName": "_Use componentes de webassembly interactivos", + "symbols/UseWebAssembly/description": "Si se especifica, esta opción configura el proyecto para representar los componentes de forma interactiva en el explorador mediante webassembly.", + "symbols/UseServer/displayName": "_Use componentes de servidor interactivos", + "symbols/UseServer/description": "Si se especifica, esta opción configura el proyecto para representar los componentes de forma interactiva en el servidor.", + "symbols/PWA/displayName": "_Aplicación web progresiva", + "symbols/PWA/description": "Si se especifica, produce una aplicación web progresiva (PWA) compatible con la instalación y el uso sin conexión.", + "symbols/NoHttps/description": "Si se va a desactivar HTTPS. Esta opción solo se aplica si Individual, IndividualB2C, SingleOrg o MultiOrg no se usan para --auth.", + "symbols/UseProgramMain/displayName": "No usar instrucciones de _nivel superior", + "symbols/UseProgramMain/description": "Indica si se debe generar una clase Program explícita y un método Main en lugar de instrucciones de nivel superior.", + "postActions/restore/description": "Restaure los paquetes NuGet necesarios para este proyecto.", + "postActions/restore/manualInstructions/default/text": "Ejecutar \"dotnet restore\"" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.fr.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.fr.json index e3bc4df88b51..9eeadb3824c6 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.fr.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.fr.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "Application web Blazor", + "description": "Modèle de projet pour la création d’une application web Blazor qui prend en charge le rendu côté serveur et l’interactivité du client. Ce modèle peut être utilisé pour les applications web avec des interfaces utilisateur dynamiques enrichies.", + "symbols/Framework/description": "Framework cible du projet.", + "symbols/Framework/choices/net8.0/description": "Cible net8.0", + "symbols/skipRestore/description": "S’il est spécifié, ignore la restauration automatique du projet lors de la création.", + "symbols/ExcludeLaunchSettings/description": "Indique s’il faut exclure launchSettings.json du modèle généré.", + "symbols/kestrelHttpPort/description": "Numéro de port à utiliser pour le point de terminaison HTTP dans launchSettings.json.", + "symbols/kestrelHttpsPort/description": "Numéro de port à utiliser pour le point de terminaison HTTPS dans launchSettings.json. Cette option s’applique uniquement lorsque le paramètre no-https n’est pas utilisé (no-https sera ignoré si IndividualAuth ou OrganizationalAuth est utilisé).", + "symbols/iisHttpPort/description": "Numéro de port à utiliser pour le point de terminaison HTTP IIS Express dans launchSettings.json.", + "symbols/iisHttpsPort/description": "Numéro de port à utiliser pour le point de terminaison HTTPS IIS Express dans launchSettings.json. Cette option s’applique uniquement lorsque le paramètre no-https n’est pas utilisé (no-https sera ignoré si IndividualAuth ou OrganizationalAuth est utilisé).", + "symbols/UseWebAssembly/displayName": "_Utiliser des composants WebAssembly interactifs", + "symbols/UseWebAssembly/description": "Si ce paramètre est spécifié, configure le projet pour afficher les composants de manière interactive sur le navigateur à l’aide de WebAssembly.", + "symbols/UseServer/displayName": "_Utiliser les composants serveur interactifs", + "symbols/UseServer/description": "Si ce paramètre est spécifié, configure le projet pour afficher les composants de manière interactive sur le serveur.", + "symbols/PWA/displayName": "_Application web progressive", + "symbols/PWA/description": "Si ce paramètre est spécifié, produit une application web progressive (PWA) qui prend en charge l’installation et l’utilisation hors connexion.", + "symbols/NoHttps/description": "Indique s’il faut désactiver HTTPS. Cette option s’applique uniquement si Individual, IndividualB2C, SingleOrg ou MultiOrg ne sont pas utilisés pour --auth.", + "symbols/UseProgramMain/displayName": "N’utilisez pas _d’instructions de niveau supérieur.", + "symbols/UseProgramMain/description": "Indique s’il faut générer une classe Programme explicite et une méthode Main au lieu d’instructions de niveau supérieur.", + "postActions/restore/description": "Restaurez les packages NuGet requis par ce projet.", + "postActions/restore/manualInstructions/default/text": "Exécuter « dotnet restore »" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.it.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.it.json index e3bc4df88b51..a21afcb165a0 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.it.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.it.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "App Web Blazor", + "description": "Modello di progetto per la creazione di un'app Web Blazor che supporta sia il rendering lato server sia l'interattività client. Questo modello può essere usato per app Web con interfacce utente dinamiche avanzate.", + "symbols/Framework/description": "Il framework di destinazione per il progetto.", + "symbols/Framework/choices/net8.0/description": "Destinazione net8.0", + "symbols/skipRestore/description": "Se specificato, ignora il ripristino automatico del progetto durante la creazione.", + "symbols/ExcludeLaunchSettings/description": "Indica se escludere launchSettings.json dal modello generato.", + "symbols/kestrelHttpPort/description": "Numero di porta da usare per l'endpoint HTTP in launchSettings.json.", + "symbols/kestrelHttpsPort/description": "Numero di porta da usare per l'endpoint HTTPS in launchSettings.json. Questa opzione è applicabile solo quando il parametro no-https non viene usato (no-https verrà ignorato se si usa IndividualAuth o OrganizationalAuth).", + "symbols/iisHttpPort/description": "Numero di porta da usare per l'endpoint HTTP in launchSettings.json.", + "symbols/iisHttpsPort/description": "Numero di porta da usare per l'endpoint HTTPS IIS Express in launchSettings.json. Questa opzione è applicabile solo quando il parametro no-https non viene usato (no-https verrà ignorato se si usa IndividualAuth o OrganizationalAuth).", + "symbols/UseWebAssembly/displayName": "_Usa componenti webassembly interattivi", + "symbols/UseWebAssembly/description": "Se specificato, configura il progetto per il rendering interattivo dei componenti nel browser tramite webassembly.", + "symbols/UseServer/displayName": "_Usa componenti server interattivi", + "symbols/UseServer/description": "Se specificato, configura il progetto per il rendering interattivo dei componenti nel server.", + "symbols/PWA/displayName": "Applicazione Web _Progressive", + "symbols/PWA/description": "Se specificato, produce un'applicazione Web progressiva (PWA) che supporta l'installazione e l'uso offline.", + "symbols/NoHttps/description": "Indica se disattivare HTTPS. Questa opzione si applica solo se Individual, IndividualB2C, SingleOrg o MultiOrg non vengono usati per --auth.", + "symbols/UseProgramMain/displayName": "Non usare_istruzioni di primo livello", + "symbols/UseProgramMain/description": "Indica se generare una classe Program esplicita e un metodo Main anziché istruzioni di primo livello.", + "postActions/restore/description": "Ripristina i pacchetti NuGet richiesti da questo progetto.", + "postActions/restore/manualInstructions/default/text": "Esegui 'dotnet restore'" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ja.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ja.json index e3bc4df88b51..1884b9dc89b3 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ja.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ja.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "Blazor Web アプリ", + "description": "サーバー側のレンダリングとクライアントの対話機能の両方をサポートする Blazor Web アプリを作成するためのプロジェクト テンプレートです。このテンプレートは、リッチな動的ユーザー インターフェイス (UI) を持つ Web アプリに使用できます。", + "symbols/Framework/description": "プロジェクトのターゲット フレームワークです。", + "symbols/Framework/choices/net8.0/description": "ターゲット net8.0", + "symbols/skipRestore/description": "指定した場合、作成時にプロジェクトの自動復元がスキップされます。", + "symbols/ExcludeLaunchSettings/description": "生成されたテンプレートから launchSettings.json を除外するかどうか。", + "symbols/kestrelHttpPort/description": "launchSettings.json の HTTP エンドポイントに使用するポート番号。", + "symbols/kestrelHttpsPort/description": "launchSettings.json で HTTPS エンドポイントに使用するポート番号。このオプションは、HTTPS 以外のパラメーターが使用されていない場合にのみ適用されます (IndividualAuth または OrganizationalAuth が使用されている場合は、HTTPS 以外は無視されます)。", + "symbols/iisHttpPort/description": "launchSettings.json の IIS Express HTTP エンドポイントに使用するポート番号。", + "symbols/iisHttpsPort/description": "launchSettings.json で IIS Express HTTPS エンドポイントに使用するポート番号。このオプションは、HTTPS 以外のパラメーターが使用されていない場合にのみ適用されます (IndividualAuth または OrganizationalAuth が使用されている場合は、HTTPS 以外は無視されます)。", + "symbols/UseWebAssembly/displayName": "_対話型 WebAssembly コンポーネントを使用する", + "symbols/UseWebAssembly/description": "指定した場合、WebAssembly を使用してブラウザー上でコンポーネントを対話的にレンダリングするようにプロジェクトを構成します。", + "symbols/UseServer/displayName": "_対話型サーバー コンポーネントを使用する", + "symbols/UseServer/description": "指定した場合、サーバー上でコンポーネントを対話的にレンダリングするようにプロジェクトを構成します。", + "symbols/PWA/displayName": "プログレッシブ Web アプリケーション(_P)", + "symbols/PWA/description": "指定した場合、インストールとオフラインでの使用をサポートするプログレッシブ Web アプリケーション (PWA) が生成されます。", + "symbols/NoHttps/description": "HTTPS をオフにするかどうか。このオプションは、Individual、IndividualB2C、SingleOrg、または MultiOrg が --auth に使用されていない場合にのみ適用されます。", + "symbols/UseProgramMain/displayName": "最上位レベルのステートメントを使用しない(_T)", + "symbols/UseProgramMain/description": "最上位レベルのステートメントではなく、明示的な Program クラスと Main メソッドを生成するかどうか。", + "postActions/restore/description": "このプロジェクトに必要な NuGet パッケージを復元します。", + "postActions/restore/manualInstructions/default/text": "'dotnet restore' を実行する" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ko.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ko.json index e3bc4df88b51..48c2f9249534 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ko.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ko.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "Blazor 웹앱", + "description": "서버 측 렌더링 및 클라이언트 대화형 작업을 모두 지원하는 Blazor 웹앱을 만들기 위한 프로젝트 템플릿입니다. 이 템플릿은 풍부한 동적 UI(사용자 인터페이스)가 있는 웹앱에 사용할 수 있습니다.", + "symbols/Framework/description": "프로젝트에 대한 대상 프레임워크입니다.", + "symbols/Framework/choices/net8.0/description": "대상 net8.0", + "symbols/skipRestore/description": "지정된 경우, 프로젝트 생성 시 자동 복원을 건너뜁니다.", + "symbols/ExcludeLaunchSettings/description": "생성된 템플릿에서 launchSettings.json을 제외할지 여부입니다.", + "symbols/kestrelHttpPort/description": "launchSettings.json의 HTTP 엔드포인트에 사용할 포트 번호입니다.", + "symbols/kestrelHttpsPort/description": "launchSettings.json의 HTTPS 엔드포인트에 사용할 포트 번호입니다. 이 옵션은 매개 변수 no-https가 사용되지 않은 경우에만 적용됩니다(IndividualAuth 또는 OrganizationalAuth가 사용되는 경우 no-https는 무시됨).", + "symbols/iisHttpPort/description": "launchSettings.json의 IIS Express HTTP 엔드포인트에 사용할 포트 번호입니다.", + "symbols/iisHttpsPort/description": "launchSettings.json의 IIS Express 엔드포인트에 사용할 포트 번호입니다. 이 옵션은 매개 변수 no-https가 사용되지 않은 경우에만 적용됩니다(IndividualAuth 또는 OrganizationalAuth가 사용되는 경우 no-https는 무시됨).", + "symbols/UseWebAssembly/displayName": "_대화형 웹어셈블리 구성 요소 사용", + "symbols/UseWebAssembly/description": "지정되는 경우 웹어셈블리를 사용하여 브라우저에서 대화형으로 구성 요소를 렌더링하도록 프로젝트를 구성합니다.", + "symbols/UseServer/displayName": "_대화형 서버 구성 요소 사용", + "symbols/UseServer/description": "지정되는 경우 서버에서 대화형으로 구성 요소를 렌더링하도록 프로젝트를 구성합니다.", + "symbols/PWA/displayName": "프로그레시브 웹 애플리케이션(_P)", + "symbols/PWA/description": "지정된 경우 설치 및 오프라인 사용을 지원하는 PWA(프로그레시브 웹 응용 프로그램)를 생성합니다.", + "symbols/NoHttps/description": "HTTPS를 끌지 여부입니다. 이 옵션은 Individual, IndividualB2C, SingleOrg 또는 MultiOrg가 --auth에 사용되지 않는 경우에만 적용됩니다.", + "symbols/UseProgramMain/displayName": "최상위 문 사용 안 함(_T)", + "symbols/UseProgramMain/description": "최상위 문 대신 명시적 Program 클래스 및 Main 메서드를 생성할지 여부입니다.", + "postActions/restore/description": "이 프로젝트에 필요한 NuGet 패키지를 복원합니다.", + "postActions/restore/manualInstructions/default/text": "'dotnet restore' 실행" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.pl.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.pl.json index e3bc4df88b51..24c8af6bbc5a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.pl.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.pl.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "Aplikacja internetowa Blazor", + "description": "Szablon projektu służący do tworzenia aplikacji internetowej platformy Blazor, która obsługuje renderowanie po stronie serwera i interakcyjność klienta. Ten szablon może być używany dla aplikacji internetowych z zaawansowanymi dynamicznymi interfejsami użytkownika.", + "symbols/Framework/description": "Platforma docelowa dla tego projektu.", + "symbols/Framework/choices/net8.0/description": "Docelowa platforma net8.0", + "symbols/skipRestore/description": "Jeśli ta opcja jest określona, pomija automatyczne przywracanie projektu podczas tworzenia.", + "symbols/ExcludeLaunchSettings/description": "Określa, czy wykluczyć plik launchSettings.json z wygenerowanego szablonu.", + "symbols/kestrelHttpPort/description": "Numer portu do użycia dla punktu końcowego HTTP w pliku launchSettings.json.", + "symbols/kestrelHttpsPort/description": "Numer portu do użycia dla punktu końcowego HTTPS w pliku launchSettings.json. Ta opcja ma zastosowanie tylko wtedy, gdy parametr no-https nie jest używany (parametr no-https zostanie zignorowany, jeśli zostanie użyte uwierzytelnianie IndividualAuth lub OrganizationalAuth).", + "symbols/iisHttpPort/description": "Numer portu do użycia dla punktu końcowego HTTP usług IIS Express w pliku launchSettings.json.", + "symbols/iisHttpsPort/description": "Numer portu do użycia dla punktu końcowego HTTPS usług IIS Express w pliku launchSettings.json. Ta opcja ma zastosowanie tylko wtedy, gdy nie jest używany parametr no-https (jeśli zostanie użyte uwierzytelnianie IndividualAuth lub OrganizationalAuth, parametr no-https zostanie zignorowana).", + "symbols/UseWebAssembly/displayName": "_Używanie interakcyjnych składników webassembly", + "symbols/UseWebAssembly/description": "Jeśli określono, konfiguruje projekt do renderowania składników w sposób interakcyjny w przeglądarce przy użyciu składnika webassembly.", + "symbols/UseServer/displayName": "_Używanie interakcyjnych składników serwera", + "symbols/UseServer/description": "Jeśli określono, konfiguruje projekt do renderowania składników w sposób interakcyjny na serwerze.", + "symbols/PWA/displayName": "_Progresywna aplikacja internetowa", + "symbols/PWA/description": "Jeśli zostanie określony, tworzy progresywną aplikację internetową (PWA) obsługującą instalację i używanie w trybie offline.", + "symbols/NoHttps/description": "Określa, czy wyłączyć protokół HTTPS. Ta opcja ma zastosowanie tylko wtedy, gdy dla uwierzytelniania --auth nie są używane elementy Individual, IndividualB2C, SingleOrg lub MultiOrg.", + "symbols/UseProgramMain/displayName": "Nie używaj ins_trukcji najwyższego poziomu", + "symbols/UseProgramMain/description": "Określa, czy wygenerować jawną klasę Program i metodę Main zamiast instrukcji najwyższego poziomu.", + "postActions/restore/description": "Przywróć pakiety NuGet wymagane przez ten projekt.", + "postActions/restore/manualInstructions/default/text": "Uruchom polecenie \"dotnet restore\"" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.pt-BR.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.pt-BR.json index e3bc4df88b51..8686e2cbe61a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.pt-BR.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.pt-BR.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "Aplicativo Web Blazor", + "description": "Um modelo de projeto para criar um aplicativo Web Blazor que dá suporte à renderização do lado do servidor e à interatividade do cliente. Este modelo pode ser usado para aplicativos da Web com interfaces de usuário (UIs) dinâmicas avançadas.", + "symbols/Framework/description": "A estrutura de destino do projeto.", + "symbols/Framework/choices/net8.0/description": "net8.0 de destino", + "symbols/skipRestore/description": "Se especificado, ignora a restauração automática do projeto sendo criado.", + "symbols/ExcludeLaunchSettings/description": "Se deve excluir launchSettings.json do modelo gerado.", + "symbols/kestrelHttpPort/description": "Número da porta a ser usada para o ponto de extremidade HTTP em launchSettings.json.", + "symbols/kestrelHttpsPort/description": "Número da porta a ser usada para o ponto de extremidade HTTPS em launchSettings.json. Essa opção só é aplicável quando o parâmetro no-https não é usado (no-https será ignorado se IndividualAuth ou OrganizationalAuth for usado).", + "symbols/iisHttpPort/description": "Número da porta a ser usada para o ponto de extremidade HTTP do IIS Express em launchSettings.json.", + "symbols/iisHttpsPort/description": "Número da porta a ser usada para o ponto de extremidade HTTPS do IIS Express em launchSettings.json. Essa opção só é aplicável quando o parâmetro no-https não é usado (no-https será ignorado se IndividualAuth ou OrganizationalAuth for usado).", + "symbols/UseWebAssembly/displayName": "_Use componentes de webassembly interativos", + "symbols/UseWebAssembly/description": "Se especificado, configura o projeto para renderizar componentes interativamente no navegador usando webassembly.", + "symbols/UseServer/displayName": "_Use componentes de servidor interativos", + "symbols/UseServer/description": "Se especificado, configura o projeto para renderizar componentes interativamente no servidor.", + "symbols/PWA/displayName": "_Aplicativo da Web Progressivo", + "symbols/PWA/description": "Se especificado, produz um Progressive Web Application (PWA) com suporte para instalação e uso offline.", + "symbols/NoHttps/description": "Se o HTTPS deve ser desativado. Essa opção se aplica somente se Individual, IndividualB2C, SingleOrg ou MultiOrg não forem usados para --auth.", + "symbols/UseProgramMain/displayName": "Não use ins_truções de nível superior", + "symbols/UseProgramMain/description": "Se deve gerar uma classe de Programa explícita e um método principal em vez de instruções de nível superior.", + "postActions/restore/description": "Restaure os pacotes NuGet exigidos por este projeto.", + "postActions/restore/manualInstructions/default/text": "Executa 'dotnet restore'" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ru.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ru.json index e3bc4df88b51..488aa72d591a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ru.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.ru.json @@ -1,24 +1,24 @@ -{ - "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} +{ + "author": "Майкрософт", + "name": "Веб-приложение Blazor", + "description": "Шаблон проекта для создания приложения Blazor, поддерживающего как отрисовку на стороне сервера, так и интерактивные возможности клиента. Этот шаблон можно использовать для веб-приложений с многофункциональными динамическими пользовательскими интерфейсами (UI).", + "symbols/Framework/description": "Целевая платформа для проекта.", + "symbols/Framework/choices/net8.0/description": "Целевая net8.0", + "symbols/skipRestore/description": "Если установлено, автоматическое восстановление проекта при создании пропускается.", + "symbols/ExcludeLaunchSettings/description": "Следует ли исключить launchSettings.json из созданного шаблона.", + "symbols/kestrelHttpPort/description": "Номер порта, используемый для конечной точки HTTP в launchSettings.json.", + "symbols/kestrelHttpsPort/description": "Номер порта, используемый для конечной точки HTTPS в launchSettings.json. Этот параметр применим только в том случае, если no-https не используется (при использовании IndividualAuth или OrganizationalAuth no-https игнорируется).", + "symbols/iisHttpPort/description": "Номер порта, используемый для конечной точки HTTP IIS Express в launchSettings.json.", + "symbols/iisHttpsPort/description": "Номер порта, используемый для конечной точки HTTPS IIS Express в launchSettings.json. Этот параметр применим только в том случае, если no-https не используется (при использовании IndividualAuth или OrganizationalAuth no-https игнорируется).", + "symbols/UseWebAssembly/displayName": "_Использовать интерактивные компоненты WebAssembly", + "symbols/UseWebAssembly/description": "Если указано, настраивает проект для интерактивного отображения компонентов в браузере с помощью WebAssembly.", + "symbols/UseServer/displayName": "_Использовать интерактивные компоненты сервера", + "symbols/UseServer/description": "Если указано, настраивает проект для интерактивного отображения компонентов на сервере.", + "symbols/PWA/displayName": "_Прогрессивное веб-приложение", + "symbols/PWA/description": "Если указывается, используется для создания прогрессивного веб-приложения (PWA), поддерживающего установку и автономное использование.", + "symbols/NoHttps/description": "Следует ли отключить HTTPS. Этот параметр применяется, только если для --auth не используются Individual, IndividualB2C, SingleOrg или MultiOrg.", + "symbols/UseProgramMain/displayName": "Не использовать _операторы верхнего уровня", + "symbols/UseProgramMain/description": "Следует ли создавать явный класс Program и метод Main вместо операторов верхнего уровня.", + "postActions/restore/description": "Восстановление пакетов NuGet, необходимых для этого проекта.", + "postActions/restore/manualInstructions/default/text": "Выполнить команду \"dotnet restore\"" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.tr.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.tr.json index e3bc4df88b51..8244287d4a46 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.tr.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.tr.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "Blazor Web Uygulaması", + "description": "Hem sunucu tarafı işlemeyi hem de istemci etkileşimini destekleyen bir Blazor web uygulaması oluşturmaya yönelik proje şablonu. Bu şablon, zengin dinamik kullanıcı arabirimlerine (UI) sahip web uygulamaları için kullanılabilir.", + "symbols/Framework/description": "Projenin hedef çerçevesi.", + "symbols/Framework/choices/net8.0/description": "Hedef net8.0", + "symbols/skipRestore/description": "Belirtilirse, oluşturma sırasında projenin otomatik geri yüklenmesini atlar.", + "symbols/ExcludeLaunchSettings/description": "launchSettings.json öğesinin oluşturulan şablondan dışlanıp dışlanmayacağı.", + "symbols/kestrelHttpPort/description": "launchSettings.json içinde HTTP uç noktası için kullanılacak bağlantı noktası numarası.", + "symbols/kestrelHttpsPort/description": "launchSettings.json içinde HTTPS uç noktası için kullanılacak bağlantı noktası numarası. Bu seçenek yalnızca no-https parametresi kullanılmazsa uygulanabilir (IndividualAuth veya OrganizationalAuth kullanılırsa no-https yoksayılır).", + "symbols/iisHttpPort/description": "launchSettings.json içinde IIS Express HTTP uç noktası için kullanılacak bağlantı noktası numarası.", + "symbols/iisHttpsPort/description": "launchSettings.json içinde IIS Express HTTPS uç noktası için kullanılacak bağlantı noktası numarası. Bu seçenek yalnızca no-https parametresi kullanılmazsa uygulanabilir (IndividualAuth veya OrganizationalAuth kullanılırsa no-https yoksayılır).", + "symbols/UseWebAssembly/displayName": "_Etkileşimli WebAssembly bileşenlerini kullanın", + "symbols/UseWebAssembly/description": "Belirtilmişse, projeyi Webassembly kullanarak tarayıcıda etkileşimli olarak işleyecek şekilde yapılandırır.", + "symbols/UseServer/displayName": "_Etkileşimli sunucu bileşenlerini kullanın", + "symbols/UseServer/description": "Belirtilmişse, projeyi bileşenleri sunucuda etkileşimli olarak işleyecek şekilde yapılandırır.", + "symbols/PWA/displayName": "_Aşamalı Web Uygulaması", + "symbols/PWA/description": "Belirtilmişse, yükleme ve çevrimdışı kullanımı destekleyen bir Aşamalı Web Uygulaması (PWA) oluşturur.", + "symbols/NoHttps/description": "HTTPS'nin kapatılıp kapatılmayacağı. Bu seçenek yalnızca Bireysel, IndividualB2C, SingleOrg veya MultiOrg -- auth için kullanılmazsa geçerlidir.", + "symbols/UseProgramMain/displayName": "_Üst düzey deyimler kullanmayın", + "symbols/UseProgramMain/description": "Üst düzey deyimler yerine açık bir Program sınıfı ve Ana yöntem oluşturup oluşturulmayacağını belirtir.", + "postActions/restore/description": "Bu projenin gerektirdiği NuGet paketlerini geri yükleyin.", + "postActions/restore/manualInstructions/default/text": "'dotnet restore' çalıştır" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.zh-Hans.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.zh-Hans.json index e3bc4df88b51..15950cf1a0b2 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.zh-Hans.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.zh-Hans.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "Blazor Web 应用", + "description": "用于创建支持服务器端呈现和客户端交互的 Blazor Web 应用的项目模板。此模板可用于具有丰富动态用户界面 (UI) 的 Web 应用。", + "symbols/Framework/description": "项目的目标框架。", + "symbols/Framework/choices/net8.0/description": "目标 net8.0", + "symbols/skipRestore/description": "如果指定,则在创建时跳过项目的自动还原。", + "symbols/ExcludeLaunchSettings/description": "是否从生成的模板中排除 launchSettings.json。", + "symbols/kestrelHttpPort/description": "要用于 launchSettings.json 中 HTTP 终结点的端口号。", + "symbols/kestrelHttpsPort/description": "要用于 launchSettings.json 中 HTTPS 终结点的端口号。仅当不使用参数 no-https 时,此选项才适用(如果使用 IndividualAuth 或 OrganizationalAuth,则将忽略 no-https)。", + "symbols/iisHttpPort/description": "要用于 launchSettings.json 中 IIS Express HTTP 终结点的端口号。", + "symbols/iisHttpsPort/description": "要用于 launchSettings.json 中 IIS Express HTTPS 终结点的端口号。仅当不使用参数 no-https 时,此选项才适用(如果使用 IndividualAuth 或 OrganizationalAuth,则将忽略 no-https)。", + "symbols/UseWebAssembly/displayName": "使用交互式 WebAssembly 组件(_U)", + "symbols/UseWebAssembly/description": "如果指定,则将项目配置为使用 WebAssembly 在浏览器上以交互方式呈现组件。", + "symbols/UseServer/displayName": "使用交互式服务器组件(_U)", + "symbols/UseServer/description": "如果指定,则将项目配置为在服务器上以交互方式呈现组件。", + "symbols/PWA/displayName": "渐进式 Web 应用程序(_P)", + "symbols/PWA/description": "如果指定,则生成支持安装和脱机使用的渐进式 Web 应用程序(PWA)。", + "symbols/NoHttps/description": "是否禁用 HTTPS。仅当 Individual、IndividualB2C、SingleOrg 或 MultiOrg 不用于 --auth 时,此选项才适用。", + "symbols/UseProgramMain/displayName": "不使用顶级语句(_T)", + "symbols/UseProgramMain/description": "是否生成显式程序类和主方法,而不是顶级语句。", + "postActions/restore/description": "还原此项目所需的 NuGet 包。", + "postActions/restore/manualInstructions/default/text": "运行 \"dotnet restore\"" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.zh-Hant.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.zh-Hant.json index e3bc4df88b51..71eebea029ac 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.zh-Hant.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/.template.config/localize/templatestrings.zh-Hant.json @@ -1,24 +1,24 @@ -{ +{ "author": "Microsoft", - "name": "Blazor Web App", - "description": "A project template for creating a Blazor app hosted inside an ASP.NET app that runs on the server. This template can be used for web apps with rich dynamic user interfaces (UIs).", - "symbols/Framework/description": "The target framework for the project.", - "symbols/Framework/choices/net8.0/description": "Target net8.0", - "symbols/skipRestore/description": "If specified, skips the automatic restore of the project on create.", - "symbols/ExcludeLaunchSettings/description": "Whether to exclude launchSettings.json from the generated template.", - "symbols/kestrelHttpPort/description": "Port number to use for the HTTP endpoint in launchSettings.json.", - "symbols/kestrelHttpsPort/description": "Port number to use for the HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/iisHttpPort/description": "Port number to use for the IIS Express HTTP endpoint in launchSettings.json.", - "symbols/iisHttpsPort/description": "Port number to use for the IIS Express HTTPS endpoint in launchSettings.json. This option is only applicable when the parameter no-https is not used (no-https will be ignored if either IndividualAuth or OrganizationalAuth is used).", - "symbols/UseWebAssembly/displayName": "_Use interactive webassembly components", - "symbols/UseWebAssembly/description": "If specified, configures the project to render components interactively on the browser using webassembly.", - "symbols/UseServer/displayName": "_Use interactive server components", - "symbols/UseServer/description": "If specified, configures the project to render components interactively on the server.", - "symbols/PWA/displayName": "_Progressive Web Application", - "symbols/PWA/description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use.", - "symbols/NoHttps/description": "Whether to turn off HTTPS. This option only applies if Individual, IndividualB2C, SingleOrg, or MultiOrg aren't used for --auth.", - "symbols/UseProgramMain/displayName": "Do not use _top-level statements", - "symbols/UseProgramMain/description": "Whether to generate an explicit Program class and Main method instead of top-level statements.", - "postActions/restore/description": "Restore NuGet packages required by this project.", - "postActions/restore/manualInstructions/default/text": "Run 'dotnet restore'" -} + "name": "Blazor Web 應用程式", + "description": "用於建立同時支援伺服器端轉譯和用戶端互動的 Blazor Web 應用程式的專案範本。此範本可用於具有豐富動態使用者介面 (UI) 的 Web 應用程式。", + "symbols/Framework/description": "專案的目標 Framework。", + "symbols/Framework/choices/net8.0/description": "目標 net8.0", + "symbols/skipRestore/description": "若指定,會在建立時跳過專案的自動還原。", + "symbols/ExcludeLaunchSettings/description": "是否要從產生的範本排除 launchSettings.json。", + "symbols/kestrelHttpPort/description": "launchSettings.json 中 HTTP 端點要使用的連接埠號碼。", + "symbols/kestrelHttpsPort/description": "launchSettings.json 中 HTTPS 端點要使用的連接埠號碼。只有在未使用參數 no-https 時,才適用此選項 (如果使用 IndividualAuth 或 OrganizationalAuth,則會忽略 no-https)。", + "symbols/iisHttpPort/description": "launchSettings.json 中 IIS Express HTTP 端點要使用的連接埠號碼。", + "symbols/iisHttpsPort/description": "launchSettings.json 中 IIS Express HTTPS 端點要使用的連接埠號碼。只有在未使用參數 no-https 時,才適用此選項 (如果使用 IndividualAuth 或 OrganizationalAuth,則會忽略 no-https)。", + "symbols/UseWebAssembly/displayName": "使用互動式網頁元件(_U)", + "symbols/UseWebAssembly/description": "若指定,會將專案設定為使用 WebAssembly 在瀏覽器上以互動方式轉譯元件。", + "symbols/UseServer/displayName": "使用互動式伺服器元件(_U)", + "symbols/UseServer/description": "若指定,會將專案設定為在伺服器上以互動方式轉譯元件。", + "symbols/PWA/displayName": "漸進式 Web 應用程式(_P)", + "symbols/PWA/description": "若指定,會產生漸進式 Web 應用程式 (PWA) 支援安裝與離線使用。", + "symbols/NoHttps/description": "是否要關閉 HTTPS。只有當 Individual、IndividualB2C、SingleOrg 或 MultiOrg 未用於 --auth 時,才適用此選項。", + "symbols/UseProgramMain/displayName": "不要使用最上層陳述式(_T)", + "symbols/UseProgramMain/description": "是否要產生明確的 Program 類別和 Main 方法,而非最上層語句。", + "postActions/restore/description": "還原此專案所需的 NuGet 套件。", + "postActions/restore/manualInstructions/default/text": "執行 'dotnet restore'" +} \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/Properties/launchSettings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/Properties/launchSettings.json index 3fd8a4e646db..c6a17ec14598 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/Properties/launchSettings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/Components-CSharp/Properties/launchSettings.json @@ -18,7 +18,9 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, + //#if (UseWebAssembly) "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + //#endif "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -30,7 +32,9 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, + //#if (UseWebAssembly) "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + //#endif "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -40,7 +44,9 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + //#if (UseWebAssembly) "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + //#endif "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs b/src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs index 1922f3516239..90bbfffe12a9 100644 --- a/src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs +++ b/src/ProjectTemplates/test/Templates.Tests/ApiTemplateTest.cs @@ -73,7 +73,10 @@ private async Task ApiTemplateCore(string languageOverride, string[] args = null : new[] { "http", "IIS Express" }; await project.VerifyLaunchSettings(expectedLaunchProfileNames); - await project.VerifyHasProperty("InvariantGlobalization", "true"); + if (nativeAot) + { + await project.VerifyHasProperty("InvariantGlobalization", "true"); + } // Avoid the F# compiler. See https://github.com/dotnet/aspnetcore/issues/14022 if (languageOverride != null) diff --git a/src/ProjectTemplates/test/Templates.Tests/GrpcTemplateTest.cs b/src/ProjectTemplates/test/Templates.Tests/GrpcTemplateTest.cs index 2f4a01b39e62..13cb09f79b85 100644 --- a/src/ProjectTemplates/test/Templates.Tests/GrpcTemplateTest.cs +++ b/src/ProjectTemplates/test/Templates.Tests/GrpcTemplateTest.cs @@ -85,7 +85,10 @@ private async Task GrpcTemplateCore(string[] args = null) var expectedLaunchProfileNames = new[] { "http", "https" }; await project.VerifyLaunchSettings(expectedLaunchProfileNames); - await project.VerifyHasProperty("InvariantGlobalization", "true"); + if (nativeAot) + { + await project.VerifyHasProperty("InvariantGlobalization", "true"); + } // Force a restore if native AOT so that RID-specific assets are restored await project.RunDotNetPublishAsync(noRestore: !nativeAot); diff --git a/src/ProjectTemplates/test/Templates.Tests/WorkerTemplateTest.cs b/src/ProjectTemplates/test/Templates.Tests/WorkerTemplateTest.cs index 8206e8a78e58..d948a1c34551 100644 --- a/src/ProjectTemplates/test/Templates.Tests/WorkerTemplateTest.cs +++ b/src/ProjectTemplates/test/Templates.Tests/WorkerTemplateTest.cs @@ -58,7 +58,10 @@ private async Task WorkerTemplateCoreAsync(string language, string[] args) await project.RunDotNetNewAsync("worker", language: language, args: args); - await project.VerifyHasProperty("InvariantGlobalization", "true"); + if (nativeAot) + { + await project.VerifyHasProperty("InvariantGlobalization", "true"); + } // Force a restore if native AOT so that RID-specific assets are restored await project.RunDotNetPublishAsync(noRestore: !nativeAot); diff --git a/src/Servers/Connections.Abstractions/src/DefaultConnectionContext.cs b/src/Servers/Connections.Abstractions/src/DefaultConnectionContext.cs index da2449ae21f5..74290d0a0425 100644 --- a/src/Servers/Connections.Abstractions/src/DefaultConnectionContext.cs +++ b/src/Servers/Connections.Abstractions/src/DefaultConnectionContext.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using System.Net; using System.Security.Claims; @@ -40,7 +41,7 @@ public DefaultConnectionContext() : /// The caller is expected to set the and pipes manually. /// /// The . - public DefaultConnectionContext(string id) + public DefaultConnectionContext([StringSyntax(StringSyntaxAttribute.GuidFormat)] string id) { ConnectionId = id; @@ -61,7 +62,7 @@ public DefaultConnectionContext(string id) /// The . /// The . /// The . - public DefaultConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application) + public DefaultConnectionContext([StringSyntax(StringSyntaxAttribute.GuidFormat)] string id, IDuplexPipe transport, IDuplexPipe application) : this(id) { Transport = transport; diff --git a/src/Servers/Connections.Abstractions/src/Features/IReconnectFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IReconnectFeature.cs new file mode 100644 index 000000000000..81a9ebe587a2 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/Features/IReconnectFeature.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Connections.Abstractions; + +/// +/// +/// +public interface IReconnectFeature +{ + /// + /// + /// + public Action NotifyOnReconnect { get; set; } + + // TODO + // void DisableReconnect(); +} diff --git a/src/Servers/Connections.Abstractions/src/Features/ITlsHandshakeFeature.cs b/src/Servers/Connections.Abstractions/src/Features/ITlsHandshakeFeature.cs index ba9af785b67f..15a99e0efc32 100644 --- a/src/Servers/Connections.Abstractions/src/Features/ITlsHandshakeFeature.cs +++ b/src/Servers/Connections.Abstractions/src/Features/ITlsHandshakeFeature.cs @@ -24,6 +24,12 @@ public interface ITlsHandshakeFeature /// Gets the . /// TlsCipherSuite? NegotiatedCipherSuite => null; + + /// + /// Gets the host name from the "server_name" (SNI) extension of the client hello if present. + /// See RFC 6066. + /// + string? HostName => null; #endif /// diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt index 3e85ed9e89fc..39ed42614d79 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1,4 +1,7 @@ #nullable enable +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature.NotifyOnReconnect.get -> System.Action! +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature.NotifyOnReconnect.set -> void Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature.Tags.get -> System.Collections.Generic.ICollection>! Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 9f1d00bb09ad..c689504701db 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,8 +1,12 @@ #nullable enable +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature.NotifyOnReconnect.get -> System.Action! +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature.NotifyOnReconnect.set -> void Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature.Tags.get -> System.Collections.Generic.ICollection>! Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature.NamedPipe.get -> System.IO.Pipes.NamedPipeServerStream! +Microsoft.AspNetCore.Connections.Features.ITlsHandshakeFeature.HostName.get -> string? Microsoft.AspNetCore.Connections.Features.ITlsHandshakeFeature.NegotiatedCipherSuite.get -> System.Net.Security.TlsCipherSuite? Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector Microsoft.AspNetCore.Connections.IConnectionListenerFactorySelector.CanBind(System.Net.EndPoint! endpoint) -> bool diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 3e85ed9e89fc..39ed42614d79 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,4 +1,7 @@ #nullable enable +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature.NotifyOnReconnect.get -> System.Action! +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature.NotifyOnReconnect.set -> void Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature.Tags.get -> System.Collections.Generic.ICollection>! Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt index 3e85ed9e89fc..39ed42614d79 100644 --- a/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt @@ -1,4 +1,7 @@ #nullable enable +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature.NotifyOnReconnect.get -> System.Action! +Microsoft.AspNetCore.Connections.Abstractions.IReconnectFeature.NotifyOnReconnect.set -> void Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature Microsoft.AspNetCore.Connections.Features.IConnectionMetricsTagsFeature.Tags.get -> System.Collections.Generic.ICollection>! Microsoft.AspNetCore.Connections.Features.IConnectionNamedPipeFeature diff --git a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs index 1582c1217524..258d05ad5c87 100644 --- a/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs +++ b/src/Servers/HttpSys/src/NativeInterop/HttpApi.cs @@ -81,6 +81,10 @@ internal static unsafe partial uint HttpCreateRequestQueue(HTTPAPI_VERSION versi internal static unsafe partial uint HttpDelegateRequestEx(SafeHandle pReqQueueHandle, SafeHandle pDelegateQueueHandle, ulong requestId, ulong delegateUrlGroupId, ulong propertyInfoSetSize, HTTP_DELEGATE_REQUEST_PROPERTY_INFO* pRequestPropertyBuffer); + [LibraryImport(HTTPAPI, SetLastError = true)] + internal static unsafe partial uint HttpQueryRequestProperty(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, + void* qualifier, ulong qualifierSize, void* output, ulong outputSize, ulong* bytesReturned, IntPtr overlapped); + internal delegate uint HttpSetRequestPropertyInvoker(SafeHandle requestQueueHandle, ulong requestId, HTTP_REQUEST_PROPERTY propertyId, void* input, uint inputSize, IntPtr overlapped); private static HTTPAPI_VERSION version; diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 7c55263ffb2d..229b9bd8032f 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -327,6 +327,8 @@ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint internal WindowsPrincipal User { get; } + public string? SniHostName { get; private set; } + public SslProtocols Protocol { get; private set; } public CipherAlgorithmType CipherAlgorithm { get; private set; } @@ -428,6 +430,9 @@ private void GetTlsHandshakeResults() HashStrength = (int)handshake.HashStrength; KeyExchangeAlgorithm = handshake.KeyExchangeType; KeyExchangeStrength = (int)handshake.KeyExchangeStrength; + + var sni = RequestContext.GetClientSni(); + SniHostName = sni.Hostname; } public X509Certificate2? ClientCertificate diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs index a16281148ca6..95148c3070ed 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs @@ -587,6 +587,8 @@ bool IHttpBodyControlFeature.AllowSynchronousIO int ITlsHandshakeFeature.KeyExchangeStrength => Request.KeyExchangeStrength; + string? ITlsHandshakeFeature.HostName => Request.SniHostName; + IReadOnlyDictionary> IHttpSysRequestInfoFeature.RequestInfo => Request.RequestInfo; ReadOnlySpan IHttpSysRequestTimingFeature.Timestamps => Request.RequestTimestamps; diff --git a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs index 6493488a5b1f..5120ce969ffa 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/RequestContext.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Runtime.InteropServices; using System.Security.Authentication.ExtendedProtection; using System.Security.Principal; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpSys.Internal; using Microsoft.Extensions.Logging; +using static Microsoft.AspNetCore.HttpSys.Internal.UnsafeNclNativeMethods; namespace Microsoft.AspNetCore.Server.HttpSys; @@ -224,6 +226,31 @@ internal void ForceCancelRequest() } } + internal unsafe HttpApiTypes.HTTP_REQUEST_PROPERTY_SNI GetClientSni() + { + var buffer = new byte[HttpApiTypes.SniPropertySizeInBytes]; + fixed (byte* pBuffer = buffer) + { + var statusCode = HttpApi.HttpQueryRequestProperty( + Server.RequestQueue.Handle, + RequestId, + HttpApiTypes.HTTP_REQUEST_PROPERTY.HttpRequestPropertySni, + qualifier: null, + qualifierSize: 0, + (void*)pBuffer, + (ulong)buffer.Length, + bytesReturned: null, + IntPtr.Zero); + + if (statusCode == ErrorCodes.ERROR_SUCCESS) + { + return Marshal.PtrToStructure((IntPtr)pBuffer); + } + } + + return default; + } + // You must still call ForceCancelRequest after this. internal unsafe void SetResetCode(int errorCode) { diff --git a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs index fad737d8af9b..7b7586491e5a 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Data.Common; using System.Diagnostics; using System.IO; using System.Net.Http; @@ -185,6 +186,12 @@ public async Task Https_SetsITlsHandshakeFeature() var keyExchangeStrength = result.GetProperty("keyExchangeStrength").GetInt32(); Assert.True(keyExchangeStrength >= 0, "KeyExchangeStrength: " + keyExchangeStrength); + + if (Environment.OSVersion.Version > new Version(10, 0, 19043, 0)) + { + var hostName = result.GetProperty("hostName").ToString(); + Assert.Equal("localhost", hostName); + } } } diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs index 74c687c6f52d..1db8e7481343 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.Log.cs @@ -11,21 +11,21 @@ internal abstract partial class IISHttpContext private static partial class Log { [LoggerMessage(1, LogLevel.Debug, @"Connection ID ""{ConnectionId}"" disconnecting.", EventName = "ConnectionDisconnect")] - public static partial void ConnectionDisconnect(ILogger logger, string connectionId); + public static partial void ConnectionDisconnect(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(2, LogLevel.Error, @"Connection ID ""{ConnectionId}"", Request ID ""{TraceIdentifier}"": An unhandled exception was thrown by the application.", EventName = "ApplicationError")] - public static partial void ApplicationError(ILogger logger, string connectionId, string traceIdentifier, Exception ex); + public static partial void ApplicationError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier, Exception ex); [LoggerMessage(3, LogLevel.Error, @"Unexpected exception in ""{ClassName}.{MethodName}"".", EventName = "UnexpectedError")] public static partial void UnexpectedError(ILogger logger, string className, Exception ex, [CallerMemberName] string? methodName = null); - public static void ConnectionBadRequest(ILogger logger, string connectionId, Microsoft.AspNetCore.Http.BadHttpRequestException ex) + public static void ConnectionBadRequest(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Microsoft.AspNetCore.Http.BadHttpRequestException ex) => ConnectionBadRequest(logger, connectionId, ex.Message, ex); [LoggerMessage(4, LogLevel.Debug, @"Connection id ""{ConnectionId}"" bad request data: ""{message}""", EventName = nameof(ConnectionBadRequest))] - private static partial void ConnectionBadRequest(ILogger logger, string connectionId, string message, Microsoft.AspNetCore.Http.BadHttpRequestException ex); + private static partial void ConnectionBadRequest(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string message, Microsoft.AspNetCore.Http.BadHttpRequestException ex); [LoggerMessage(5, LogLevel.Debug, @"Connection ID ""{ConnectionId}"", Request ID ""{TraceIdentifier}"": The request was aborted by the client.", EventName = "RequestAborted")] - public static partial void RequestAborted(ILogger logger, string connectionId, string traceIdentifier); + public static partial void RequestAborted(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier); } } diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj b/src/Servers/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj index 9edc221a0646..16aeeb34a989 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj +++ b/src/Servers/IIS/IIS/test/testassets/InProcessNewShimWebSite/InProcessNewShimWebSite.csproj @@ -16,7 +16,7 @@ + Properties="SelfContained=true;RuntimeIdentifier=win-x64;ReferenceTestTasks=false" /> diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/InProcessWebSite.csproj b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/InProcessWebSite.csproj index be69b71e9217..4fdccefc70e8 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/InProcessWebSite.csproj +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/InProcessWebSite.csproj @@ -13,7 +13,7 @@ + Properties="SelfContained=true;RuntimeIdentifier=win-x64;ReferenceTestTasks=false" /> diff --git a/src/Servers/Kestrel/Core/src/Internal/ConnectionLogScope.cs b/src/Servers/Kestrel/Core/src/Internal/ConnectionLogScope.cs index 22094285b904..5db1ad4c770c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/ConnectionLogScope.cs +++ b/src/Servers/Kestrel/Core/src/Internal/ConnectionLogScope.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Globalization; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; @@ -12,7 +13,7 @@ internal sealed class ConnectionLogScope : IReadOnlyList memoryPool, KestrelTrace log, diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index e8fca0c394b3..3a0aad1f5ad6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Buffers.Binary; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using System.Net.Http.HPack; using System.Threading.Channels; @@ -58,7 +59,7 @@ public Http2FrameWriter( int maxStreamsPerConnection, ITimeoutControl timeoutControl, MinDataRate? minResponseDataRate, - string connectionId, + [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, MemoryPool memoryPool, ServiceContext serviceContext) { @@ -82,7 +83,7 @@ public Http2FrameWriter( // This is bounded by the maximum number of concurrent Http2Streams per Http2Connection. // This isn't the same as SETTINGS_MAX_CONCURRENT_STREAMS, but typically double (with a floor of 100) // which is the max number of Http2Streams that can end up in the Http2Connection._streams dictionary. - // + // // Setting a lower limit of SETTINGS_MAX_CONCURRENT_STREAMS might be sufficient because a stream shouldn't // be rescheduling itself after being completed or canceled, but we're going with the more conservative limit // in case there's some logic scheduling completed or canceled streams unnecessarily. diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamContext.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamContext.cs index 9537d30b55fa..872b1ecf0ea0 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamContext.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.Net; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; @@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; internal sealed class Http2StreamContext : HttpConnectionContext { public Http2StreamContext( - string connectionId, + [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, HttpProtocols protocols, AltSvcHeader? altSvcHeader, BaseConnectionContext connectionContext, diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs index dbcd774af4d6..e0552e001d44 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using System.Net.Http; using System.Net.Http.QPack; @@ -78,7 +79,7 @@ public Http3FrameWriter(ConnectionContext connectionContext, ITimeoutControl tim : (int)clientPeerSettings.MaxRequestHeaderFieldSectionSize; } - public void Reset(PipeWriter output, string connectionId) + public void Reset(PipeWriter output, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { _outputWriter = output; _flusher.Initialize(output); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamContext.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamContext.cs index 50c406b3d06c..f3e70b6b204e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamContext.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Net; +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; internal sealed class Http3StreamContext : HttpConnectionContext { public Http3StreamContext( - string connectionId, + [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, HttpProtocols protocols, AltSvcHeader? altSvcHeader, BaseConnectionContext connectionContext, diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs index 92c6ad1a0583..feb8d3034b38 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.IO.Pipelines; using System.Net; +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; internal class HttpConnectionContext : BaseHttpConnectionContext { public HttpConnectionContext( - string connectionId, + [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, HttpProtocols protocols, AltSvcHeader? altSvcHeader, BaseConnectionContext connectionContext, diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpMultiplexedConnectionContext.cs b/src/Servers/Kestrel/Core/src/Internal/HttpMultiplexedConnectionContext.cs index a579e0e8e3e1..727c6193c6be 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpMultiplexedConnectionContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpMultiplexedConnectionContext.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Net; +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; internal sealed class HttpMultiplexedConnectionContext : BaseHttpConnectionContext { public HttpMultiplexedConnectionContext( - string connectionId, + [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, HttpProtocols protocols, AltSvcHeader? altSvcHeader, MultiplexedConnectionContext connectionContext, diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs index a52720dd2a6f..2fdefba5ce2c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs @@ -70,7 +70,7 @@ public void ConnectionStart(BaseConnectionContext connection) [MethodImpl(MethodImplOptions.NoInlining)] [Event(1, Level = EventLevel.Informational)] - private void ConnectionStart(string connectionId, string? localEndPoint, string? remoteEndPoint) + private void ConnectionStart([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string? localEndPoint, string? remoteEndPoint) { WriteEvent(1, connectionId, localEndPoint, remoteEndPoint); } @@ -88,7 +88,7 @@ public void ConnectionStop(BaseConnectionContext connection) [MethodImpl(MethodImplOptions.NoInlining)] [Event(2, Level = EventLevel.Informational)] - private void ConnectionStop(string connectionId) + private void ConnectionStop([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { WriteEvent(2, connectionId); } @@ -113,7 +113,7 @@ void Core(HttpProtocol httpProtocol) } [Event(3, Level = EventLevel.Informational)] - private void RequestStart(string connectionId, string requestId, string httpVersion, string path, string method) + private void RequestStart([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string requestId, string httpVersion, string path, string method) { WriteEvent(3, connectionId, requestId, httpVersion, path, method); } @@ -138,14 +138,14 @@ void Core(HttpProtocol httpProtocol) } [Event(4, Level = EventLevel.Informational)] - private void RequestStop(string connectionId, string requestId, string httpVersion, string path, string method) + private void RequestStop([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string requestId, string httpVersion, string path, string method) { WriteEvent(4, connectionId, requestId, httpVersion, path, method); } [MethodImpl(MethodImplOptions.NoInlining)] [Event(5, Level = EventLevel.Informational)] - public void ConnectionRejected(string connectionId) + public void ConnectionRejected([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { if (IsEnabled(EventLevel.Informational, EventKeywords.None)) { @@ -179,7 +179,7 @@ public void TlsHandshakeStart(BaseConnectionContext connectionContext, SslServer [MethodImpl(MethodImplOptions.NoInlining)] [Event(8, Level = EventLevel.Informational)] - private void TlsHandshakeStart(string connectionId, string sslProtocols) + private void TlsHandshakeStart([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string sslProtocols) { WriteEvent(8, connectionId, sslProtocols); } @@ -202,14 +202,14 @@ public void TlsHandshakeStop(BaseConnectionContext connectionContext, TlsConnect [MethodImpl(MethodImplOptions.NoInlining)] [Event(9, Level = EventLevel.Informational)] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Parameters passed to WriteEvent are all primative values.")] - private void TlsHandshakeStop(string connectionId, string sslProtocols, string applicationProtocol, string hostName) + private void TlsHandshakeStop([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string sslProtocols, string applicationProtocol, string hostName) { WriteEvent(9, connectionId, sslProtocols, applicationProtocol, hostName); } [MethodImpl(MethodImplOptions.NoInlining)] [Event(10, Level = EventLevel.Error)] - public void TlsHandshakeFailed(string connectionId) + public void TlsHandshakeFailed([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { Interlocked.Increment(ref _failedTlsHandshakes); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs index 71102514ee60..8cfcc983b7b4 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Connections; -using Microsoft.Extensions.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics; using System.Diagnostics; using System.Diagnostics.Metrics; using System.Runtime.CompilerServices; @@ -27,7 +27,7 @@ internal sealed class KestrelMetrics public KestrelMetrics(IMeterFactory meterFactory) { - _meter = meterFactory.CreateMeter(MeterName); + _meter = meterFactory.Create(MeterName); _currentConnectionsCounter = _meter.CreateUpDownCounter( "current-connections", diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.BadRequests.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.BadRequests.cs index 3903a32c215a..be504983b831 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.BadRequests.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.BadRequests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.Extensions.Logging; @@ -8,27 +9,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; internal sealed partial class KestrelTrace : ILogger { - public void ConnectionBadRequest(string connectionId, AspNetCore.Http.BadHttpRequestException ex) + public void ConnectionBadRequest([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, AspNetCore.Http.BadHttpRequestException ex) { BadRequestsLog.ConnectionBadRequest(_badRequestsLogger, connectionId, ex.Message, ex); } - public void RequestProcessingError(string connectionId, Exception ex) + public void RequestProcessingError([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Exception ex) { BadRequestsLog.RequestProcessingError(_badRequestsLogger, connectionId, ex); } - public void RequestBodyMinimumDataRateNotSatisfied(string connectionId, string? traceIdentifier, double rate) + public void RequestBodyMinimumDataRateNotSatisfied([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string? traceIdentifier, double rate) { BadRequestsLog.RequestBodyMinimumDataRateNotSatisfied(_badRequestsLogger, connectionId, traceIdentifier, rate); } - public void ResponseMinimumDataRateNotSatisfied(string connectionId, string? traceIdentifier) + public void ResponseMinimumDataRateNotSatisfied([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string? traceIdentifier) { BadRequestsLog.ResponseMinimumDataRateNotSatisfied(_badRequestsLogger, connectionId, traceIdentifier); } - public void PossibleInvalidHttpVersionDetected(string connectionId, HttpVersion expectedHttpVersion, HttpVersion detectedHttpVersion) + public void PossibleInvalidHttpVersionDetected([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, HttpVersion expectedHttpVersion, HttpVersion detectedHttpVersion) { if (_generalLogger.IsEnabled(LogLevel.Debug)) { @@ -39,19 +40,19 @@ public void PossibleInvalidHttpVersionDetected(string connectionId, HttpVersion private static partial class BadRequestsLog { [LoggerMessage(17, LogLevel.Debug, @"Connection id ""{ConnectionId}"" bad request data: ""{message}""", EventName = "ConnectionBadRequest")] - public static partial void ConnectionBadRequest(ILogger logger, string connectionId, string message, Microsoft.AspNetCore.Http.BadHttpRequestException ex); + public static partial void ConnectionBadRequest(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string message, Microsoft.AspNetCore.Http.BadHttpRequestException ex); [LoggerMessage(20, LogLevel.Debug, @"Connection id ""{ConnectionId}"" request processing ended abnormally.", EventName = "RequestProcessingError")] - public static partial void RequestProcessingError(ILogger logger, string connectionId, Exception ex); + public static partial void RequestProcessingError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Exception ex); [LoggerMessage(27, LogLevel.Debug, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the request timed out because it was not sent by the client at a minimum of {Rate} bytes/second.", EventName = "RequestBodyMinimumDataRateNotSatisfied")] - public static partial void RequestBodyMinimumDataRateNotSatisfied(ILogger logger, string connectionId, string? traceIdentifier, double rate); + public static partial void RequestBodyMinimumDataRateNotSatisfied(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string? traceIdentifier, double rate); [LoggerMessage(28, LogLevel.Debug, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the connection was closed because the response was not read by the client at the specified minimum data rate.", EventName = "ResponseMinimumDataRateNotSatisfied")] - public static partial void ResponseMinimumDataRateNotSatisfied(ILogger logger, string connectionId, string? traceIdentifier); + public static partial void ResponseMinimumDataRateNotSatisfied(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string? traceIdentifier); [LoggerMessage(54, LogLevel.Debug, @"Connection id ""{ConnectionId}"": Invalid content received on connection. Possible incorrect HTTP version detected. Expected {ExpectedHttpVersion} but received {DetectedHttpVersion}.", EventName = "PossibleInvalidHttpVersionDetected", SkipEnabledCheck = true)] - public static partial void PossibleInvalidHttpVersionDetected(ILogger logger, string connectionId, string expectedHttpVersion, string detectedHttpVersion); + public static partial void PossibleInvalidHttpVersionDetected(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string expectedHttpVersion, string detectedHttpVersion); // Highest shared ID is 63. New consecutive IDs start at 64 } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Connections.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Connections.cs index 1d1ab4c3598a..a1da080a30e5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Connections.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Connections.cs @@ -2,37 +2,38 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; internal sealed partial class KestrelTrace : ILogger { - public void ConnectionStart(string connectionId) + public void ConnectionStart([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { ConnectionsLog.ConnectionStart(_connectionsLogger, connectionId); } - public void ConnectionStop(string connectionId) + public void ConnectionStop([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { ConnectionsLog.ConnectionStop(_connectionsLogger, connectionId); } - public void ConnectionPause(string connectionId) + public void ConnectionPause([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { ConnectionsLog.ConnectionPause(_connectionsLogger, connectionId); } - public void ConnectionResume(string connectionId) + public void ConnectionResume([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { ConnectionsLog.ConnectionResume(_connectionsLogger, connectionId); } - public void ConnectionKeepAlive(string connectionId) + public void ConnectionKeepAlive([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { ConnectionsLog.ConnectionKeepAlive(_connectionsLogger, connectionId); } - public void ConnectionDisconnect(string connectionId) + public void ConnectionDisconnect([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { ConnectionsLog.ConnectionDisconnect(_connectionsLogger, connectionId); } @@ -47,17 +48,17 @@ public void NotAllConnectionsAborted() ConnectionsLog.NotAllConnectionsAborted(_connectionsLogger); } - public void ConnectionRejected(string connectionId) + public void ConnectionRejected([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { ConnectionsLog.ConnectionRejected(_connectionsLogger, connectionId); } - public void ApplicationAbortedConnection(string connectionId, string traceIdentifier) + public void ApplicationAbortedConnection([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier) { ConnectionsLog.ApplicationAbortedConnection(_connectionsLogger, connectionId, traceIdentifier); } - public void ConnectionAccepted(string connectionId) + public void ConnectionAccepted([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { ConnectionsLog.ConnectionAccepted(_connectionsLogger, connectionId); } @@ -65,22 +66,22 @@ public void ConnectionAccepted(string connectionId) private static partial class ConnectionsLog { [LoggerMessage(1, LogLevel.Debug, @"Connection id ""{ConnectionId}"" started.", EventName = "ConnectionStart")] - public static partial void ConnectionStart(ILogger logger, string connectionId); + public static partial void ConnectionStart(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(2, LogLevel.Debug, @"Connection id ""{ConnectionId}"" stopped.", EventName = "ConnectionStop")] - public static partial void ConnectionStop(ILogger logger, string connectionId); + public static partial void ConnectionStop(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(4, LogLevel.Debug, @"Connection id ""{ConnectionId}"" paused.", EventName = "ConnectionPause")] - public static partial void ConnectionPause(ILogger logger, string connectionId); + public static partial void ConnectionPause(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(5, LogLevel.Debug, @"Connection id ""{ConnectionId}"" resumed.", EventName = "ConnectionResume")] - public static partial void ConnectionResume(ILogger logger, string connectionId); + public static partial void ConnectionResume(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(9, LogLevel.Debug, @"Connection id ""{ConnectionId}"" completed keep alive response.", EventName = "ConnectionKeepAlive")] - public static partial void ConnectionKeepAlive(ILogger logger, string connectionId); + public static partial void ConnectionKeepAlive(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(10, LogLevel.Debug, @"Connection id ""{ConnectionId}"" disconnecting.", EventName = "ConnectionDisconnect")] - public static partial void ConnectionDisconnect(ILogger logger, string connectionId); + public static partial void ConnectionDisconnect(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(16, LogLevel.Debug, "Some connections failed to close gracefully during server shutdown.", EventName = "NotAllConnectionsClosedGracefully")] public static partial void NotAllConnectionsClosedGracefully(ILogger logger); @@ -89,13 +90,13 @@ private static partial class ConnectionsLog public static partial void NotAllConnectionsAborted(ILogger logger); [LoggerMessage(24, LogLevel.Warning, @"Connection id ""{ConnectionId}"" rejected because the maximum number of concurrent connections has been reached.", EventName = "ConnectionRejected")] - public static partial void ConnectionRejected(ILogger logger, string connectionId); + public static partial void ConnectionRejected(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(34, LogLevel.Information, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the application aborted the connection.", EventName = "ApplicationAbortedConnection")] - public static partial void ApplicationAbortedConnection(ILogger logger, string connectionId, string traceIdentifier); + public static partial void ApplicationAbortedConnection(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier); [LoggerMessage(39, LogLevel.Debug, @"Connection id ""{ConnectionId}"" accepted.", EventName = "ConnectionAccepted")] - public static partial void ConnectionAccepted(ILogger logger, string connectionId); + public static partial void ConnectionAccepted(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); // Highest shared ID is 63. New consecutive IDs start at 64 } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.General.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.General.cs index 689459c9ec99..960a28561041 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.General.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.General.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Net; using Microsoft.Extensions.Logging; @@ -8,12 +9,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; internal sealed partial class KestrelTrace : ILogger { - public void ApplicationError(string connectionId, string traceIdentifier, Exception ex) + public void ApplicationError([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier, Exception ex) { GeneralLog.ApplicationError(_generalLogger, connectionId, traceIdentifier, ex); } - public void ConnectionHeadResponseBodyWrite(string connectionId, long count) + public void ConnectionHeadResponseBodyWrite([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long count) { GeneralLog.ConnectionHeadResponseBodyWrite(_generalLogger, connectionId, count); } @@ -24,27 +25,27 @@ public void HeartbeatSlow(TimeSpan heartbeatDuration, TimeSpan interval, DateTim GeneralLog.HeartbeatSlow(_generalLogger, now, heartbeatDuration, interval); } - public void ApplicationNeverCompleted(string connectionId) + public void ApplicationNeverCompleted([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { GeneralLog.ApplicationNeverCompleted(_generalLogger, connectionId); } - public void RequestBodyStart(string connectionId, string traceIdentifier) + public void RequestBodyStart([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier) { GeneralLog.RequestBodyStart(_generalLogger, connectionId, traceIdentifier); } - public void RequestBodyDone(string connectionId, string traceIdentifier) + public void RequestBodyDone([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier) { GeneralLog.RequestBodyDone(_generalLogger, connectionId, traceIdentifier); } - public void RequestBodyNotEntirelyRead(string connectionId, string traceIdentifier) + public void RequestBodyNotEntirelyRead([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier) { GeneralLog.RequestBodyNotEntirelyRead(_generalLogger, connectionId, traceIdentifier); } - public void RequestBodyDrainTimedOut(string connectionId, string traceIdentifier) + public void RequestBodyDrainTimedOut([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier) { GeneralLog.RequestBodyDrainTimedOut(_generalLogger, connectionId, traceIdentifier); } @@ -64,7 +65,7 @@ public void Http3DisabledWithHttp1AndNoTls(EndPoint endPoint) GeneralLog.Http3DisabledWithHttp1AndNoTls(_generalLogger, endPoint); } - public void RequestAborted(string connectionId, string traceIdentifier) + public void RequestAborted([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier) { GeneralLog.RequestAbortedException(_generalLogger, connectionId, traceIdentifier); } @@ -72,28 +73,28 @@ public void RequestAborted(string connectionId, string traceIdentifier) private static partial class GeneralLog { [LoggerMessage(13, LogLevel.Error, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": An unhandled exception was thrown by the application.", EventName = "ApplicationError")] - public static partial void ApplicationError(ILogger logger, string connectionId, string traceIdentifier, Exception ex); + public static partial void ApplicationError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier, Exception ex); [LoggerMessage(18, LogLevel.Debug, @"Connection id ""{ConnectionId}"" write of ""{count}"" body bytes to non-body HEAD response.", EventName = "ConnectionHeadResponseBodyWrite")] - public static partial void ConnectionHeadResponseBodyWrite(ILogger logger, string connectionId, long count); + public static partial void ConnectionHeadResponseBodyWrite(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long count); [LoggerMessage(22, LogLevel.Warning, @"As of ""{now}"", the heartbeat has been running for ""{heartbeatDuration}"" which is longer than ""{interval}"". This could be caused by thread pool starvation.", EventName = "HeartbeatSlow")] public static partial void HeartbeatSlow(ILogger logger, DateTimeOffset now, TimeSpan heartbeatDuration, TimeSpan interval); [LoggerMessage(23, LogLevel.Critical, @"Connection id ""{ConnectionId}"" application never completed.", EventName = "ApplicationNeverCompleted")] - public static partial void ApplicationNeverCompleted(ILogger logger, string connectionId); + public static partial void ApplicationNeverCompleted(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(25, LogLevel.Debug, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": started reading request body.", EventName = "RequestBodyStart", SkipEnabledCheck = true)] - public static partial void RequestBodyStart(ILogger logger, string connectionId, string traceIdentifier); + public static partial void RequestBodyStart(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier); [LoggerMessage(26, LogLevel.Debug, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": done reading request body.", EventName = "RequestBodyDone", SkipEnabledCheck = true)] - public static partial void RequestBodyDone(ILogger logger, string connectionId, string traceIdentifier); + public static partial void RequestBodyDone(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier); [LoggerMessage(32, LogLevel.Information, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the application completed without reading the entire request body.", EventName = "RequestBodyNotEntirelyRead")] - public static partial void RequestBodyNotEntirelyRead(ILogger logger, string connectionId, string traceIdentifier); + public static partial void RequestBodyNotEntirelyRead(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier); [LoggerMessage(33, LogLevel.Information, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": automatic draining of the request body timed out after taking over 5 seconds.", EventName = "RequestBodyDrainTimedOut")] - public static partial void RequestBodyDrainTimedOut(ILogger logger, string connectionId, string traceIdentifier); + public static partial void RequestBodyDrainTimedOut(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier); [LoggerMessage(41, LogLevel.Warning, "One or more of the following response headers have been removed because they are invalid for HTTP/2 and HTTP/3 responses: 'Connection', 'Transfer-Encoding', 'Keep-Alive', 'Upgrade' and 'Proxy-Connection'.", EventName = "InvalidResponseHeaderRemoved")] public static partial void InvalidResponseHeaderRemoved(ILogger logger); @@ -105,7 +106,7 @@ private static partial class GeneralLog public static partial void Http3DisabledWithHttp1AndNoTls(ILogger logger, EndPoint endPoint); [LoggerMessage(66, LogLevel.Debug, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": The request was aborted by the client.", EventName = "RequestAborted")] - public static partial void RequestAbortedException(ILogger logger, string connectionId, string traceIdentifier); + public static partial void RequestAbortedException(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string traceIdentifier); // Highest shared ID is 66. New consecutive IDs start at 67 } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http2.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http2.cs index 84a7609f0710..5bf02849a9a6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http2.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http2.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.Extensions.Logging; @@ -9,12 +10,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; internal sealed partial class KestrelTrace : ILogger { - public void Http2ConnectionError(string connectionId, Http2ConnectionErrorException ex) + public void Http2ConnectionError([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Http2ConnectionErrorException ex) { Http2Log.Http2ConnectionError(_http2Logger, connectionId, ex); } - public void Http2StreamError(string connectionId, Http2StreamErrorException ex) + public void Http2StreamError([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Http2StreamErrorException ex) { Http2Log.Http2StreamError(_http2Logger, connectionId, ex); } @@ -29,12 +30,12 @@ public void Http2StreamResetAbort(string traceIdentifier, Http2ErrorCode error, Http2Log.Http2StreamResetAbort(_http2Logger, traceIdentifier, error, abortReason); } - public void Http2ConnectionClosing(string connectionId) + public void Http2ConnectionClosing([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { Http2Log.Http2ConnectionClosing(_http2Logger, connectionId); } - public void Http2FrameReceived(string connectionId, Http2Frame frame) + public void Http2FrameReceived([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Http2Frame frame) { if (_http2Logger.IsEnabled(LogLevel.Trace)) { @@ -42,22 +43,22 @@ public void Http2FrameReceived(string connectionId, Http2Frame frame) } } - public void HPackEncodingError(string connectionId, int streamId, Exception ex) + public void HPackEncodingError([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, int streamId, Exception ex) { Http2Log.HPackEncodingError(_http2Logger, connectionId, streamId, ex); } - public void Http2MaxConcurrentStreamsReached(string connectionId) + public void Http2MaxConcurrentStreamsReached([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { Http2Log.Http2MaxConcurrentStreamsReached(_http2Logger, connectionId); } - public void Http2ConnectionClosed(string connectionId, int highestOpenedStreamId) + public void Http2ConnectionClosed([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, int highestOpenedStreamId) { Http2Log.Http2ConnectionClosed(_http2Logger, connectionId, highestOpenedStreamId); } - public void Http2FrameSending(string connectionId, Http2Frame frame) + public void Http2FrameSending([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Http2Frame frame) { if (_http2Logger.IsEnabled(LogLevel.Trace)) { @@ -65,22 +66,22 @@ public void Http2FrameSending(string connectionId, Http2Frame frame) } } - public void Http2QueueOperationsExceeded(string connectionId, ConnectionAbortedException ex) + public void Http2QueueOperationsExceeded([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, ConnectionAbortedException ex) { Http2Log.Http2QueueOperationsExceeded(_http2Logger, connectionId, ex); } - public void Http2UnexpectedDataRemaining(int streamId, string connectionId) + public void Http2UnexpectedDataRemaining(int streamId, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { Http2Log.Http2UnexpectedDataRemaining(_http2Logger, streamId, connectionId); } - public void Http2ConnectionQueueProcessingCompleted(string connectionId) + public void Http2ConnectionQueueProcessingCompleted([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { Http2Log.Http2ConnectionQueueProcessingCompleted(_http2Logger, connectionId); } - public void Http2UnexpectedConnectionQueueError(string connectionId, Exception ex) + public void Http2UnexpectedConnectionQueueError([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Exception ex) { Http2Log.Http2UnexpectedConnectionQueueError(_http2Logger, connectionId, ex); } @@ -88,46 +89,46 @@ public void Http2UnexpectedConnectionQueueError(string connectionId, Exception e private static partial class Http2Log { [LoggerMessage(29, LogLevel.Debug, @"Connection id ""{ConnectionId}"": HTTP/2 connection error.", EventName = "Http2ConnectionError")] - public static partial void Http2ConnectionError(ILogger logger, string connectionId, Http2ConnectionErrorException ex); + public static partial void Http2ConnectionError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Http2ConnectionErrorException ex); [LoggerMessage(30, LogLevel.Debug, @"Connection id ""{ConnectionId}"": HTTP/2 stream error.", EventName = "Http2StreamError")] - public static partial void Http2StreamError(ILogger logger, string connectionId, Http2StreamErrorException ex); + public static partial void Http2StreamError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Http2StreamErrorException ex); [LoggerMessage(31, LogLevel.Debug, @"Connection id ""{ConnectionId}"": HPACK decoding error while decoding headers for stream ID {StreamId}.", EventName = "HPackDecodingError")] - public static partial void HPackDecodingError(ILogger logger, string connectionId, int streamId, Exception ex); + public static partial void HPackDecodingError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, int streamId, Exception ex); [LoggerMessage(35, LogLevel.Debug, @"Trace id ""{TraceIdentifier}"": HTTP/2 stream error ""{error}"". A Reset is being sent to the stream.", EventName = "Http2StreamResetAbort")] public static partial void Http2StreamResetAbort(ILogger logger, string traceIdentifier, Http2ErrorCode error, ConnectionAbortedException abortReason); [LoggerMessage(36, LogLevel.Debug, @"Connection id ""{ConnectionId}"" is closing.", EventName = "Http2ConnectionClosing")] - public static partial void Http2ConnectionClosing(ILogger logger, string connectionId); + public static partial void Http2ConnectionClosing(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(37, LogLevel.Trace, @"Connection id ""{ConnectionId}"" received {type} frame for stream ID {id} with length {length} and flags {flags}.", EventName = "Http2FrameReceived", SkipEnabledCheck = true)] - public static partial void Http2FrameReceived(ILogger logger, string connectionId, Http2FrameType type, int id, int length, object flags); + public static partial void Http2FrameReceived(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Http2FrameType type, int id, int length, object flags); [LoggerMessage(38, LogLevel.Information, @"Connection id ""{ConnectionId}"": HPACK encoding error while encoding headers for stream ID {StreamId}.", EventName = "HPackEncodingError")] - public static partial void HPackEncodingError(ILogger logger, string connectionId, int streamId, Exception ex); + public static partial void HPackEncodingError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, int streamId, Exception ex); [LoggerMessage(40, LogLevel.Debug, @"Connection id ""{ConnectionId}"" reached the maximum number of concurrent HTTP/2 streams allowed.", EventName = "Http2MaxConcurrentStreamsReached")] - public static partial void Http2MaxConcurrentStreamsReached(ILogger logger, string connectionId); + public static partial void Http2MaxConcurrentStreamsReached(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(48, LogLevel.Debug, @"Connection id ""{ConnectionId}"" is closed. The last processed stream ID was {HighestOpenedStreamId}.", EventName = "Http2ConnectionClosed")] - public static partial void Http2ConnectionClosed(ILogger logger, string connectionId, int highestOpenedStreamId); + public static partial void Http2ConnectionClosed(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, int highestOpenedStreamId); [LoggerMessage(49, LogLevel.Trace, @"Connection id ""{ConnectionId}"" sending {type} frame for stream ID {id} with length {length} and flags {flags}.", EventName = "Http2FrameSending", SkipEnabledCheck = true)] - public static partial void Http2FrameSending(ILogger logger, string connectionId, Http2FrameType type, int id, int length, object flags); + public static partial void Http2FrameSending(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Http2FrameType type, int id, int length, object flags); [LoggerMessage(60, LogLevel.Critical, @"Connection id ""{ConnectionId}"" exceeded the output operations maximum queue size.", EventName = "Http2QueueOperationsExceeded")] - public static partial void Http2QueueOperationsExceeded(ILogger logger, string connectionId, ConnectionAbortedException ex); + public static partial void Http2QueueOperationsExceeded(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, ConnectionAbortedException ex); [LoggerMessage(61, LogLevel.Critical, @"Stream {StreamId} on connection id ""{ConnectionId}"" observed an unexpected state where the streams output ended with data still remaining in the pipe.", EventName = "Http2UnexpectedDataRemaining")] - public static partial void Http2UnexpectedDataRemaining(ILogger logger, int streamId, string connectionId); + public static partial void Http2UnexpectedDataRemaining(ILogger logger, int streamId, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(62, LogLevel.Debug, @"The connection queue processing loop for {ConnectionId} completed.", EventName = "Http2ConnectionQueueProcessingCompleted")] - public static partial void Http2ConnectionQueueProcessingCompleted(ILogger logger, string connectionId); + public static partial void Http2ConnectionQueueProcessingCompleted(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(63, LogLevel.Critical, @"The event loop in connection {ConnectionId} failed unexpectedly.", EventName = "Http2UnexpectedConnectionQueueError")] - public static partial void Http2UnexpectedConnectionQueueError(ILogger logger, string connectionId, Exception ex); + public static partial void Http2UnexpectedConnectionQueueError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Exception ex); // Highest shared ID is 63. New consecutive IDs start at 64 } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs index cca9dbd0eb3e..19b8e0a13e13 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.Http3.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; @@ -10,17 +11,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; internal sealed partial class KestrelTrace : ILogger { - public void Http3ConnectionError(string connectionId, Http3ConnectionErrorException ex) + public void Http3ConnectionError([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Http3ConnectionErrorException ex) { Http3Log.Http3ConnectionError(_http3Logger, connectionId, ex); } - public void Http3ConnectionClosing(string connectionId) + public void Http3ConnectionClosing([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { Http3Log.Http3ConnectionClosing(_http3Logger, connectionId); } - public void Http3ConnectionClosed(string connectionId, long? highestOpenedStreamId) + public void Http3ConnectionClosed([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long? highestOpenedStreamId) { Http3Log.Http3ConnectionClosed(_http3Logger, connectionId, highestOpenedStreamId); } @@ -33,7 +34,7 @@ public void Http3StreamAbort(string traceIdentifier, Http3ErrorCode error, Conne } } - public void Http3FrameReceived(string connectionId, long streamId, Http3RawFrame frame) + public void Http3FrameReceived([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long streamId, Http3RawFrame frame) { if (_http3Logger.IsEnabled(LogLevel.Trace)) { @@ -41,7 +42,7 @@ public void Http3FrameReceived(string connectionId, long streamId, Http3RawFrame } } - public void Http3FrameSending(string connectionId, long streamId, Http3RawFrame frame) + public void Http3FrameSending([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long streamId, Http3RawFrame frame) { if (_http3Logger.IsEnabled(LogLevel.Trace)) { @@ -49,22 +50,22 @@ public void Http3FrameSending(string connectionId, long streamId, Http3RawFrame } } - public void Http3OutboundControlStreamError(string connectionId, Exception ex) + public void Http3OutboundControlStreamError([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Exception ex) { Http3Log.Http3OutboundControlStreamError(_http3Logger, connectionId, ex); } - public void QPackDecodingError(string connectionId, long streamId, Exception ex) + public void QPackDecodingError([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long streamId, Exception ex) { Http3Log.QPackDecodingError(_http3Logger, connectionId, streamId, ex); } - public void QPackEncodingError(string connectionId, long streamId, Exception ex) + public void QPackEncodingError([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long streamId, Exception ex) { Http3Log.QPackEncodingError(_http3Logger, connectionId, streamId, ex); } - public void Http3GoAwayStreamId(string connectionId, long goAwayStreamId) + public void Http3GoAwayStreamId([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long goAwayStreamId) { Http3Log.Http3GoAwayStreamId(_http3Logger, connectionId, goAwayStreamId); } @@ -72,34 +73,34 @@ public void Http3GoAwayStreamId(string connectionId, long goAwayStreamId) private static partial class Http3Log { [LoggerMessage(42, LogLevel.Debug, @"Connection id ""{ConnectionId}"": HTTP/3 connection error.", EventName = "Http3ConnectionError")] - public static partial void Http3ConnectionError(ILogger logger, string connectionId, Http3ConnectionErrorException ex); + public static partial void Http3ConnectionError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Http3ConnectionErrorException ex); [LoggerMessage(43, LogLevel.Debug, @"Connection id ""{ConnectionId}"" is closing.", EventName = "Http3ConnectionClosing")] - public static partial void Http3ConnectionClosing(ILogger logger, string connectionId); + public static partial void Http3ConnectionClosing(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(44, LogLevel.Debug, @"Connection id ""{ConnectionId}"" is closed. The last processed stream ID was {HighestOpenedStreamId}.", EventName = "Http3ConnectionClosed")] - public static partial void Http3ConnectionClosed(ILogger logger, string connectionId, long? highestOpenedStreamId); + public static partial void Http3ConnectionClosed(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long? highestOpenedStreamId); [LoggerMessage(45, LogLevel.Debug, @"Trace id ""{TraceIdentifier}"": HTTP/3 stream error ""{error}"". An abort is being sent to the stream.", EventName = "Http3StreamAbort", SkipEnabledCheck = true)] public static partial void Http3StreamAbort(ILogger logger, string traceIdentifier, string error, ConnectionAbortedException abortReason); [LoggerMessage(46, LogLevel.Trace, @"Connection id ""{ConnectionId}"" received {type} frame for stream ID {id} with length {length}.", EventName = "Http3FrameReceived", SkipEnabledCheck = true)] - public static partial void Http3FrameReceived(ILogger logger, string connectionId, string type, long id, long length); + public static partial void Http3FrameReceived(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string type, long id, long length); [LoggerMessage(47, LogLevel.Trace, @"Connection id ""{ConnectionId}"" sending {type} frame for stream ID {id} with length {length}.", EventName = "Http3FrameSending", SkipEnabledCheck = true)] - public static partial void Http3FrameSending(ILogger logger, string connectionId, string type, long id, long length); + public static partial void Http3FrameSending(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string type, long id, long length); [LoggerMessage(50, LogLevel.Debug, @"Connection id ""{ConnectionId}"": Unexpected error when initializing outbound control stream.", EventName = "Http3OutboundControlStreamError")] - public static partial void Http3OutboundControlStreamError(ILogger logger, string connectionId, Exception ex); + public static partial void Http3OutboundControlStreamError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Exception ex); [LoggerMessage(51, LogLevel.Debug, @"Connection id ""{ConnectionId}"": QPACK decoding error while decoding headers for stream ID {StreamId}.", EventName = "QPackDecodingError")] - public static partial void QPackDecodingError(ILogger logger, string connectionId, long streamId, Exception ex); + public static partial void QPackDecodingError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long streamId, Exception ex); [LoggerMessage(52, LogLevel.Information, @"Connection id ""{ConnectionId}"": QPACK encoding error while encoding headers for stream ID {StreamId}.", EventName = "QPackEncodingError")] - public static partial void QPackEncodingError(ILogger logger, string connectionId, long streamId, Exception ex); + public static partial void QPackEncodingError(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long streamId, Exception ex); [LoggerMessage(53, LogLevel.Debug, @"Connection id ""{ConnectionId}"": GOAWAY stream ID {GoAwayStreamId}.", EventName = "Http3GoAwayHighestOpenedStreamId")] - public static partial void Http3GoAwayStreamId(ILogger logger, string connectionId, long goAwayStreamId); + public static partial void Http3GoAwayStreamId(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long goAwayStreamId); // Highest shared ID is 63. New consecutive IDs start at 64 } diff --git a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs index d74a7266a3fc..53c24c80677b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs +++ b/src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs @@ -42,7 +42,6 @@ public X509Certificate2? ClientCertificate } } - // Used for event source, not part of any of the feature interfaces. public string? HostName { get; set; } public ReadOnlyMemory ApplicationProtocol => _sslStream.NegotiatedApplicationProtocol.Protocol; diff --git a/src/Servers/Kestrel/Core/src/KestrelServer.cs b/src/Servers/Kestrel/Core/src/KestrelServer.cs index cd8284a87304..54fc492304f8 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServer.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServer.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.Kestrel.Core; @@ -71,9 +71,9 @@ public void Dispose() // This factory used when type is created without DI. For example, via KestrelServer. private sealed class DummyMeterFactory : IMeterFactory { - public Meter CreateMeter(string name) => new Meter(name); + public Meter Create(MeterOptions options) => new Meter(options); - public Meter CreateMeter(MeterOptions options) => new Meter(options.Name, options.Version); + public void Dispose() { } } private sealed class SimpleHttpsConfigurationService : IHttpsConfigurationService diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index e4b328c02a61..cb48c665434f 100644 --- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -20,6 +20,8 @@ + + @@ -37,6 +39,7 @@ + diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index 25a23ee3f491..e365e8c57bfa 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using System.Net.Security; using System.Security; @@ -577,7 +578,7 @@ internal static partial class HttpsConnectionMiddlewareLoggerExtensions public static partial void AuthenticationTimedOut(this ILogger logger); [LoggerMessage(3, LogLevel.Debug, "Connection {ConnectionId} established using the following protocol: {Protocol}", EventName = "HttpsConnectionEstablished")] - public static partial void HttpsConnectionEstablished(this ILogger logger, string connectionId, SslProtocols protocol); + public static partial void HttpsConnectionEstablished(this ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, SslProtocols protocol); [LoggerMessage(4, LogLevel.Information, "HTTP/2 over TLS is not supported on Windows versions older than Windows 10 and Windows Server 2016 due to incompatible ciphers or missing ALPN support. Falling back to HTTP/1.1 instead.", EventName = "Http2DefaultCiphersInsufficient")] diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index 39eea7cbb2cc..e13e3717688e 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -16,7 +16,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Metrics; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; diff --git a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj index 1bc7490209d6..34d9d34335dc 100644 --- a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj +++ b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs index 1979300aa144..83777bc0e2aa 100644 --- a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs @@ -13,7 +13,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Metrics; using Microsoft.Extensions.Primitives; using Moq; diff --git a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj index 99c9f591ee43..ea39d1e46d6f 100644 --- a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj +++ b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs index 2968dcd50558..cb3f09432d85 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeConnectionListener.cs @@ -27,7 +27,7 @@ internal sealed class NamedPipeConnectionListener : IConnectionListener private readonly MemoryPool _memoryPool; private readonly PipeOptions _inputOptions; private readonly PipeOptions _outputOptions; - private readonly Mutex _mutex; + private readonly NamedPipeServerStreamPoolPolicy _poolPolicy; private Task? _completeListeningTask; private int _disposed; @@ -35,17 +35,16 @@ public NamedPipeConnectionListener( NamedPipeEndPoint endpoint, NamedPipeTransportOptions options, ILoggerFactory loggerFactory, - ObjectPoolProvider objectPoolProvider, - Mutex mutex) + ObjectPoolProvider objectPoolProvider) { _log = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes"); _endpoint = endpoint; _options = options; - _mutex = mutex; _memoryPool = options.MemoryPoolFactory(); _listeningToken = _listeningTokenSource.Token; // Have to create the pool here (instead of DI) because the pool is specific to an endpoint. - _namedPipeServerStreamPool = objectPoolProvider.Create(new NamedPipeServerStreamPoolPolicy(endpoint, options)); + _poolPolicy = new NamedPipeServerStreamPoolPolicy(endpoint, options); + _namedPipeServerStreamPool = objectPoolProvider.Create(_poolPolicy); // The OS maintains a backlog of clients that are waiting to connect, so the app queue only stores a single connection. // We want to have a queue plus a background task that populates the queue, rather than creating NamedPipeServerStream @@ -77,6 +76,7 @@ public void Start() { // Start first stream inline to catch creation errors. var initialStream = _namedPipeServerStreamPool.Get(); + _poolPolicy.SetFirstPipeStarted(); listeningTasks[i] = Task.Run(() => StartAsync(initialStream)); } @@ -170,7 +170,6 @@ public async ValueTask DisposeAsync() } _listeningTokenSource.Dispose(); - _mutex.Dispose(); if (_completeListeningTask != null) { await _completeListeningTask; @@ -185,6 +184,7 @@ private sealed class NamedPipeServerStreamPoolPolicy : IPooledObjectPolicy !obj.IsConnected; + + public void SetFirstPipeStarted() + { + _hasFirstPipeStarted = true; + } } } diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeLog.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeLog.cs index 1e50f7eccdf6..9ec30b5f2cd4 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeLog.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeLog.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Connections; using Microsoft.Extensions.Logging; @@ -9,7 +10,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Internal; internal static partial class NamedPipeLog { [LoggerMessage(1, LogLevel.Debug, @"Connection id ""{ConnectionId}"" accepted.", EventName = "AcceptedConnection", SkipEnabledCheck = true)] - private static partial void AcceptedConnectionCore(ILogger logger, string connectionId); + private static partial void AcceptedConnectionCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void AcceptedConnection(ILogger logger, BaseConnectionContext connection) { @@ -20,7 +21,7 @@ public static void AcceptedConnection(ILogger logger, BaseConnectionContext conn } [LoggerMessage(2, LogLevel.Debug, @"Connection id ""{ConnectionId}"" unexpected error.", EventName = "ConnectionError", SkipEnabledCheck = true)] - private static partial void ConnectionErrorCore(ILogger logger, string connectionId, Exception ex); + private static partial void ConnectionErrorCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Exception ex); public static void ConnectionError(ILogger logger, BaseConnectionContext connection, Exception ex) { @@ -34,7 +35,7 @@ public static void ConnectionError(ILogger logger, BaseConnectionContext connect public static partial void ConnectionListenerAborted(ILogger logger, Exception exception); [LoggerMessage(4, LogLevel.Debug, @"Connection id ""{ConnectionId}"" paused.", EventName = "ConnectionPause", SkipEnabledCheck = true)] - private static partial void ConnectionPauseCore(ILogger logger, string connectionId); + private static partial void ConnectionPauseCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void ConnectionPause(ILogger logger, NamedPipeConnection connection) { @@ -45,7 +46,7 @@ public static void ConnectionPause(ILogger logger, NamedPipeConnection connectio } [LoggerMessage(5, LogLevel.Debug, @"Connection id ""{ConnectionId}"" resumed.", EventName = "ConnectionResume", SkipEnabledCheck = true)] - private static partial void ConnectionResumeCore(ILogger logger, string connectionId); + private static partial void ConnectionResumeCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void ConnectionResume(ILogger logger, NamedPipeConnection connection) { @@ -56,7 +57,7 @@ public static void ConnectionResume(ILogger logger, NamedPipeConnection connecti } [LoggerMessage(6, LogLevel.Debug, @"Connection id ""{ConnectionId}"" received end of stream.", EventName = "ConnectionReadEnd", SkipEnabledCheck = true)] - private static partial void ConnectionReadEndCore(ILogger logger, string connectionId); + private static partial void ConnectionReadEndCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void ConnectionReadEnd(ILogger logger, NamedPipeConnection connection) { @@ -67,7 +68,7 @@ public static void ConnectionReadEnd(ILogger logger, NamedPipeConnection connect } [LoggerMessage(7, LogLevel.Debug, @"Connection id ""{ConnectionId}"" disconnecting stream because: ""{Reason}""", EventName = "ConnectionDisconnect", SkipEnabledCheck = true)] - private static partial void ConnectionDisconnectCore(ILogger logger, string connectionId, string reason); + private static partial void ConnectionDisconnectCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string reason); public static void ConnectionDisconnect(ILogger logger, NamedPipeConnection connection, string reason) { diff --git a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeTransportFactory.cs b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeTransportFactory.cs index e8f8023a91bf..162e256a38bc 100644 --- a/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.NamedPipes/src/Internal/NamedPipeTransportFactory.cs @@ -45,19 +45,22 @@ public ValueTask BindAsync(EndPoint endpoint, CancellationT throw new NotSupportedException($@"Server name '{namedPipeEndPoint.ServerName}' is invalid. The server name must be ""{LocalComputerServerName}""."); } - // Creating a named pipe server with an name isn't exclusive. Create a mutex with the pipe name to prevent multiple endpoints - // accidently sharing the same pipe name. Will detect across Kestrel processes. - // Note that this doesn't prevent other applications from using the pipe name. - var mutexName = "Kestrel-NamedPipe-" + namedPipeEndPoint.PipeName; - var mutex = new Mutex(false, mutexName, out var createdNew); - if (!createdNew) + var listener = new NamedPipeConnectionListener(namedPipeEndPoint, _options, _loggerFactory, _objectPoolProvider); + + // Start the listener and create NamedPipeServerStream instances immediately. + // The first server stream is created with the FirstPipeInstance flag. The FirstPipeInstance flag ensures + // that no other apps have a running pipe with the configured name. This check is important because: + // 1. Some settings, such as ACL, are chosen by the first pipe to start with a given name. + // 2. It's easy to run two Kestrel app instances. Want to avoid two apps listening on the same pipe and creating confusion. + // The second launched app instance should immediately fail with an error message, just like port binding conflicts. + try { - mutex.Dispose(); - throw new AddressInUseException($"Named pipe '{namedPipeEndPoint.PipeName}' is already in use by Kestrel."); + listener.Start(); + } + catch (UnauthorizedAccessException ex) + { + throw new AddressInUseException($"Named pipe '{namedPipeEndPoint.PipeName}' is already in use.", ex); } - - var listener = new NamedPipeConnectionListener(namedPipeEndPoint, _options, _loggerFactory, _objectPoolProvider, mutex); - listener.Start(); return new ValueTask(listener); } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs index 7077f79ca6fb..af556af1d502 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicLog.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Security; using Microsoft.AspNetCore.Connections; @@ -11,7 +12,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal; internal static partial class QuicLog { [LoggerMessage(1, LogLevel.Debug, @"Connection id ""{ConnectionId}"" accepted.", EventName = "AcceptedConnection", SkipEnabledCheck = true)] - private static partial void AcceptedConnectionCore(ILogger logger, string connectionId); + private static partial void AcceptedConnectionCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void AcceptedConnection(ILogger logger, BaseConnectionContext connection) { @@ -22,7 +23,7 @@ public static void AcceptedConnection(ILogger logger, BaseConnectionContext conn } [LoggerMessage(2, LogLevel.Debug, @"Stream id ""{ConnectionId}"" type {StreamType} accepted.", EventName = "AcceptedStream", SkipEnabledCheck = true)] - private static partial void AcceptedStreamCore(ILogger logger, string connectionId, StreamType streamType); + private static partial void AcceptedStreamCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, StreamType streamType); public static void AcceptedStream(ILogger logger, QuicStreamContext streamContext) { @@ -33,7 +34,7 @@ public static void AcceptedStream(ILogger logger, QuicStreamContext streamContex } [LoggerMessage(3, LogLevel.Debug, @"Stream id ""{ConnectionId}"" type {StreamType} connected.", EventName = "ConnectedStream", SkipEnabledCheck = true)] - private static partial void ConnectedStreamCore(ILogger logger, string connectionId, StreamType streamType); + private static partial void ConnectedStreamCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, StreamType streamType); public static void ConnectedStream(ILogger logger, QuicStreamContext streamContext) { @@ -44,7 +45,7 @@ public static void ConnectedStream(ILogger logger, QuicStreamContext streamConte } [LoggerMessage(4, LogLevel.Debug, @"Connection id ""{ConnectionId}"" unexpected error.", EventName = "ConnectionError", SkipEnabledCheck = true)] - private static partial void ConnectionErrorCore(ILogger logger, string connectionId, Exception ex); + private static partial void ConnectionErrorCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Exception ex); public static void ConnectionError(ILogger logger, BaseConnectionContext connection, Exception ex) { @@ -55,7 +56,7 @@ public static void ConnectionError(ILogger logger, BaseConnectionContext connect } [LoggerMessage(5, LogLevel.Debug, @"Connection id ""{ConnectionId}"" aborted by peer with error code {ErrorCode}.", EventName = "ConnectionAborted", SkipEnabledCheck = true)] - private static partial void ConnectionAbortedCore(ILogger logger, string connectionId, long errorCode, Exception ex); + private static partial void ConnectionAbortedCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long errorCode, Exception ex); public static void ConnectionAborted(ILogger logger, BaseConnectionContext connection, long errorCode, Exception ex) { @@ -66,7 +67,7 @@ public static void ConnectionAborted(ILogger logger, BaseConnectionContext conne } [LoggerMessage(6, LogLevel.Debug, @"Connection id ""{ConnectionId}"" aborted by application with error code {ErrorCode} because: ""{Reason}"".", EventName = "ConnectionAbort", SkipEnabledCheck = true)] - private static partial void ConnectionAbortCore(ILogger logger, string connectionId, long errorCode, string reason); + private static partial void ConnectionAbortCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long errorCode, string reason); public static void ConnectionAbort(ILogger logger, BaseConnectionContext connection, long errorCode, string reason) { @@ -77,7 +78,7 @@ public static void ConnectionAbort(ILogger logger, BaseConnectionContext connect } [LoggerMessage(7, LogLevel.Debug, @"Stream id ""{ConnectionId}"" unexpected error.", EventName = "StreamError", SkipEnabledCheck = true)] - private static partial void StreamErrorCore(ILogger logger, string connectionId, Exception ex); + private static partial void StreamErrorCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Exception ex); public static void StreamError(ILogger logger, QuicStreamContext streamContext, Exception ex) { @@ -88,7 +89,7 @@ public static void StreamError(ILogger logger, QuicStreamContext streamContext, } [LoggerMessage(8, LogLevel.Debug, @"Stream id ""{ConnectionId}"" paused.", EventName = "StreamPause", SkipEnabledCheck = true)] - private static partial void StreamPauseCore(ILogger logger, string connectionId); + private static partial void StreamPauseCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void StreamPause(ILogger logger, QuicStreamContext streamContext) { @@ -99,7 +100,7 @@ public static void StreamPause(ILogger logger, QuicStreamContext streamContext) } [LoggerMessage(9, LogLevel.Debug, @"Stream id ""{ConnectionId}"" resumed.", EventName = "StreamResume", SkipEnabledCheck = true)] - private static partial void StreamResumeCore(ILogger logger, string connectionId); + private static partial void StreamResumeCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void StreamResume(ILogger logger, QuicStreamContext streamContext) { @@ -110,7 +111,7 @@ public static void StreamResume(ILogger logger, QuicStreamContext streamContext) } [LoggerMessage(10, LogLevel.Debug, @"Stream id ""{ConnectionId}"" shutting down writes because: ""{Reason}"".", EventName = "StreamShutdownWrite", SkipEnabledCheck = true)] - private static partial void StreamShutdownWriteCore(ILogger logger, string connectionId, string reason); + private static partial void StreamShutdownWriteCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string reason); public static void StreamShutdownWrite(ILogger logger, QuicStreamContext streamContext, string reason) { @@ -121,7 +122,7 @@ public static void StreamShutdownWrite(ILogger logger, QuicStreamContext streamC } [LoggerMessage(11, LogLevel.Debug, @"Stream id ""{ConnectionId}"" read aborted by peer with error code {ErrorCode}.", EventName = "StreamAbortedRead", SkipEnabledCheck = true)] - private static partial void StreamAbortedReadCore(ILogger logger, string connectionId, long errorCode); + private static partial void StreamAbortedReadCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long errorCode); public static void StreamAbortedRead(ILogger logger, QuicStreamContext streamContext, long errorCode) { @@ -132,7 +133,7 @@ public static void StreamAbortedRead(ILogger logger, QuicStreamContext streamCon } [LoggerMessage(12, LogLevel.Debug, @"Stream id ""{ConnectionId}"" write aborted by peer with error code {ErrorCode}.", EventName = "StreamAbortedWrite", SkipEnabledCheck = true)] - private static partial void StreamAbortedWriteCore(ILogger logger, string connectionId, long errorCode); + private static partial void StreamAbortedWriteCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long errorCode); public static void StreamAbortedWrite(ILogger logger, QuicStreamContext streamContext, long errorCode) { @@ -143,7 +144,7 @@ public static void StreamAbortedWrite(ILogger logger, QuicStreamContext streamCo } [LoggerMessage(13, LogLevel.Debug, @"Stream id ""{ConnectionId}"" aborted by application with error code {ErrorCode} because: ""{Reason}"".", EventName = "StreamAbort", SkipEnabledCheck = true)] - private static partial void StreamAbortCore(ILogger logger, string connectionId, long errorCode, string reason); + private static partial void StreamAbortCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long errorCode, string reason); public static void StreamAbort(ILogger logger, QuicStreamContext streamContext, long errorCode, string reason) { @@ -154,7 +155,7 @@ public static void StreamAbort(ILogger logger, QuicStreamContext streamContext, } [LoggerMessage(14, LogLevel.Debug, @"Stream id ""{ConnectionId}"" read side aborted by application with error code {ErrorCode} because: ""{Reason}"".", EventName = "StreamAbortRead", SkipEnabledCheck = true)] - private static partial void StreamAbortReadCore(ILogger logger, string connectionId, long errorCode, string reason); + private static partial void StreamAbortReadCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long errorCode, string reason); public static void StreamAbortRead(ILogger logger, QuicStreamContext streamContext, long errorCode, string reason) { @@ -165,7 +166,7 @@ public static void StreamAbortRead(ILogger logger, QuicStreamContext streamConte } [LoggerMessage(15, LogLevel.Debug, @"Stream id ""{ConnectionId}"" write side aborted by application with error code {ErrorCode} because: ""{Reason}"".", EventName = "StreamAbortWrite", SkipEnabledCheck = true)] - private static partial void StreamAbortWriteCore(ILogger logger, string connectionId, long errorCode, string reason); + private static partial void StreamAbortWriteCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long errorCode, string reason); public static void StreamAbortWrite(ILogger logger, QuicStreamContext streamContext, long errorCode, string reason) { @@ -176,7 +177,7 @@ public static void StreamAbortWrite(ILogger logger, QuicStreamContext streamCont } [LoggerMessage(16, LogLevel.Trace, @"Stream id ""{ConnectionId}"" pooled for reuse.", EventName = "StreamPooled", SkipEnabledCheck = true)] - private static partial void StreamPooledCore(ILogger logger, string connectionId); + private static partial void StreamPooledCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void StreamPooled(ILogger logger, QuicStreamContext streamContext) { @@ -187,7 +188,7 @@ public static void StreamPooled(ILogger logger, QuicStreamContext streamContext) } [LoggerMessage(17, LogLevel.Trace, @"Stream id ""{ConnectionId}"" reused from pool.", EventName = "StreamReused", SkipEnabledCheck = true)] - private static partial void StreamReusedCore(ILogger logger, string connectionId); + private static partial void StreamReusedCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void StreamReused(ILogger logger, QuicStreamContext streamContext) { @@ -211,7 +212,7 @@ public static void StreamReused(ILogger logger, QuicStreamContext streamContext) public static partial void ConnectionListenerAborted(ILogger logger, Exception exception); [LoggerMessage(22, LogLevel.Debug, @"Stream id ""{ConnectionId}"" read timed out.", EventName = "StreamTimeoutRead", SkipEnabledCheck = true)] - private static partial void StreamTimeoutReadCore(ILogger logger, string connectionId); + private static partial void StreamTimeoutReadCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void StreamTimeoutRead(ILogger logger, QuicStreamContext streamContext) { @@ -222,7 +223,7 @@ public static void StreamTimeoutRead(ILogger logger, QuicStreamContext streamCon } [LoggerMessage(23, LogLevel.Debug, @"Stream id ""{ConnectionId}"" write timed out.", EventName = "StreamTimeoutWrite", SkipEnabledCheck = true)] - private static partial void StreamTimeoutWriteCore(ILogger logger, string connectionId); + private static partial void StreamTimeoutWriteCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void StreamTimeoutWrite(ILogger logger, QuicStreamContext streamContext) { diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsLog.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsLog.cs index 7d5c791fc98f..c3283779b1bf 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsLog.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsLog.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; @@ -10,7 +11,7 @@ internal static partial class SocketsLog // Reserved: Event ID 3, EventName = ConnectionRead [LoggerMessage(6, LogLevel.Debug, @"Connection id ""{ConnectionId}"" received FIN.", EventName = "ConnectionReadFin", SkipEnabledCheck = true)] - private static partial void ConnectionReadFinCore(ILogger logger, string connectionId); + private static partial void ConnectionReadFinCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void ConnectionReadFin(ILogger logger, SocketConnection connection) { @@ -21,7 +22,7 @@ public static void ConnectionReadFin(ILogger logger, SocketConnection connection } [LoggerMessage(7, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending FIN because: ""{Reason}""", EventName = "ConnectionWriteFin", SkipEnabledCheck = true)] - private static partial void ConnectionWriteFinCore(ILogger logger, string connectionId, string reason); + private static partial void ConnectionWriteFinCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string reason); public static void ConnectionWriteFin(ILogger logger, SocketConnection connection, string reason) { @@ -36,7 +37,7 @@ public static void ConnectionWriteFin(ILogger logger, SocketConnection connectio // Reserved: Event ID 12, EventName = ConnectionWriteCallback [LoggerMessage(14, LogLevel.Debug, @"Connection id ""{ConnectionId}"" communication error.", EventName = "ConnectionError", SkipEnabledCheck = true)] - private static partial void ConnectionErrorCore(ILogger logger, string connectionId, Exception ex); + private static partial void ConnectionErrorCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, Exception ex); public static void ConnectionError(ILogger logger, SocketConnection connection, Exception ex) { @@ -47,7 +48,7 @@ public static void ConnectionError(ILogger logger, SocketConnection connection, } [LoggerMessage(19, LogLevel.Debug, @"Connection id ""{ConnectionId}"" reset.", EventName = "ConnectionReset", SkipEnabledCheck = true)] - public static partial void ConnectionReset(ILogger logger, string connectionId); + public static partial void ConnectionReset(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void ConnectionReset(ILogger logger, SocketConnection connection) { @@ -58,7 +59,7 @@ public static void ConnectionReset(ILogger logger, SocketConnection connection) } [LoggerMessage(4, LogLevel.Debug, @"Connection id ""{ConnectionId}"" paused.", EventName = "ConnectionPause", SkipEnabledCheck = true)] - private static partial void ConnectionPauseCore(ILogger logger, string connectionId); + private static partial void ConnectionPauseCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void ConnectionPause(ILogger logger, SocketConnection connection) { @@ -69,7 +70,7 @@ public static void ConnectionPause(ILogger logger, SocketConnection connection) } [LoggerMessage(5, LogLevel.Debug, @"Connection id ""{ConnectionId}"" resumed.", EventName = "ConnectionResume", SkipEnabledCheck = true)] - private static partial void ConnectionResumeCore(ILogger logger, string connectionId); + private static partial void ConnectionResumeCore(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); public static void ConnectionResume(ILogger logger, SocketConnection connection) { diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj b/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj index acb6b02307c2..ae10ecd08351 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Servers/Kestrel/shared/test/TestContextFactory.cs b/src/Servers/Kestrel/shared/test/TestContextFactory.cs index 5413e4c13249..060457c4be2c 100644 --- a/src/Servers/Kestrel/shared/test/TestContextFactory.cs +++ b/src/Servers/Kestrel/shared/test/TestContextFactory.cs @@ -15,7 +15,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Metrics; namespace Microsoft.AspNetCore.Testing; diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index 49712725a9a9..0e6f8eb3b823 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Metrics; namespace Microsoft.AspNetCore.Testing; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs index 15393c8950e1..8ccbd261b97e 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs @@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Server.Kestrel.Tests; using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -102,7 +102,7 @@ public async Task UpgradedConnectionsCountsAgainstDifferentLimit() public async Task RejectsConnectionsWhenLimitReached() { var testMeterFactory = new TestMeterFactory(); - using var rejectedConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "rejected-connections"); + using var rejectedConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "rejected-connections"); const int max = 10; var requestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs index 3362f78150ea..7d7c61e5a477 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; -using System.Net; using System.Net.Http; using System.Net.Security; using System.Security.Authentication; @@ -24,7 +22,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Metrics; using Moq; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -129,15 +126,25 @@ void ConfigureListenOptions(ListenOptions listenOptions) [Fact] public async Task HandshakeDetailsAreAvailable() { + string expectedHostname = null; void ConfigureListenOptions(ListenOptions listenOptions) { - listenOptions.UseHttps(new HttpsConnectionAdapterOptions { ServerCertificate = _x509Certificate2 }); + listenOptions.UseHttps( + new HttpsConnectionAdapterOptions + { + ServerCertificateSelector = (connection, name) => + { + expectedHostname = name; + return _x509Certificate2; + } + }); }; await using (var server = new TestServer(context => { var tlsFeature = context.Features.Get(); Assert.NotNull(tlsFeature); + Assert.Equal(expectedHostname, tlsFeature.HostName); Assert.True(tlsFeature.Protocol > SslProtocols.None, "Protocol"); Assert.True(tlsFeature.NegotiatedCipherSuite >= TlsCipherSuite.TLS_NULL_WITH_NULL_NULL, "NegotiatedCipherSuite"); Assert.True(tlsFeature.CipherAlgorithm > CipherAlgorithmType.Null, "Cipher"); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs index f0f223373bf4..4c252cb6ffbd 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs @@ -1,14 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -24,10 +20,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Metrics; -using Microsoft.Extensions.Options; using Moq; -using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj index 576463f85fca..7f5ed843519d 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs index c0e8349a3bbb..86457feab09b 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs @@ -18,7 +18,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -46,9 +46,9 @@ public async Task Http1Connection() }); var testMeterFactory = new TestMeterFactory(); - using var connectionDuration = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); - using var currentConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); - using var queuedConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "queued-connections"); + using var connectionDuration = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); + using var currentConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); + using var queuedConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "queued-connections"); var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); @@ -122,9 +122,9 @@ public async Task Http1Connection_BeginListeningAfterConnectionStarted() // Wait for connection to start on the server. await sync.WaitForSyncPoint(); - using var connectionDuration = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); - using var currentConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); - using var queuedConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "queued-connections"); + using var connectionDuration = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); + using var currentConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); + using var queuedConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "queued-connections"); // Signal that connection can continue. sync.Continue(); @@ -169,9 +169,9 @@ public async Task Http1Connection_IHttpConnectionTagsFeatureIgnoreFeatureSetOnTr }); var testMeterFactory = new TestMeterFactory(); - using var connectionDuration = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); - using var currentConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); - using var queuedConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "queued-connections"); + using var connectionDuration = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); + using var currentConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); + using var queuedConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "queued-connections"); var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); @@ -244,9 +244,9 @@ public async Task Http1Connection_Error() }); var testMeterFactory = new TestMeterFactory(); - using var connectionDuration = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); - using var currentConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); - using var queuedConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "queued-connections"); + using var connectionDuration = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); + using var currentConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); + using var queuedConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "queued-connections"); var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); @@ -287,9 +287,9 @@ public async Task Http1Connection_Upgrade() var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); var testMeterFactory = new TestMeterFactory(); - using var connectionDuration = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); - using var currentConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); - using var currentUpgradedRequests = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "current-upgraded-connections"); + using var connectionDuration = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); + using var currentConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); + using var currentUpgradedRequests = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "current-upgraded-connections"); var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); @@ -331,12 +331,12 @@ public async Task Http2Connection() var requestsReceived = 0; var testMeterFactory = new TestMeterFactory(); - using var connectionDuration = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); - using var currentConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); - using var queuedConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "queued-connections"); - using var queuedRequests = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "queued-requests"); - using var tlsHandshakeDuration = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "tls-handshake-duration"); - using var currentTlsHandshakes = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), "Microsoft.AspNetCore.Server.Kestrel", "current-tls-handshakes"); + using var connectionDuration = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "connection-duration"); + using var currentConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "current-connections"); + using var queuedConnections = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "queued-connections"); + using var queuedRequests = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "queued-requests"); + using var tlsHandshakeDuration = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "tls-handshake-duration"); + using var currentTlsHandshakes = new InstrumentRecorder(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "current-tls-handshakes"); await using (var server = new TestServer(context => { diff --git a/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj b/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj index 5d7992a557a7..a584f2368fdf 100644 --- a/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj +++ b/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj b/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj index 2b8e9b81899c..4a45a7f70434 100644 --- a/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Shared/ClosedGenericMatcher/ClosedGenericMatcher.cs b/src/Shared/ClosedGenericMatcher/ClosedGenericMatcher.cs index 7a77dfa3d4cc..c1989174bc1e 100644 --- a/src/Shared/ClosedGenericMatcher/ClosedGenericMatcher.cs +++ b/src/Shared/ClosedGenericMatcher/ClosedGenericMatcher.cs @@ -32,8 +32,13 @@ internal static class ClosedGenericMatcher /// public static Type? ExtractGenericInterface(Type queryType, Type interfaceType) { +#if !NET8_0_OR_GREATER ArgumentNullThrowHelper.ThrowIfNull(queryType); ArgumentNullThrowHelper.ThrowIfNull(interfaceType); +#else + ArgumentNullException.ThrowIfNull(queryType); + ArgumentNullException.ThrowIfNull(interfaceType); +#endif if (IsGenericInstantiation(queryType, interfaceType)) { diff --git a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs index 1ee230c27011..4e9f90351f3f 100644 --- a/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs +++ b/src/Shared/HttpSys/NativeInterop/HttpApiTypes.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Authentication; using Microsoft.AspNetCore.Http; @@ -108,6 +109,28 @@ internal struct HTTP_REQUEST_PROPERTY_STREAM_ERROR internal uint ErrorCode; } + internal const int HTTP_REQUEST_PROPERTY_SNI_HOST_MAX_LENGTH = 255; + internal const int SniPropertySizeInBytes = (sizeof(ushort) * (HTTP_REQUEST_PROPERTY_SNI_HOST_MAX_LENGTH + 1)) + sizeof(ulong); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Size = SniPropertySizeInBytes)] + internal struct HTTP_REQUEST_PROPERTY_SNI + { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = HTTP_REQUEST_PROPERTY_SNI_HOST_MAX_LENGTH + 1)] + internal string Hostname; + + internal HTTP_REQUEST_PROPERTY_SNI_FLAGS Flags; + } + + [Flags] + internal enum HTTP_REQUEST_PROPERTY_SNI_FLAGS : uint + { + // Indicates that SNI was used for successful endpoint lookup during handshake. + // If client sent the SNI but Http.sys still decided to use IP endpoint binding then this flag will not be set. + HTTP_REQUEST_PROPERTY_SNI_FLAG_SNI_USED = 0x00000001, + // Indicates that client did not send the SNI. + HTTP_REQUEST_PROPERTY_SNI_FLAG_NO_SNI = 0x00000002, + } + internal const int MaxTimeout = 6; [StructLayout(LayoutKind.Sequential)] diff --git a/src/Shared/Metrics/DefaultMeterFactory.cs b/src/Shared/Metrics/DefaultMeterFactory.cs deleted file mode 100644 index f6961526d1f7..000000000000 --- a/src/Shared/Metrics/DefaultMeterFactory.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.Metrics; -using System.Linq; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.Metrics; - -// TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 -internal sealed class DefaultMeterFactory : IMeterFactory -{ - private readonly IOptions _options; - private readonly IMeterRegistry _meterRegistry; - private readonly Dictionary _meters = new Dictionary(); - - public DefaultMeterFactory(IOptions options, IMeterRegistry meterRegistry) - { - _options = options; - _meterRegistry = meterRegistry; - } - - public Meter CreateMeter(string name) - { - return CreateMeterCore(name, version: null, defaultTags: null); - } - - public Meter CreateMeter(MeterOptions options) - { - return CreateMeterCore(options.Name, options.Version, options.DefaultTags); - } - - private Meter CreateMeterCore(string name, string? version, IList>? defaultTags) - { - var tags = defaultTags?.ToArray(); - if (tags != null) - { - Array.Sort(tags, (t1, t2) => string.Compare(t1.Key, t2.Key, StringComparison.Ordinal)); - } - var key = new MeterKey(name, version, tags); - - if (_meters.TryGetValue(key, out var meter)) - { - return meter; - } - - // TODO: Configure meter with default tags. - meter = new Meter(name, version); - _meters[key] = meter; - _meterRegistry.Add(meter); - - return meter; - } - - private readonly struct MeterKey : IEquatable - { - public MeterKey(string name, string? version, KeyValuePair[]? defaultTags) - { - Name = name; - Version = version; - DefaultTags = defaultTags; - } - - public string Name { get; } - public string? Version { get; } - public IList>? DefaultTags { get; } - - public bool Equals(MeterKey other) - { - return Name == other.Name - && Version == other.Version - && TagsEqual(other); - } - - private bool TagsEqual(MeterKey other) - { - if (DefaultTags is null && other.DefaultTags is null) - { - return true; - } - if (DefaultTags is not null && other.DefaultTags is not null && DefaultTags.SequenceEqual(other.DefaultTags)) - { - return true; - } - return false; - } - - public override bool Equals(object? obj) - { - return obj is MeterKey key && Equals(key); - } - - public override int GetHashCode() - { - var hashCode = new HashCode(); - hashCode.Add(Name); - hashCode.Add(Version); - if (DefaultTags is not null) - { - foreach (var item in DefaultTags) - { - hashCode.Add(item); - } - } - - return hashCode.ToHashCode(); - } - } -} diff --git a/src/Shared/Metrics/IMeterFactory.cs b/src/Shared/Metrics/IMeterFactory.cs deleted file mode 100644 index 82563cc89701..000000000000 --- a/src/Shared/Metrics/IMeterFactory.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.Metrics; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Metrics; - -// TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 -internal sealed class MetricsOptions -{ - public IList> DefaultTags { get; } = new List>(); -} - -internal interface IMetricsBuilder -{ - IServiceCollection Services { get; } -} - -internal sealed class MetricsBuilder : IMetricsBuilder -{ - public MetricsBuilder(IServiceCollection services) => Services = services; - public IServiceCollection Services { get; } -} - -internal sealed class MeterOptions -{ - public required string Name { get; set; } - public string? Version { get; set; } - public IList>? DefaultTags { get; set; } -} - -internal interface IMeterFactory -{ - Meter CreateMeter(string name); - Meter CreateMeter(MeterOptions options); -} - -internal interface IMeterRegistry -{ - void Add(Meter meter); - bool Contains(Meter meter); -} - -internal sealed class DefaultMeterRegistry : IMeterRegistry, IDisposable -{ - private readonly object _lock = new object(); - private readonly List _meters = new List(); - - public void Add(Meter meter) - { - lock (_lock) - { - _meters.Add(meter); - } - } - - public bool Contains(Meter meter) - { - lock (_lock) - { - return _meters.Contains(meter); - } - } - - public void Dispose() - { - lock (_lock) - { - foreach (var meter in _meters) - { - meter.Dispose(); - } - _meters.Clear(); - } - } -} diff --git a/src/Shared/Metrics/InstrumentRecorder.cs b/src/Shared/Metrics/InstrumentRecorder.cs deleted file mode 100644 index 33fb51ea8f4e..000000000000 --- a/src/Shared/Metrics/InstrumentRecorder.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.Metrics; - -namespace Microsoft.Extensions.Metrics; - -// TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 -internal sealed class InstrumentRecorder : IDisposable where T : struct -{ - private readonly object _lock = new object(); - private readonly string _meterName; - private readonly string _instrumentName; - private readonly MeterListener _meterListener; - private readonly List> _values; - private readonly List>> _callbacks; - - public InstrumentRecorder(IMeterRegistry registry, string meterName, string instrumentName, object? state = null) - { - _meterName = meterName; - _instrumentName = instrumentName; - _callbacks = new List>>(); - _values = new List>(); - _meterListener = new MeterListener(); - _meterListener.InstrumentPublished = (instrument, listener) => - { - if (instrument.Meter.Name == _meterName && registry.Contains(instrument.Meter) && instrument.Name == _instrumentName) - { - listener.EnableMeasurementEvents(instrument, state); - } - }; - _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded); - _meterListener.Start(); - } - - private void OnMeasurementRecorded(Instrument instrument, T measurement, ReadOnlySpan> tags, object? state) - { - lock (_lock) - { - var m = new Measurement(measurement, tags); - _values.Add(m); - - // Should this happen in the lock? - // Is there a better way to notify listeners that there are new measurements? - foreach (var callback in _callbacks) - { - callback(m); - } - } - } - - public void Register(Action> callback) - { - _callbacks.Add(callback); - } - - public IReadOnlyList> GetMeasurements() - { - lock (_lock) - { - return _values.ToArray(); - } - } - - public void Dispose() - { - _meterListener.Dispose(); - } -} diff --git a/src/Shared/Metrics/MetricsServiceExtensions.cs b/src/Shared/Metrics/MetricsServiceExtensions.cs deleted file mode 100644 index f18fa7148476..000000000000 --- a/src/Shared/Metrics/MetricsServiceExtensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Metrics; - -namespace Microsoft.Extensions.DependencyInjection; - -// TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 -internal static class MetricsServiceExtensions -{ - public static IServiceCollection AddMetrics(this IServiceCollection services) - { - ArgumentNullException.ThrowIfNull(services); - - services.TryAddSingleton(); - services.TryAddSingleton(); - - return services; - } - - public static IServiceCollection AddMetrics(this IServiceCollection services, Action configure) - { - ArgumentNullException.ThrowIfNull(services); - - services.AddMetrics(); - configure(new MetricsBuilder(services)); - - return services; - } - - public static IMetricsBuilder AddDefaultTag(this IMetricsBuilder builder, string name, object? value) - { - builder.Services.Configure(o => o.DefaultTags.Add(new KeyValuePair(name, value))); - return builder; - } -} diff --git a/src/Shared/Metrics/TestMeterFactory.cs b/src/Shared/Metrics/TestMeterFactory.cs index 9f1f2495d247..9553f06dce04 100644 --- a/src/Shared/Metrics/TestMeterFactory.cs +++ b/src/Shared/Metrics/TestMeterFactory.cs @@ -2,43 +2,72 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics; -namespace Microsoft.Extensions.Metrics; +namespace Microsoft.AspNetCore.Testing; -// TODO: Remove when Metrics DI intergration package is available https://github.com/dotnet/aspnetcore/issues/47618 -internal class TestMeterFactory : IMeterFactory +internal sealed class TestMeterFactory : IMeterFactory { public List Meters { get; } = new List(); - public Meter CreateMeter(string name) + public Meter Create(MeterOptions options) { - var meter = new Meter(name); + var meter = new Meter(options.Name, options.Version, Array.Empty>(), scope: this); Meters.Add(meter); return meter; } - public Meter CreateMeter(MeterOptions options) + public void Dispose() { - var meter = new Meter(options.Name, options.Version); - Meters.Add(meter); - return meter; + foreach (var meter in Meters) + { + meter.Dispose(); + } + + Meters.Clear(); } } -internal class TestMeterRegistry : IMeterRegistry +internal sealed class MeasurementReporter : IDisposable where T : struct { - private readonly List _meters; + private readonly string _meterName; + private readonly string _instrumentName; + private readonly List>> _callbacks; + private readonly MeterListener _meterListener; - public TestMeterRegistry() : this(new List()) + public MeasurementReporter(IMeterFactory factory, string meterName, string instrumentName, object state = null) { + _meterName = meterName; + _instrumentName = instrumentName; + _callbacks = new List>>(); + _meterListener = new MeterListener(); + _meterListener.InstrumentPublished = (instrument, listener) => + { + if (instrument.Meter.Name == _meterName && instrument.Meter.Scope == factory && instrument.Name == _instrumentName) + { + listener.EnableMeasurementEvents(instrument, state); + } + }; + _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded); + _meterListener.Start(); } - public TestMeterRegistry(List meters) + private void OnMeasurementRecorded(Instrument instrument, T measurement, ReadOnlySpan> tags, object state) { - _meters = meters; + var m = new Measurement(measurement, tags); + foreach (var callback in _callbacks) + { + callback(m); + } } - public void Add(Meter meter) => _meters.Add(meter); + public void Register(Action> callback) + { + _callbacks.Add(callback); + } - public bool Contains(Meter meter) => _meters.Contains(meter); + public void Dispose() + { + _meterListener.Dispose(); + } } diff --git a/src/Shared/PropertyHelper/PropertyHelper.cs b/src/Shared/PropertyHelper/PropertyHelper.cs index 027066a6d1d7..20b10b4a6be5 100644 --- a/src/Shared/PropertyHelper/PropertyHelper.cs +++ b/src/Shared/PropertyHelper/PropertyHelper.cs @@ -42,8 +42,6 @@ internal sealed class PropertyHelper private static readonly ConcurrentDictionary VisiblePropertiesCache = new(); - private static readonly Type IsByRefLikeAttribute = typeof(System.Runtime.CompilerServices.IsByRefLikeAttribute); - private Action? _valueSetter; private Func? _valueGetter; @@ -552,25 +550,16 @@ private static bool IsInterestingProperty(PropertyInfo property) property.GetMethod.IsPublic && !property.GetMethod.IsStatic && - // PropertyHelper can't work with ref structs. - !IsRefStructProperty(property) && + // PropertyHelper can't really interact with ref-struct properties since they can't be + // boxed and can't be used as generic types. We just ignore them. + // + // see: https://github.com/aspnet/Mvc/issues/8545 + !property.PropertyType.IsByRefLike && // Indexed properties are not useful (or valid) for grabbing properties off an object. property.GetMethod.GetParameters().Length == 0; } - // PropertyHelper can't really interact with ref-struct properties since they can't be - // boxed and can't be used as generic types. We just ignore them. - // - // see: https://github.com/aspnet/Mvc/issues/8545 - private static bool IsRefStructProperty(PropertyInfo property) - { - return - IsByRefLikeAttribute != null && - property.PropertyType.IsValueType && - property.PropertyType.IsDefined(IsByRefLikeAttribute); - } - internal static class MetadataUpdateHandler { /// diff --git a/src/Shared/RoslynUtils/ParsabilityHelper.cs b/src/Shared/RoslynUtils/ParsabilityHelper.cs index a5ea24493d46..496510f5bbdc 100644 --- a/src/Shared/RoslynUtils/ParsabilityHelper.cs +++ b/src/Shared/RoslynUtils/ParsabilityHelper.cs @@ -151,9 +151,10 @@ private static bool IsReturningValueTaskOfTOrNullableT(INamedTypeSymbol returnTy SymbolEqualityComparer.Default.Equals(returnType.TypeArguments[0].UnwrapTypeSymbol(unwrapNullable: true), containingType); } - internal static Bindability GetBindability(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, out BindabilityMethod? bindabilityMethod) + internal static Bindability GetBindability(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, out BindabilityMethod? bindabilityMethod, out IMethodSymbol? bindMethodSymbol) { bindabilityMethod = null; + bindMethodSymbol = null; if (IsBindableViaIBindableFromHttpContext(typeSymbol, wellKnownTypes)) { @@ -165,6 +166,7 @@ internal static Bindability GetBindability(ITypeSymbol typeSymbol, WellKnownType // It's easy to find, but we need to flow the interface back to the emitter to call it. // With parent types, we can continue to pretend we're calling a method directly on the child. var bindAsyncMethods = typeSymbol.GetThisAndBaseTypes() + .Concat(typeSymbol.AllInterfaces) .SelectMany(t => t.GetMembers("BindAsync")) .OfType(); @@ -173,11 +175,13 @@ internal static Bindability GetBindability(ITypeSymbol typeSymbol, WellKnownType if (IsBindAsyncWithParameter(methodSymbol, typeSymbol, wellKnownTypes)) { bindabilityMethod = BindabilityMethod.BindAsyncWithParameter; + bindMethodSymbol = methodSymbol; break; } if (IsBindAsync(methodSymbol, typeSymbol, wellKnownTypes)) { bindabilityMethod = BindabilityMethod.BindAsync; + bindMethodSymbol = methodSymbol; } } diff --git a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.Log.cs b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.Log.cs index 3aa896767775..070fb2d2d43c 100644 --- a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.Log.cs +++ b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.Log.cs @@ -325,5 +325,14 @@ public static void ErrorHandshakeTimedOut(ILogger logger, TimeSpan handshakeTime [LoggerMessage(89, LogLevel.Trace, "Error sending Completion message for stream '{StreamId}'.", EventName = "ErrorSendingStreamCompletion")] public static partial void ErrorSendingStreamCompletion(ILogger logger, string streamId, Exception exception); + + [LoggerMessage(90, LogLevel.Trace, "Dropping {MessageType} with ID '{InvocationId}'.", EventName = "DroppingMessage")] + public static partial void DroppingMessage(ILogger logger, string messageType, string? invocationId); + + [LoggerMessage(91, LogLevel.Trace, "Received AckMessage with Sequence ID '{SequenceId}'.", EventName = "ReceivedAckMessage")] + public static partial void ReceivedAckMessage(ILogger logger, long sequenceId); + + [LoggerMessage(92, LogLevel.Trace, "Received SequenceMessage with Sequence ID '{SequenceId}'.", EventName = "ReceivedSequenceMessage")] + public static partial void ReceivedSequenceMessage(ILogger logger, long sequenceId); } } diff --git a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs index 797ecee716c8..f2b9b70eda1f 100644 --- a/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs +++ b/src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs @@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.IO.Pipelines; using System.Linq; using System.Net; using System.Reflection; @@ -16,6 +17,7 @@ using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Abstractions; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Shared; @@ -946,11 +948,19 @@ private async Task InvokeStreamCore(ConnectionState connectionState, string meth private async Task SendHubMessage(ConnectionState connectionState, HubMessage hubMessage, CancellationToken cancellationToken = default) { _state.AssertConnectionValid(); - _protocol.WriteMessage(hubMessage, connectionState.Connection.Transport.Output); Log.SendingMessage(_logger, hubMessage); - await connectionState.Connection.Transport.Output.FlushAsync(cancellationToken).ConfigureAwait(false); + if (connectionState.UsingAcks()) + { + await connectionState.WriteAsync(new SerializedHubMessage(hubMessage), cancellationToken).ConfigureAwait(false); + } + else + { + _protocol.WriteMessage(hubMessage, connectionState.Connection.Transport.Output); + + await connectionState.Connection.Transport.Output.FlushAsync(cancellationToken).ConfigureAwait(false); + } Log.MessageSent(_logger, hubMessage); // We've sent a message, so don't ping for a while @@ -1004,6 +1014,11 @@ private async Task SendWithLock(ConnectionState expectedConnectionState, HubMess Log.ResettingKeepAliveTimer(_logger); connectionState.ResetTimeout(); + if (!connectionState.ShouldProcessMessage(message)) + { + return null; + } + InvocationRequest? irq; switch (message) { @@ -1055,6 +1070,14 @@ private async Task SendWithLock(ConnectionState expectedConnectionState, HubMess Log.ReceivedPing(_logger); // timeout is reset above, on receiving any message break; + case AckMessage ackMessage: + Log.ReceivedAckMessage(_logger, ackMessage.SequenceId); + connectionState.Ack(ackMessage); + break; + case SequenceMessage sequenceMessage: + Log.ReceivedSequenceMessage(_logger, sequenceMessage.SequenceId); + connectionState.ResetSequence(sequenceMessage); + break; default: throw new InvalidOperationException($"Unexpected message type: {message.GetType().FullName}"); } @@ -1235,6 +1258,7 @@ private async Task HandshakeAsync(ConnectionState startingConnectionState, Cance } Log.HandshakeComplete(_logger); + break; } } @@ -1813,6 +1837,7 @@ private sealed class ConnectionState : IInvocationBinder private readonly HubConnection _hubConnection; private readonly ILogger _logger; private readonly bool _hasInherentKeepAlive; + private readonly MessageBuffer? _messageBuffer; private readonly object _lock = new object(); private readonly Dictionary _pendingCalls = new Dictionary(StringComparer.Ordinal); @@ -1850,6 +1875,13 @@ public ConnectionState(ConnectionContext connection, HubConnection hubConnection _logger = _hubConnection._logger; _hasInherentKeepAlive = connection.Features.Get()?.HasInherentKeepAlive ?? false; + + if (Connection.Features.Get() is IReconnectFeature feature) + { + _messageBuffer = new MessageBuffer(connection, hubConnection._protocol); + + feature.NotifyOnReconnect = _messageBuffer.Resend; + } } public string GetNextId() => (++_nextInvocationId).ToString(CultureInfo.InvariantCulture); @@ -1935,6 +1967,8 @@ private async Task StopAsyncCore() { Log.Stopping(_logger); + _messageBuffer?.Dispose(); + // Complete our write pipe, which should cause everything to shut down Log.TerminatingReceiveLoop(_logger); Connection.Transport.Input.CancelPendingRead(); @@ -1966,6 +2000,44 @@ public async Task TimerLoop(TimerAwaitable timer) } } + public ValueTask WriteAsync(SerializedHubMessage message, CancellationToken cancellationToken) + { + Debug.Assert(_messageBuffer is not null); + return _messageBuffer.WriteAsync(message, cancellationToken); + } + + public bool ShouldProcessMessage(HubMessage message) + { + if (UsingAcks()) + { + if (!_messageBuffer.ShouldProcessMessage(message)) + { + Log.DroppingMessage(_logger, ((HubInvocationMessage)message).GetType().Name, ((HubInvocationMessage)message).InvocationId); + return false; + } + } + return true; + } + + public void Ack(AckMessage ackMessage) + { + if (UsingAcks()) + { + _messageBuffer.Ack(ackMessage); + } + } + + public void ResetSequence(SequenceMessage sequenceMessage) + { + if (UsingAcks()) + { + _messageBuffer.ResetSequence(sequenceMessage); + } + } + + [MemberNotNullWhen(true, nameof(_messageBuffer))] + public bool UsingAcks() => _messageBuffer is not null; + public void ResetSendPing() { Volatile.Write(ref _nextActivationSendPing, (DateTime.UtcNow + _hubConnection.KeepAliveInterval).Ticks); diff --git a/src/SignalR/clients/csharp/Client.Core/src/Internal/SerializedHubMessage.cs b/src/SignalR/clients/csharp/Client.Core/src/Internal/SerializedHubMessage.cs new file mode 100644 index 000000000000..4fd1d41d5df9 --- /dev/null +++ b/src/SignalR/clients/csharp/Client.Core/src/Internal/SerializedHubMessage.cs @@ -0,0 +1,191 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Protocol; + +namespace Microsoft.AspNetCore.SignalR.Internal; + +/// +/// Represents a serialization cache for a single message. +/// +internal class SerializedHubMessage +{ + private SerializedMessage _cachedItem1; + private SerializedMessage _cachedItem2; + private List? _cachedItems; + private readonly object _lock = new object(); + + /// + /// Gets the hub message for the serialization cache. + /// + public HubMessage? Message { get; } + + /// + /// Initializes a new instance of the class. + /// + /// A collection of already serialized messages to cache. + public SerializedHubMessage(IReadOnlyList messages) + { + // A lock isn't needed here because nobody has access to this type until the constructor finishes. + for (var i = 0; i < messages.Count; i++) + { + var message = messages[i]; + SetCacheUnsynchronized(message.ProtocolName, message.Serialized); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The hub message for the cache. This will be serialized with an in to get the message's serialized representation. + public SerializedHubMessage(HubMessage message) + { + Message = message; + } + + /// + /// Gets the serialized representation of the using the specified . + /// + /// The protocol used to create the serialized representation. + /// The serialized representation of the . + public ReadOnlyMemory GetSerializedMessage(IHubProtocol protocol) + { + lock (_lock) + { + if (!TryGetCachedUnsynchronized(protocol.Name, out var serialized)) + { + if (Message == null) + { + throw new InvalidOperationException( + "This message was received from another server that did not have the requested protocol available."); + } + + serialized = protocol.GetMessageBytes(Message); + SetCacheUnsynchronized(protocol.Name, serialized); + } + + return serialized; + } + } + + // Used for unit testing. + internal IReadOnlyList GetAllSerializations() + { + // Even if this is only used in tests, let's do it right. + lock (_lock) + { + if (_cachedItem1.ProtocolName == null) + { + return Array.Empty(); + } + + var list = new List(2); + list.Add(_cachedItem1); + + if (_cachedItem2.ProtocolName != null) + { + list.Add(_cachedItem2); + + if (_cachedItems != null) + { + list.AddRange(_cachedItems); + } + } + + return list; + } + } + + private void SetCacheUnsynchronized(string protocolName, ReadOnlyMemory serialized) + { + // We set the fields before moving on to the list, if we need it to hold more than 2 items. + // We have to read/write these fields under the lock because the structs might tear and another + // thread might observe them half-assigned + + if (_cachedItem1.ProtocolName == null) + { + _cachedItem1 = new SerializedMessage(protocolName, serialized); + } + else if (_cachedItem2.ProtocolName == null) + { + _cachedItem2 = new SerializedMessage(protocolName, serialized); + } + else + { + if (_cachedItems == null) + { + _cachedItems = new List(); + } + + foreach (var item in _cachedItems) + { + if (string.Equals(item.ProtocolName, protocolName, StringComparison.Ordinal)) + { + // No need to add + return; + } + } + + _cachedItems.Add(new SerializedMessage(protocolName, serialized)); + } + } + + private bool TryGetCachedUnsynchronized(string protocolName, out ReadOnlyMemory result) + { + if (string.Equals(_cachedItem1.ProtocolName, protocolName, StringComparison.Ordinal)) + { + result = _cachedItem1.Serialized; + return true; + } + + if (string.Equals(_cachedItem2.ProtocolName, protocolName, StringComparison.Ordinal)) + { + result = _cachedItem2.Serialized; + return true; + } + + if (_cachedItems != null) + { + foreach (var serializedMessage in _cachedItems) + { + if (string.Equals(serializedMessage.ProtocolName, protocolName, StringComparison.Ordinal)) + { + result = serializedMessage.Serialized; + return true; + } + } + } + + result = default; + return false; + } +} + +internal readonly struct SerializedMessage +{ + /// + /// Gets the protocol of the serialized message. + /// + public string ProtocolName { get; } + + /// + /// Gets the serialized representation of the message. + /// + public ReadOnlyMemory Serialized { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The protocol of the serialized message. + /// The serialized representation of the message. + public SerializedMessage(string protocolName, ReadOnlyMemory serialized) + { + ProtocolName = protocolName; + Serialized = serialized; + } +} diff --git a/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj b/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj index 65cf5d649c19..e0cd9f43d95b 100644 --- a/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj +++ b/src/SignalR/clients/csharp/Client.Core/src/Microsoft.AspNetCore.SignalR.Client.Core.csproj @@ -14,6 +14,7 @@ + diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs index c3d34fa616de..966ed454e54a 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Http; +using System.Net.WebSockets; using System.Text.Json; using System.Threading.Channels; using Microsoft.AspNetCore.Connections; @@ -2537,6 +2538,186 @@ public async Task ServerSentEventsWorksWithHttp2OnlyEndpoint() } } + [Fact] + public async Task CanReconnectAndSendMessageWhileDisconnected() + { + var protocol = HubProtocols["json"]; + await using (var server = await StartServer(w => w.EventId.Name == "ReceivedUnexpectedResponse")) + { + var websocket = new ClientWebSocket(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + tcs.SetResult(); + + const string originalMessage = "SignalR"; + var connectionBuilder = new HubConnectionBuilder() + .WithLoggerFactory(LoggerFactory) + .WithUrl(server.Url + "/default", HttpTransportType.WebSockets, o => + { + o.WebSocketFactory = async (context, token) => + { + await tcs.Task; + await websocket.ConnectAsync(context.Uri, token); + tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + return websocket; + }; + o.UseAcks = true; + }); + connectionBuilder.Services.AddSingleton(protocol); + var connection = connectionBuilder.Build(); + + try + { + await connection.StartAsync().DefaultTimeout(); + var originalConnectionId = connection.ConnectionId; + + var result = await connection.InvokeAsync(nameof(TestHub.Echo), originalMessage).DefaultTimeout(); + + Assert.Equal(originalMessage, result); + + var originalWebsocket = websocket; + websocket = new ClientWebSocket(); + originalWebsocket.Dispose(); + + var resultTask = connection.InvokeAsync(nameof(TestHub.Echo), originalMessage).DefaultTimeout(); + tcs.SetResult(); + result = await resultTask; + + Assert.Equal(originalMessage, result); + Assert.Equal(originalConnectionId, connection.ConnectionId); + } + catch (Exception ex) + { + LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); + throw; + } + finally + { + await connection.DisposeAsync().DefaultTimeout(); + } + } + } + + [Fact] + public async Task CanReconnectAndSendMessageOnceConnected() + { + var protocol = HubProtocols["json"]; + await using (var server = await StartServer(w => w.EventId.Name == "ReceivedUnexpectedResponse")) + { + var websocket = new ClientWebSocket(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + const string originalMessage = "SignalR"; + var connectionBuilder = new HubConnectionBuilder() + .WithLoggerFactory(LoggerFactory) + .WithUrl(server.Url + "/default", HttpTransportType.WebSockets, o => + { + o.WebSocketFactory = async (context, token) => + { + await websocket.ConnectAsync(context.Uri, token); + tcs.SetResult(); + return websocket; + }; + o.UseAcks = true; + }) + .WithAutomaticReconnect(); + connectionBuilder.Services.AddSingleton(protocol); + var connection = connectionBuilder.Build(); + + var reconnectCalled = false; + connection.Reconnecting += ex => + { + reconnectCalled = true; + return Task.CompletedTask; + }; + + try + { + await connection.StartAsync().DefaultTimeout(); + await tcs.Task; + tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var originalConnectionId = connection.ConnectionId; + + var result = await connection.InvokeAsync(nameof(TestHub.Echo), originalMessage).DefaultTimeout(); + + Assert.Equal(originalMessage, result); + + var originalWebsocket = websocket; + websocket = new ClientWebSocket(); + + originalWebsocket.Dispose(); + + await tcs.Task.DefaultTimeout(); + result = await connection.InvokeAsync(nameof(TestHub.Echo), originalMessage).DefaultTimeout(); + + Assert.Equal(originalMessage, result); + Assert.Equal(originalConnectionId, connection.ConnectionId); + Assert.False(reconnectCalled); + } + catch (Exception ex) + { + LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); + throw; + } + finally + { + await connection.DisposeAsync().DefaultTimeout(); + } + } + } + + [Fact] + public async Task ServerAbortsConnectionWithAckingEnabledNoReconnectAttempted() + { + var protocol = HubProtocols["json"]; + await using (var server = await StartServer()) + { + var connectCount = 0; + var connectionBuilder = new HubConnectionBuilder() + .WithLoggerFactory(LoggerFactory) + .WithUrl(server.Url + "/default", HttpTransportType.WebSockets, o => + { + o.WebSocketFactory = async (context, token) => + { + connectCount++; + var ws = new ClientWebSocket(); + await ws.ConnectAsync(context.Uri, token); + return ws; + }; + o.UseAcks = true; + }); + connectionBuilder.Services.AddSingleton(protocol); + var connection = connectionBuilder.Build(); + + var closedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + connection.Closed += ex => + { + closedTcs.SetResult(ex); + return Task.CompletedTask; + }; + + try + { + await connection.StartAsync().DefaultTimeout(); + + await connection.SendAsync(nameof(TestHub.Abort)).DefaultTimeout(); + + Assert.Null(await closedTcs.Task.DefaultTimeout()); + Assert.Equal(HubConnectionState.Disconnected, connection.State); + Assert.Equal(1, connectCount); + } + catch (Exception ex) + { + LoggerFactory.CreateLogger().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName); + throw; + } + finally + { + await connection.DisposeAsync().DefaultTimeout(); + } + } + } + private class OneAtATimeSynchronizationContext : SynchronizationContext, IAsyncDisposable { private readonly Channel<(SendOrPostCallback, object)> _taskQueue = Channel.CreateUnbounded<(SendOrPostCallback, object)>(); diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs index 0b270d5e4a4b..fdb2f56c175a 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs @@ -101,6 +101,11 @@ public string GetHttpProtocol() return Context.GetHttpContext()?.Request?.Protocol ?? "unknown"; } + public void Abort() + { + Context.Abort(); + } + public async Task CallWithUnserializableObject() { await Clients.All.SendAsync("Foo", Unserializable.Create()); diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionFactoryTests.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionFactoryTests.cs index bc9952577264..f019ec2a749b 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionFactoryTests.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionFactoryTests.cs @@ -100,6 +100,7 @@ public void ShallowCopyHttpConnectionOptionsCopiesAllPublicProperties() { $"{nameof(HttpConnectionOptions.WebSocketFactory)}", webSocketFactory }, { $"{nameof(HttpConnectionOptions.ApplicationMaxBufferSize)}", 1L * 1024 * 1024 }, { $"{nameof(HttpConnectionOptions.TransportMaxBufferSize)}", 1L * 1024 * 1024 }, + { $"{nameof(HttpConnectionOptions.UseAcks)}", true }, }; var options = new HttpConnectionOptions(); diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Negotiate.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Negotiate.cs index 810633c73587..ba621a57e32f 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Negotiate.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Negotiate.cs @@ -508,7 +508,7 @@ public async Task StartSkipsOverTransportsThatTheClientDoesNotUnderstand() var transportFactory = new Mock(MockBehavior.Strict); - transportFactory.Setup(t => t.CreateTransport(HttpTransportType.LongPolling)) + transportFactory.Setup(t => t.CreateTransport(HttpTransportType.LongPolling, false)) .Returns(new TestTransport(transferFormat: TransferFormat.Text | TransferFormat.Binary)); using (var noErrorScope = new VerifyNoErrorsScope()) @@ -523,7 +523,7 @@ await WithConnectionAsync( } [Fact] - public async Task StartSkipsOverTransportsThatDoNotSupportTheRequredTransferFormat() + public async Task StartSkipsOverTransportsThatDoNotSupportTheRequiredTransferFormat() { var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false); @@ -557,7 +557,7 @@ public async Task StartSkipsOverTransportsThatDoNotSupportTheRequredTransferForm var transportFactory = new Mock(MockBehavior.Strict); - transportFactory.Setup(t => t.CreateTransport(HttpTransportType.LongPolling)) + transportFactory.Setup(t => t.CreateTransport(HttpTransportType.LongPolling, false)) .Returns(new TestTransport(transferFormat: TransferFormat.Text | TransferFormat.Binary)); await WithConnectionAsync( diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransportFactory.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransportFactory.cs index 2923c387e755..07491c809d7b 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransportFactory.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransportFactory.cs @@ -15,7 +15,7 @@ public TestTransportFactory(ITransport transport) _transport = transport; } - public ITransport CreateTransport(HttpTransportType availableServerTransports) + public ITransport CreateTransport(HttpTransportType availableServerTransports, bool useAck) { return _transport; } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.Log.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.Log.cs index 7d54757cec6b..4a8f93d88594 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.Log.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.Log.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using System.Net; using Microsoft.AspNetCore.Connections; using Microsoft.Extensions.Logging; @@ -45,7 +46,7 @@ public static void StartingTransport(ILogger logger, HttpTransportType transport public static partial void EstablishingConnection(ILogger logger, Uri url); [LoggerMessage(9, LogLevel.Debug, "Established connection '{ConnectionId}' with the server.", EventName = "Established")] - public static partial void ConnectionEstablished(ILogger logger, string connectionId); + public static partial void ConnectionEstablished(ILogger logger, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); [LoggerMessage(10, LogLevel.Error, "Failed to start connection. Error getting negotiation response from '{Url}'.", EventName = "ErrorWithNegotiation")] public static partial void ErrorWithNegotiation(ILogger logger, Uri url, Exception exception); diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs index 1d56c314f784..22c250f865f7 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs @@ -6,12 +6,12 @@ using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using System.Linq; -using System.Net; using System.Net.Http; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Abstractions; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Connections.Client.Internal; using Microsoft.AspNetCore.Http.Features; @@ -313,7 +313,7 @@ private async Task SelectAndStartTransport(TransferFormat transferFormat, Cancel if (_httpConnectionOptions.Transports == HttpTransportType.WebSockets) { Log.StartingTransport(_logger, _httpConnectionOptions.Transports, uri); - await StartTransport(uri, _httpConnectionOptions.Transports, transferFormat, cancellationToken).ConfigureAwait(false); + await StartTransport(uri, _httpConnectionOptions.Transports, transferFormat, cancellationToken, useAck: false).ConfigureAwait(false); } else { @@ -398,12 +398,14 @@ private async Task SelectAndStartTransport(TransferFormat transferFormat, Cancel // The negotiation response gets cleared in the fallback scenario. if (negotiationResponse == null) { + // Temporary until other transports work + _httpConnectionOptions.UseAcks = transportType == HttpTransportType.WebSockets ? _httpConnectionOptions.UseAcks : false; negotiationResponse = await GetNegotiationResponseAsync(uri, cancellationToken).ConfigureAwait(false); connectUrl = CreateConnectUrl(uri, negotiationResponse.ConnectionToken); } Log.StartingTransport(_logger, transportType, uri); - await StartTransport(connectUrl, transportType, transferFormat, cancellationToken).ConfigureAwait(false); + await StartTransport(connectUrl, transportType, transferFormat, cancellationToken, negotiationResponse.UseAcking).ConfigureAwait(false); break; } } @@ -455,6 +457,11 @@ private async Task NegotiateAsync(Uri url, HttpClient httpC uri = Utils.AppendQueryString(urlBuilder.Uri, $"negotiateVersion={_protocolVersionNumber}"); } + if (_httpConnectionOptions.UseAcks) + { + uri = Utils.AppendQueryString(uri, "useAck=true"); + } + using (var request = new HttpRequestMessage(HttpMethod.Post, uri)) { #if NET5_0_OR_GREATER @@ -500,10 +507,10 @@ private static Uri CreateConnectUrl(Uri url, string? connectionId) return Utils.AppendQueryString(url, $"id={connectionId}"); } - private async Task StartTransport(Uri connectUrl, HttpTransportType transportType, TransferFormat transferFormat, CancellationToken cancellationToken) + private async Task StartTransport(Uri connectUrl, HttpTransportType transportType, TransferFormat transferFormat, CancellationToken cancellationToken, bool useAck) { // Construct the transport - var transport = _transportFactory.CreateTransport(transportType); + var transport = _transportFactory.CreateTransport(transportType, useAck); // Start the transport, giving it one end of the pipe try @@ -524,6 +531,11 @@ private async Task StartTransport(Uri connectUrl, HttpTransportType transportTyp // We successfully started, set the transport properties (we don't want to set these until the transport is definitely running). _transport = transport; + if (useAck && _transport is IReconnectFeature reconnectFeature) + { + Features.Set(reconnectFeature); + } + Log.TransportStarted(_logger, transportType); } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnectionFactory.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnectionFactory.cs index 03b52fa385a4..b5f2b035e071 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnectionFactory.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnectionFactory.cs @@ -88,7 +88,8 @@ internal static HttpConnectionOptions ShallowCopyHttpConnectionOptions(HttpConne CloseTimeout = options.CloseTimeout, DefaultTransferFormat = options.DefaultTransferFormat, ApplicationMaxBufferSize = options.ApplicationMaxBufferSize, - TransportMaxBufferSize = options.TransportMaxBufferSize + TransportMaxBufferSize = options.TransportMaxBufferSize, + UseAcks = options.UseAcks, }; if (!OperatingSystem.IsBrowser()) diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnectionOptions.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnectionOptions.cs index 443dc2307786..c7a2ae78ed07 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnectionOptions.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnectionOptions.cs @@ -275,6 +275,16 @@ public Action? WebSocketConfiguration } } + /// + /// Setting to enable acking bytes sent between client and server, this allows reconnecting that preserves messages sent while disconnected. + /// Also preserves the when the reconnect is successful. + /// + /// + /// Only works with WebSockets transport currently. + /// API likely to change in future previews. + /// + public bool UseAcks { get; set; } + private static void ThrowIfUnsupportedPlatform() { if (OperatingSystem.IsBrowser()) diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/DefaultTransportFactory.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/DefaultTransportFactory.cs index 3a99961b41bd..5b37a0c561a4 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/DefaultTransportFactory.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/DefaultTransportFactory.cs @@ -31,13 +31,13 @@ public DefaultTransportFactory(HttpTransportType requestedTransportType, ILogger _accessTokenProvider = accessTokenProvider; } - public ITransport CreateTransport(HttpTransportType availableServerTransports) + public ITransport CreateTransport(HttpTransportType availableServerTransports, bool useAck) { if (_websocketsSupported && (availableServerTransports & HttpTransportType.WebSockets & _requestedTransportType) == HttpTransportType.WebSockets) { try { - return new WebSocketsTransport(_httpConnectionOptions, _loggerFactory, _accessTokenProvider, _httpClient); + return new WebSocketsTransport(_httpConnectionOptions, _loggerFactory, _accessTokenProvider, _httpClient, useAck); } catch (PlatformNotSupportedException ex) { @@ -49,13 +49,13 @@ public ITransport CreateTransport(HttpTransportType availableServerTransports) if ((availableServerTransports & HttpTransportType.ServerSentEvents & _requestedTransportType) == HttpTransportType.ServerSentEvents) { // We don't need to give the transport the accessTokenProvider because the HttpClient has a message handler that does the work for us. - return new ServerSentEventsTransport(_httpClient!, _httpConnectionOptions, _loggerFactory); + return new ServerSentEventsTransport(_httpClient!, _httpConnectionOptions, _loggerFactory, useAck); } if ((availableServerTransports & HttpTransportType.LongPolling & _requestedTransportType) == HttpTransportType.LongPolling) { // We don't need to give the transport the accessTokenProvider because the HttpClient has a message handler that does the work for us. - return new LongPollingTransport(_httpClient!, _httpConnectionOptions, _loggerFactory); + return new LongPollingTransport(_httpClient!, _httpConnectionOptions, _loggerFactory, useAck); } throw new InvalidOperationException("No requested transports available on the server."); diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ITransportFactory.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ITransportFactory.cs index cad87cd31772..e9779ba01fa8 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ITransportFactory.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ITransportFactory.cs @@ -5,5 +5,5 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal; internal interface ITransportFactory { - ITransport CreateTransport(HttpTransportType availableServerTransports); + ITransport CreateTransport(HttpTransportType availableServerTransports, bool useAck); } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/LongPollingTransport.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/LongPollingTransport.cs index 8a14b77ba3e0..66d2a02c015b 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/LongPollingTransport.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/LongPollingTransport.cs @@ -19,6 +19,7 @@ internal sealed partial class LongPollingTransport : ITransport private readonly HttpClient _httpClient; private readonly ILogger _logger; private readonly HttpConnectionOptions _httpConnectionOptions; + private readonly bool _useAck; private IDuplexPipe? _application; private IDuplexPipe? _transport; // Volatile so that the poll loop sees the updated value set from a different thread @@ -32,11 +33,12 @@ internal sealed partial class LongPollingTransport : ITransport public PipeWriter Output => _transport!.Output; - public LongPollingTransport(HttpClient httpClient, HttpConnectionOptions? httpConnectionOptions = null, ILoggerFactory? loggerFactory = null) + public LongPollingTransport(HttpClient httpClient, HttpConnectionOptions? httpConnectionOptions = null, ILoggerFactory? loggerFactory = null, bool useAck = false) { _httpClient = httpClient; _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); _httpConnectionOptions = httpConnectionOptions ?? new(); + _useAck = useAck; } public async Task StartAsync(Uri url, TransferFormat transferFormat, CancellationToken cancellationToken = default) diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsTransport.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsTransport.cs index 25cbce7ba4a2..d6758849df4f 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsTransport.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/ServerSentEventsTransport.cs @@ -26,6 +26,7 @@ internal sealed partial class ServerSentEventsTransport : ITransport private readonly CancellationTokenSource _transportCts = new CancellationTokenSource(); private readonly CancellationTokenSource _inputCts = new CancellationTokenSource(); private readonly ServerSentEventsMessageParser _parser = new ServerSentEventsMessageParser(); + private readonly bool _useAck; private IDuplexPipe? _transport; private IDuplexPipe? _application; @@ -35,10 +36,11 @@ internal sealed partial class ServerSentEventsTransport : ITransport public PipeWriter Output => _transport!.Output; - public ServerSentEventsTransport(HttpClient httpClient, HttpConnectionOptions? httpConnectionOptions = null, ILoggerFactory? loggerFactory = null) + public ServerSentEventsTransport(HttpClient httpClient, HttpConnectionOptions? httpConnectionOptions = null, ILoggerFactory? loggerFactory = null, bool useAck = false) { ArgumentNullThrowHelper.ThrowIfNull(httpClient); + _useAck = useAck; _httpClient = httpClient; _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); _httpConnectionOptions = httpConnectionOptions ?? new(); diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.Log.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.Log.cs index 1540844ec4b0..8792a85bd2a9 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.Log.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.Log.cs @@ -72,5 +72,11 @@ private static partial class Log [LoggerMessage(20, LogLevel.Warning, $"Configuring request headers using {nameof(HttpConnectionOptions)}.{nameof(HttpConnectionOptions.Headers)} is not supported when using websockets transport " + "on the browser platform.", EventName = "HeadersNotSupported")] public static partial void HeadersNotSupported(ILogger logger); + + [LoggerMessage(21, LogLevel.Debug, "Receive loop errored.", EventName = "ReceiveErrored")] + public static partial void ReceiveErrored(ILogger logger, Exception exception); + + [LoggerMessage(22, LogLevel.Debug, "Send loop errored.", EventName = "SendErrored")] + public static partial void SendErrored(ILogger logger, Exception exception); } } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs index 9c3d8184fc26..578ff3460bc5 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/WebSocketsTransport.cs @@ -2,24 +2,30 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Diagnostics; +using System.IO; using System.IO.Pipelines; using System.Net; using System.Net.Http; +using System.Net.Sockets; using System.Net.WebSockets; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; +using System.Text; using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Abstractions; using Microsoft.AspNetCore.Shared; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using static System.IO.Pipelines.DuplexPipe; namespace Microsoft.AspNetCore.Http.Connections.Client.Internal; -internal sealed partial class WebSocketsTransport : ITransport +internal sealed partial class WebSocketsTransport : ITransport, IReconnectFeature { private WebSocket? _webSocket; private IDuplexPipe? _application; @@ -29,9 +35,14 @@ internal sealed partial class WebSocketsTransport : ITransport private volatile bool _aborted; private readonly HttpConnectionOptions _httpConnectionOptions; private readonly HttpClient? _httpClient; - private readonly CancellationTokenSource _stopCts = new CancellationTokenSource(); + private CancellationTokenSource _stopCts = default!; + private readonly bool _useAck; private IDuplexPipe? _transport; + // Used for reconnect (when enabled) to determine if the close was ungraceful or not, reconnect only happens on ungraceful disconnect + // The assumption is that a graceful close was triggered purposefully by either the client or server and a reconnect shouldn't occur + private bool _gracefulClose; + private Action? _notifyOnReconnect; internal Task Running { get; private set; } = Task.CompletedTask; @@ -39,8 +50,12 @@ internal sealed partial class WebSocketsTransport : ITransport public PipeWriter Output => _transport!.Output; - public WebSocketsTransport(HttpConnectionOptions httpConnectionOptions, ILoggerFactory loggerFactory, Func> accessTokenProvider, HttpClient? httpClient) + public Action NotifyOnReconnect { get => _notifyOnReconnect is not null ? _notifyOnReconnect : () => { }; set => _notifyOnReconnect = value; } + + public WebSocketsTransport(HttpConnectionOptions httpConnectionOptions, ILoggerFactory loggerFactory, Func> accessTokenProvider, HttpClient? httpClient, + bool useAck = false) { + _useAck = useAck; _logger = (loggerFactory ?? NullLoggerFactory.Instance).CreateLogger(); _httpConnectionOptions = httpConnectionOptions ?? new HttpConnectionOptions(); @@ -278,18 +293,29 @@ public async Task StartAsync(Uri url, TransferFormat transferFormat, Cancellatio Log.StartedTransport(_logger); - // Create the pipe pair (Application's writer is connected to Transport's reader, and vice versa) - var pair = DuplexPipe.CreateConnectionPair(_httpConnectionOptions.TransportPipeOptions, _httpConnectionOptions.AppPipeOptions); + _stopCts = new CancellationTokenSource(); + + var ignoreFirstCanceled = false; - _transport = pair.Transport; - _application = pair.Application; + if (_transport is null) + { + // Create the pipe pair (Application's writer is connected to Transport's reader, and vice versa) + var pair = CreateConnectionPair(_httpConnectionOptions.TransportPipeOptions, _httpConnectionOptions.AppPipeOptions); + + _transport = pair.Transport; + _application = pair.Application; + } + else + { + ignoreFirstCanceled = true; + } // TODO: Handle TCP connection errors // https://github.com/SignalR/SignalR/blob/1fba14fa3437e24c204dfaf8a18db3fce8acad3c/src/Microsoft.AspNet.SignalR.Core/Owin/WebSockets/WebSocketHandler.cs#L248-L251 - Running = ProcessSocketAsync(_webSocket); + Running = ProcessSocketAsync(_webSocket, url, ignoreFirstCanceled); } - private async Task ProcessSocketAsync(WebSocket socket) + private async Task ProcessSocketAsync(WebSocket socket, Uri url, bool ignoreFirstCanceled) { Debug.Assert(_application != null); @@ -297,7 +323,7 @@ private async Task ProcessSocketAsync(WebSocket socket) { // Begin sending and receiving. var receiving = StartReceiving(socket); - var sending = StartSending(socket); + var sending = StartSending(socket, ignoreFirstCanceled); // Wait for send or receive to complete var trigger = await Task.WhenAny(receiving, sending).ConfigureAwait(false); @@ -335,9 +361,18 @@ private async Task ProcessSocketAsync(WebSocket socket) socket.Abort(); // Cancel any pending flush so that we can quit - _application.Output.CancelPendingFlush(); + if (_gracefulClose) + { + _application.Output.CancelPendingFlush(); + } } } + + if (_useAck && !_gracefulClose) + { + UpdateConnectionPair(); + await StartAsync(url, _webSocketMessageType == WebSocketMessageType.Binary ? TransferFormat.Binary : TransferFormat.Text, default).ConfigureAwait(false); + } } private async Task StartReceiving(WebSocket socket) @@ -354,6 +389,7 @@ private async Task StartReceiving(WebSocket socket) if (result.MessageType == WebSocketMessageType.Close) { + _gracefulClose = true; Log.WebSocketClosed(_logger, socket.CloseStatus); if (socket.CloseStatus != WebSocketCloseStatus.NormalClosure) @@ -380,6 +416,7 @@ private async Task StartReceiving(WebSocket socket) // Need to check again for netstandard2.1 because a close can happen between a 0-byte read and the actual read if (receiveResult.MessageType == WebSocketMessageType.Close) { + _gracefulClose = true; Log.WebSocketClosed(_logger, socket.CloseStatus); if (socket.CloseStatus != WebSocketCloseStatus.NormalClosure) @@ -412,19 +449,30 @@ private async Task StartReceiving(WebSocket socket) { if (!_aborted) { - _application.Output.Complete(ex); + if (_gracefulClose) + { + _application.Output.Complete(ex); + } + else + { + // only logging in this case because the other case gets the exception flowed to application code + Log.ReceiveErrored(_logger, ex); + } } } finally { // We're done writing - _application.Output.Complete(); + if (_gracefulClose) + { + _application.Output.Complete(); + } Log.ReceiveStopped(_logger); } } - private async Task StartSending(WebSocket socket) + private async Task StartSending(WebSocket socket, bool ignoreFirstCanceled) { Debug.Assert(_application != null); @@ -441,11 +489,14 @@ private async Task StartSending(WebSocket socket) try { - if (result.IsCanceled) + if (result.IsCanceled && !ignoreFirstCanceled) { + _logger.LogInformation("send canceled"); break; } + ignoreFirstCanceled = false; + if (!buffer.IsEmpty) { try @@ -509,7 +560,17 @@ private async Task StartSending(WebSocket socket) } } - _application.Input.Complete(); + if (_gracefulClose) + { + _application.Input.Complete(error); + } + else + { + if (error is not null) + { + Log.SendErrored(_logger, error); + } + } Log.SendStopped(_logger); } @@ -539,6 +600,7 @@ private static Uri ResolveWebSocketsUrl(Uri url) public async Task StopAsync() { + _gracefulClose = true; Log.TransportStopping(_logger); if (_application == null) @@ -574,4 +636,23 @@ public async Task StopAsync() Log.TransportStopped(_logger, null); } + + private void UpdateConnectionPair() + { + var prevPipe = _application!.Input; + var input = new Pipe(_httpConnectionOptions.TransportPipeOptions); + + // Add new pipe for reading from and writing to transport from app code + var transportToApplication = new DuplexPipe(_transport!.Input, input.Writer); + var applicationToTransport = new DuplexPipe(input.Reader, _application!.Output); + + _application = applicationToTransport; + _transport = transportToApplication; + + // Close previous pipe with specific error that application code can catch to know a restart is occurring + prevPipe.Complete(new ConnectionResetException("")); + + Debug.Assert(_notifyOnReconnect is not null); + _notifyOnReconnect.Invoke(); + } } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/PublicAPI.Unshipped.txt b/src/SignalR/clients/csharp/Http.Connections.Client/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..80a34974298d 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/PublicAPI.Unshipped.txt +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions.UseAcks.get -> bool +Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionOptions.UseAcks.set -> void diff --git a/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs b/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs index 72b6838753a4..8ff9ba259ed1 100644 --- a/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs +++ b/src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs @@ -34,6 +34,8 @@ public static class NegotiateProtocol private static readonly JsonEncodedText ErrorPropertyNameBytes = JsonEncodedText.Encode(ErrorPropertyName); private const string NegotiateVersionPropertyName = "negotiateVersion"; private static readonly JsonEncodedText NegotiateVersionPropertyNameBytes = JsonEncodedText.Encode(NegotiateVersionPropertyName); + private const string AckPropertyName = "useAck"; + private static readonly JsonEncodedText AckPropertyNameBytes = JsonEncodedText.Encode(AckPropertyName); // Use C#7.3's ReadOnlySpan optimization for static data https://vcsjones.com/2019/02/01/csharp-readonly-span-bytes-static/ // Used to detect ASP.NET SignalR Server connection attempt @@ -64,6 +66,11 @@ public static void WriteResponse(NegotiationResponse response, IBufferWriter content) List? availableTransports = null; string? error = null; int version = 0; + bool useAck = false; var completed = false; while (!completed && reader.CheckRead()) @@ -206,6 +214,10 @@ public static NegotiationResponse ParseResponse(ReadOnlySpan content) { throw new InvalidOperationException("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details."); } + else if (reader.ValueTextEquals(AckPropertyNameBytes.EncodedUtf8Bytes)) + { + useAck = reader.ReadAsBoolean(AckPropertyName); + } else { reader.Skip(); @@ -249,7 +261,8 @@ public static NegotiationResponse ParseResponse(ReadOnlySpan content) AccessToken = accessToken, AvailableTransports = availableTransports, Error = error, - Version = version + Version = version, + UseAcking = useAck, }; } catch (Exception ex) diff --git a/src/SignalR/common/Http.Connections.Common/src/NegotiationResponse.cs b/src/SignalR/common/Http.Connections.Common/src/NegotiationResponse.cs index 17c838137f44..4ec99bba1055 100644 --- a/src/SignalR/common/Http.Connections.Common/src/NegotiationResponse.cs +++ b/src/SignalR/common/Http.Connections.Common/src/NegotiationResponse.cs @@ -44,4 +44,9 @@ public class NegotiationResponse /// An optional error during the negotiate. If this is not null the other properties on the response can be ignored. /// public string? Error { get; set; } + + /// + /// + /// + public bool UseAcking { get; set; } } diff --git a/src/SignalR/common/Http.Connections.Common/src/PublicAPI.Unshipped.txt b/src/SignalR/common/Http.Connections.Common/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..437066a1e801 100644 --- a/src/SignalR/common/Http.Connections.Common/src/PublicAPI.Unshipped.txt +++ b/src/SignalR/common/Http.Connections.Common/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Http.Connections.NegotiationResponse.UseAcking.get -> bool +Microsoft.AspNetCore.Http.Connections.NegotiationResponse.UseAcking.set -> void diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs index a74f17d6aad7..2430b49d8b89 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs @@ -3,14 +3,17 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using System.Security.Claims; using System.Security.Principal; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Abstractions; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Connections.Features; using Microsoft.AspNetCore.Http.Connections.Internal.Transports; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Timeouts; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -28,7 +31,8 @@ internal sealed partial class HttpConnectionContext : ConnectionContext, IHttpTransportFeature, IConnectionInherentKeepAliveFeature, IConnectionLifetimeFeature, - IConnectionLifetimeNotificationFeature + IConnectionLifetimeNotificationFeature, + IReconnectFeature { private readonly HttpConnectionDispatcherOptions _options; @@ -46,6 +50,7 @@ internal sealed partial class HttpConnectionContext : ConnectionContext, private CancellationTokenSource? _sendCts; private bool _activeSend; private long _startedSendTime; + private readonly bool _useAcks; private readonly object _sendingLock = new object(); internal CancellationToken SendingToken { get; private set; } @@ -57,7 +62,8 @@ internal sealed partial class HttpConnectionContext : ConnectionContext, /// Creates the DefaultConnectionContext without Pipes to avoid upfront allocations. /// The caller is expected to set the and pipes manually. /// - public HttpConnectionContext(string connectionId, string connectionToken, ILogger logger, MetricsContext metricsContext, IDuplexPipe transport, IDuplexPipe application, HttpConnectionDispatcherOptions options) + public HttpConnectionContext([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string connectionToken, ILogger logger, MetricsContext metricsContext, + IDuplexPipe transport, IDuplexPipe application, HttpConnectionDispatcherOptions options, bool useAcks) { Transport = transport; _applicationStream = new PipeWriterStream(application.Output); @@ -89,14 +95,22 @@ public HttpConnectionContext(string connectionId, string connectionToken, ILogge Features.Set(this); Features.Set(this); + if (useAcks) + { + Features.Set(this); + } + _connectionClosedTokenSource = new CancellationTokenSource(); ConnectionClosed = _connectionClosedTokenSource.Token; _connectionCloseRequested = new CancellationTokenSource(); ConnectionClosedRequested = _connectionCloseRequested.Token; AuthenticationExpiration = DateTimeOffset.MaxValue; + _useAcks = useAcks; } + public bool UseAcks => _useAcks; + public CancellationTokenSource? Cancellation { get; set; } public HttpTransportType TransportType { get; set; } @@ -113,7 +127,7 @@ public HttpConnectionContext(string connectionId, string connectionToken, ILogge internal bool IsAuthenticationExpirationEnabled => _options.CloseOnAuthenticationExpiration; - public Task? TransportTask { get; set; } + public Task? TransportTask { get; set; } public Task PreviousPollTask { get; set; } = Task.CompletedTask; @@ -189,6 +203,8 @@ public IDuplexPipe Application public CancellationToken ConnectionClosedRequested { get; set; } + public Action NotifyOnReconnect { get; set; } = () => { }; + public override void Abort() { ThreadPool.UnsafeQueueUserWorkItem(cts => ((CancellationTokenSource)cts!).Cancel(), _connectionClosedTokenSource); @@ -384,6 +400,7 @@ private async Task WaitOnTasks(Task applicationTask, Task transportTask, bool cl internal bool TryActivatePersistentConnection( ConnectionDelegate connectionDelegate, IHttpTransport transport, + Task currentRequestTask, HttpContext context, ILogger dispatcherLogger) { @@ -393,12 +410,16 @@ internal bool TryActivatePersistentConnection( { Status = HttpConnectionStatus.Active; + PreviousPollTask = currentRequestTask; + // Call into the end point passing the connection - ApplicationTask = ExecuteApplication(connectionDelegate); + ApplicationTask ??= ExecuteApplication(connectionDelegate); // Start the transport TransportTask = transport.ProcessRequestAsync(context, context.RequestAborted); + context.Features.Get()?.DisableTimeout(); + return true; } else @@ -440,7 +461,12 @@ public bool TryActivateLongPollingConnection( // On the first poll, we flush the response immediately to mark the poll as "initialized" so future // requests can be made safely - TransportTask = nonClonedContext.Response.Body.FlushAsync(); + TransportTask = Func(); + async Task Func() + { + await nonClonedContext.Response.Body.FlushAsync(); + return false; + }; } else { @@ -520,6 +546,14 @@ internal async Task CancelPreviousPoll(HttpContext context) // Cancel the previous request cts?.Cancel(); + // TODO: remove transport check once other transports support acks + if (UseAcks && TransportType == HttpTransportType.WebSockets) + { + // Break transport send loop in case it's still waiting on reading from the application + Application.Input.CancelPendingRead(); + UpdateConnectionPair(); + } + try { // Wait for the previous request to drain @@ -620,6 +654,23 @@ public void RequestClose() ThreadPool.UnsafeQueueUserWorkItem(static cts => ((CancellationTokenSource)cts!).Cancel(), _connectionCloseRequested); } + private void UpdateConnectionPair() + { + var prevPipe = Application.Input; + var input = new Pipe(_options.TransportPipeOptions); + + // Add new pipe for reading from and writing to transport from app code + var transportToApplication = new DuplexPipe(Transport.Input, input.Writer); + var applicationToTransport = new DuplexPipe(input.Reader, Application.Output); + + Application = applicationToTransport; + Transport = transportToApplication; + + // Close previous pipe with specific error that application code can catch to know a restart is occurring + prevPipe.Complete(new ConnectionResetException("")); + Features.GetRequiredFeature().NotifyOnReconnect.Invoke(); + } + private static partial class Log { [LoggerMessage(1, LogLevel.Trace, "Disposing connection {TransportConnectionId}.", EventName = "DisposingConnection")] diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs index de9fc73186df..de6cc55921a0 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs @@ -153,67 +153,74 @@ private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connecti // We only need to provide the Input channel since writing to the application is handled through /send. var sse = new ServerSentEventsServerTransport(connection.Application.Input, connection.ConnectionId, connection, _loggerFactory); - await DoPersistentConnection(connectionDelegate, sse, context, connection); - } - else if (context.WebSockets.IsWebSocketRequest) - { - // Connection can be established lazily - var connection = await GetOrCreateConnectionAsync(context, options); - if (connection == null) - { - // No such connection, GetOrCreateConnection already set the response status code - return; - } - - if (!await EnsureConnectionStateAsync(connection, context, HttpTransportType.WebSockets, supportedTransports, logScope)) + if (connection.TryActivatePersistentConnection(connectionDelegate, sse, Task.CompletedTask, context, _logger)) { - // Bad connection state. It's already set the response status code. - return; + await DoPersistentConnection(connection); } - - Log.EstablishedConnection(_logger); - - // Allow the reads to be canceled - connection.Cancellation = new CancellationTokenSource(); - - var ws = new WebSocketsServerTransport(options.WebSockets, connection.Application, connection, _loggerFactory); - - await DoPersistentConnection(connectionDelegate, ws, context, connection); } else { - // GET /{path} maps to long polling + // GET /{path} maps to long polling or WebSockets - AddNoCacheHeaders(context.Response); + HttpConnectionContext? connection; + var transport = HttpTransportType.LongPolling; + if (context.WebSockets.IsWebSocketRequest) + { + transport = HttpTransportType.WebSockets; + connection = await GetOrCreateConnectionAsync(context, options); + } + else + { + AddNoCacheHeaders(context.Response); + // Connection must already exist + connection = await GetConnectionAsync(context); + } - // Connection must already exist - var connection = await GetConnectionAsync(context); if (connection == null) { // No such connection, GetConnection already set the response status code return; } - if (!await EnsureConnectionStateAsync(connection, context, HttpTransportType.LongPolling, supportedTransports, logScope)) + if (!await EnsureConnectionStateAsync(connection, context, transport, supportedTransports, logScope)) { // Bad connection state. It's already set the response status code. return; } - if (!await connection.CancelPreviousPoll(context)) + if (connection.TransportType != HttpTransportType.WebSockets || connection.UseAcks) { - // Connection closed. It's already set the response status code. - return; + if (!await connection.CancelPreviousPoll(context)) + { + // Connection closed. It's already set the response status code. + return; + } } // Create a new Tcs every poll to keep track of the poll finishing, so we can properly wait on previous polls var currentRequestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - if (!connection.TryActivateLongPollingConnection( - connectionDelegate, context, options.LongPolling.PollTimeout, - currentRequestTcs.Task, _loggerFactory, _logger)) + switch (transport) { - return; + case HttpTransportType.None: + break; + case HttpTransportType.WebSockets: + var ws = new WebSocketsServerTransport(options.WebSockets, connection.Application, connection, _loggerFactory); + if (!connection.TryActivatePersistentConnection(connectionDelegate, ws, currentRequestTcs.Task, context, _logger)) + { + return; + } + break; + case HttpTransportType.LongPolling: + if (!connection.TryActivateLongPollingConnection( + connectionDelegate, context, options.LongPolling.PollTimeout, + currentRequestTcs.Task, _loggerFactory, _logger)) + { + return; + } + break; + default: + break; } context.Features.Get()?.DisableTimeout(); @@ -244,8 +251,15 @@ private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connecti } else { - // Only allow repoll if we aren't removing the connection. - connection.MarkInactive(); + if (transport != HttpTransportType.LongPolling) + { + await _manager.DisposeAndRemoveAsync(connection, closeGracefully: false, HttpConnectionStopStatus.NormalClosure); + } + else + { + // Only allow repoll if we aren't removing the connection. + connection.MarkInactive(); + } } } else if (resultTask.IsFaulted || resultTask.IsCanceled) @@ -258,8 +272,18 @@ private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connecti } else { - // Only allow repoll if we aren't removing the connection. - connection.MarkInactive(); + // If false then the transport was ungracefully closed, this can mean a temporary network disconnection + // We'll mark the connection as inactive and allow the connection to reconnect if that's the case. + // TODO: If acks aren't enabled we can close the connection immediately (not LongPolling) + if (await connection.TransportTask!) + { + await _manager.DisposeAndRemoveAsync(connection, closeGracefully: true, HttpConnectionStopStatus.NormalClosure); + } + else + { + // Only allow repoll if we aren't removing the connection. + connection.MarkInactive(); + } } } finally @@ -271,19 +295,12 @@ private async Task ExecuteAsync(HttpContext context, ConnectionDelegate connecti } } - private async Task DoPersistentConnection(ConnectionDelegate connectionDelegate, - IHttpTransport transport, - HttpContext context, - HttpConnectionContext connection) + private async Task DoPersistentConnection(HttpConnectionContext connection) { - if (connection.TryActivatePersistentConnection(connectionDelegate, transport, context, _logger)) - { - context.Features.Get()?.DisableTimeout(); - // Wait for any of them to end - await Task.WhenAny(connection.ApplicationTask!, connection.TransportTask!); + // Wait for any of them to end + await Task.WhenAny(connection.ApplicationTask!, connection.TransportTask!); - await _manager.DisposeAndRemoveAsync(connection, closeGracefully: true, HttpConnectionStopStatus.NormalClosure); - } + await _manager.DisposeAndRemoveAsync(connection, closeGracefully: true, HttpConnectionStopStatus.NormalClosure); } private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatcherOptions options, ConnectionLogScope logScope) @@ -317,11 +334,18 @@ private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatche Log.NegotiateProtocolVersionMismatch(_logger, 0); } + var useAck = false; + if (context.Request.Query.TryGetValue("UseAck", out var useAckValue)) + { + var useAckStringValue = useAckValue.ToString(); + bool.TryParse(useAckStringValue, out useAck); + } + // Establish the connection HttpConnectionContext? connection = null; if (error == null) { - connection = CreateConnection(options, clientProtocolVersion); + connection = CreateConnection(options, clientProtocolVersion, useAck); } // Set the Connection ID on the logging scope so that logs from now on will have the @@ -334,7 +358,7 @@ private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatche try { // Get the bytes for the connection id - WriteNegotiatePayload(writer, connection?.ConnectionId, connection?.ConnectionToken, context, options, clientProtocolVersion, error); + WriteNegotiatePayload(writer, connection?.ConnectionId, connection?.ConnectionToken, context, options, clientProtocolVersion, error, useAck); Log.NegotiationRequest(_logger); @@ -349,7 +373,7 @@ private async Task ProcessNegotiate(HttpContext context, HttpConnectionDispatche } private static void WriteNegotiatePayload(IBufferWriter writer, string? connectionId, string? connectionToken, HttpContext context, HttpConnectionDispatcherOptions options, - int clientProtocolVersion, string? error) + int clientProtocolVersion, string? error, bool useAck) { var response = new NegotiationResponse(); @@ -364,6 +388,7 @@ private static void WriteNegotiatePayload(IBufferWriter writer, string? co response.ConnectionId = connectionId; response.ConnectionToken = connectionToken; response.AvailableTransports = new List(); + response.UseAcking = useAck; if ((options.Transports & HttpTransportType.WebSockets) != 0 && ServerHasWebSockets(context.Features)) { @@ -745,9 +770,9 @@ private static void CloneHttpContext(HttpContext context, HttpConnectionContext return connection; } - private HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions options, int clientProtocolVersion = 0) + private HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions options, int clientProtocolVersion = 0, bool useAck = false) { - return _manager.CreateConnection(options, clientProtocolVersion); + return _manager.CreateConnection(options, clientProtocolVersion, useAck); } private static void AddNoCacheHeaders(HttpResponse response) diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs index 3f1cce2edb6b..c970679841ce 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs @@ -4,13 +4,13 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO.Pipelines; using System.Net.WebSockets; using System.Security.Cryptography; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using static System.IO.Pipelines.DuplexPipe; namespace Microsoft.AspNetCore.Http.Connections.Internal; @@ -67,7 +67,7 @@ internal HttpConnectionContext CreateConnection() /// Creates a connection without Pipes setup to allow saving allocations until Pipes are needed. /// /// - internal HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions options, int negotiateVersion = 0) + internal HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions options, int negotiateVersion = 0, bool useAck = false) { string connectionToken; var id = MakeNewConnectionId(); @@ -90,8 +90,8 @@ internal HttpConnectionContext CreateConnection(HttpConnectionDispatcherOptions HttpConnectionsEventSource.Log.ConnectionStart(id); _metrics.ConnectionStart(metricsContext); - var pair = DuplexPipe.CreateConnectionPair(options.TransportPipeOptions, options.AppPipeOptions); - var connection = new HttpConnectionContext(id, connectionToken, _connectionLogger, metricsContext, pair.Application, pair.Transport, options); + var pair = CreateConnectionPair(options.TransportPipeOptions, options.AppPipeOptions); + var connection = new HttpConnectionContext(id, connectionToken, _connectionLogger, metricsContext, pair.Application, pair.Transport, options, useAck); _connections.TryAdd(connectionToken, (connection, startTimestamp)); diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionsEventSource.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionsEventSource.cs index a96d84664b1b..1278affe664d 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionsEventSource.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionsEventSource.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; namespace Microsoft.AspNetCore.Http.Connections.Internal; @@ -35,7 +36,7 @@ internal HttpConnectionsEventSource(string eventSourceName) // This has to go through NonEvent because only Primitive types are allowed // in function parameters for Events [NonEvent] - public void ConnectionStop(string connectionId, long startTimestamp, long currentTimestamp) + public void ConnectionStop([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, long startTimestamp, long currentTimestamp) { Interlocked.Increment(ref _connectionsStopped); Interlocked.Decrement(ref _currentConnections); @@ -53,7 +54,7 @@ public void ConnectionStop(string connectionId, long startTimestamp, long curren } [Event(eventId: 1, Level = EventLevel.Informational, Message = "Started connection '{0}'.")] - public void ConnectionStart(string connectionId) + public void ConnectionStart([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { Interlocked.Increment(ref _connectionsStarted); Interlocked.Increment(ref _currentConnections); @@ -65,10 +66,10 @@ public void ConnectionStart(string connectionId) } [Event(eventId: 2, Level = EventLevel.Informational, Message = "Stopped connection '{0}'.")] - private void ConnectionStop(string connectionId) => WriteEvent(2, connectionId); + private void ConnectionStop([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) => WriteEvent(2, connectionId); [Event(eventId: 3, Level = EventLevel.Informational, Message = "Connection '{0}' timed out.")] - public void ConnectionTimedOut(string connectionId) + public void ConnectionTimedOut([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { Interlocked.Increment(ref _connectionsTimedOut); diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionsMetrics.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionsMetrics.cs index 80807c5d0534..9637def5df6f 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionsMetrics.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionsMetrics.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Diagnostics.Metrics; -using Microsoft.Extensions.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics; namespace Microsoft.AspNetCore.Http.Connections.Internal; @@ -32,7 +32,7 @@ internal sealed class HttpConnectionsMetrics : IDisposable public HttpConnectionsMetrics(IMeterFactory meterFactory) { - _meter = meterFactory.CreateMeter(MeterName); + _meter = meterFactory.Create(MeterName); _currentConnectionsCounter = _meter.CreateUpDownCounter( "current-connections", diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/IHttpTransport.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/IHttpTransport.cs index af9ab19f4ef4..557c42c550e5 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/Transports/IHttpTransport.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/IHttpTransport.cs @@ -11,5 +11,5 @@ internal interface IHttpTransport /// /// /// A that completes when the transport has finished processing - Task ProcessRequestAsync(HttpContext context, CancellationToken token); + Task ProcessRequestAsync(HttpContext context, CancellationToken token); } diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/LongPollingServerTransport.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/LongPollingServerTransport.cs index e3e360328848..34251849d8d1 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/Transports/LongPollingServerTransport.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/LongPollingServerTransport.cs @@ -29,7 +29,7 @@ public LongPollingServerTransport(CancellationToken timeoutToken, PipeReader app _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Connections.Internal.Transports.LongPollingTransport"); } - public async Task ProcessRequestAsync(HttpContext context, CancellationToken token) + public async Task ProcessRequestAsync(HttpContext context, CancellationToken token) { try { @@ -43,7 +43,7 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationToken tok Log.LongPolling204(_logger); context.Response.ContentType = "text/plain"; context.Response.StatusCode = StatusCodes.Status204NoContent; - return; + return false; } // We're intentionally not checking cancellation here because we need to drain messages we've got so far, @@ -109,6 +109,7 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationToken tok context.Response.StatusCode = StatusCodes.Status500InternalServerError; throw; } + return false; } private static partial class Log diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/ServerSentEventsServerTransport.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/ServerSentEventsServerTransport.cs index 72c3c816b423..a7451543f856 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/Transports/ServerSentEventsServerTransport.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/ServerSentEventsServerTransport.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; @@ -14,11 +15,11 @@ internal sealed partial class ServerSentEventsServerTransport : IHttpTransport private readonly ILogger _logger; private readonly HttpConnectionContext? _connection; - public ServerSentEventsServerTransport(PipeReader application, string connectionId, ILoggerFactory loggerFactory) + public ServerSentEventsServerTransport(PipeReader application, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, ILoggerFactory loggerFactory) : this(application, connectionId, connection: null, loggerFactory) { } - public ServerSentEventsServerTransport(PipeReader application, string connectionId, HttpConnectionContext? connection, ILoggerFactory loggerFactory) + public ServerSentEventsServerTransport(PipeReader application, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, HttpConnectionContext? connection, ILoggerFactory loggerFactory) { _application = application; _connectionId = connectionId; @@ -28,7 +29,7 @@ public ServerSentEventsServerTransport(PipeReader application, string connection _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Connections.Internal.Transports.ServerSentEventsTransport"); } - public async Task ProcessRequestAsync(HttpContext context, CancellationToken cancellationToken) + public async Task ProcessRequestAsync(HttpContext context, CancellationToken cancellationToken) { context.Response.ContentType = "text/event-stream"; context.Response.Headers.CacheControl = "no-cache,no-store"; @@ -81,6 +82,8 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationToken can { // Closed connection } + + return true; } private static partial class Log diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.Log.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.Log.cs index 026285075b7d..d085cf8b0f84 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.Log.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.Log.cs @@ -51,5 +51,8 @@ private static partial class Log [LoggerMessage(15, LogLevel.Debug, "Closing webSocket failed.", EventName = "ClosingWebSocketFailed")] public static partial void ClosingWebSocketFailed(ILogger logger, Exception ex); + + [LoggerMessage(16, LogLevel.Debug, "Send loop errored.", EventName = "SendErrored")] + public static partial void SendErrored(ILogger logger, Exception exception); } } diff --git a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs index 3d473aa095b9..77892ab942dc 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/Transports/WebSocketsServerTransport.cs @@ -17,6 +17,9 @@ internal sealed partial class WebSocketsServerTransport : IHttpTransport private readonly HttpConnectionContext _connection; private volatile bool _aborted; + // Used to determine if the close was graceful or a network issue + private bool _gracefulClose; + public WebSocketsServerTransport(WebSocketOptions options, IDuplexPipe application, HttpConnectionContext connection, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(options); @@ -31,7 +34,7 @@ public WebSocketsServerTransport(WebSocketOptions options, IDuplexPipe applicati _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Http.Connections.Internal.Transports.WebSocketsTransport"); } - public async Task ProcessRequestAsync(HttpContext context, CancellationToken token) + public async Task ProcessRequestAsync(HttpContext context, CancellationToken token) { Debug.Assert(context.WebSockets.IsWebSocketRequest, "Not a websocket request"); @@ -50,13 +53,16 @@ public async Task ProcessRequestAsync(HttpContext context, CancellationToken tok Log.SocketClosed(_logger); } } + + return _gracefulClose; } public async Task ProcessSocketAsync(WebSocket socket) { - // Begin sending and receiving. Receiving must be started first because ExecuteAsync enables SendAsync. + var ignoreFirstCancel = false; + var receiving = StartReceiving(socket); - var sending = StartSending(socket); + var sending = StartSending(socket, ignoreFirstCancel); // Wait for send or receive to complete var trigger = await Task.WhenAny(receiving, sending); @@ -135,6 +141,7 @@ private async Task StartReceiving(WebSocket socket) if (result.MessageType == WebSocketMessageType.Close) { + _gracefulClose = true; return; } @@ -145,6 +152,7 @@ private async Task StartReceiving(WebSocket socket) // Need to check again for netcoreapp3.0 and later because a close can happen between a 0-byte read and the actual read if (receiveResult.MessageType == WebSocketMessageType.Close) { + _gracefulClose = true; return; } @@ -175,17 +183,21 @@ private async Task StartReceiving(WebSocket socket) { if (!_aborted && !token.IsCancellationRequested) { + _gracefulClose = true; _application.Output.Complete(ex); } } finally { - // We're done writing - _application.Output.Complete(); + if (_gracefulClose) + { + // We're done writing + _application.Output.Complete(); + } } } - private async Task StartSending(WebSocket socket) + private async Task StartSending(WebSocket socket, bool ignoreFirstCancel) { Exception? error = null; @@ -200,11 +212,13 @@ private async Task StartSending(WebSocket socket) try { - if (result.IsCanceled) + if (result.IsCanceled && !ignoreFirstCancel) { break; } + ignoreFirstCancel = false; + if (!buffer.IsEmpty) { try @@ -225,6 +239,12 @@ private async Task StartSending(WebSocket socket) break; } } + catch (OperationCanceledException ex) when (ex.CancellationToken == _connection.SendingToken) + { + _gracefulClose = true; + // TODO: probably log + break; + } catch (Exception ex) { if (!_aborted) @@ -266,7 +286,14 @@ private async Task StartSending(WebSocket socket) } } - _application.Input.Complete(); + if (_gracefulClose) + { + _application.Input.Complete(error); + } + else if (error is not null) + { + Log.SendErrored(_logger, error); + } } } diff --git a/src/SignalR/common/Http.Connections/src/Microsoft.AspNetCore.Http.Connections.csproj b/src/SignalR/common/Http.Connections/src/Microsoft.AspNetCore.Http.Connections.csproj index e6ee74ec7735..03cbb1beac26 100644 --- a/src/SignalR/common/Http.Connections/src/Microsoft.AspNetCore.Http.Connections.csproj +++ b/src/SignalR/common/Http.Connections/src/Microsoft.AspNetCore.Http.Connections.csproj @@ -36,6 +36,7 @@ + diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs index 9d16f32f4a6b..5440366363b6 100644 --- a/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs +++ b/src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs @@ -36,7 +36,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; -using Microsoft.Extensions.Metrics; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Tokens; @@ -1091,9 +1090,9 @@ public async Task Metrics() using (StartVerifiableLog()) { var testMeterFactory = new TestMeterFactory(); - using var connectionDuration = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), HttpConnectionsMetrics.MeterName, "connection-duration"); - using var currentConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), HttpConnectionsMetrics.MeterName, "current-connections"); - using var currentTransports = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), HttpConnectionsMetrics.MeterName, "current-transports"); + using var connectionDuration = new InstrumentRecorder(testMeterFactory, HttpConnectionsMetrics.MeterName, "connection-duration"); + using var currentConnections = new InstrumentRecorder(testMeterFactory, HttpConnectionsMetrics.MeterName, "current-connections"); + using var currentTransports = new InstrumentRecorder(testMeterFactory, HttpConnectionsMetrics.MeterName, "current-transports"); var metrics = new HttpConnectionsMetrics(testMeterFactory); var manager = CreateConnectionManager(LoggerFactory, metrics); @@ -1392,7 +1391,7 @@ public async Task RequestToActiveConnectionId409ForStreamingTransports(HttpTrans var options = new HttpConnectionDispatcherOptions(); var request1 = dispatcher.ExecuteAsync(context1, options, app); - await dispatcher.ExecuteAsync(context2, options, app); + await dispatcher.ExecuteAsync(context2, options, app).DefaultTimeout(); Assert.Equal(StatusCodes.Status409Conflict, context2.Response.StatusCode); diff --git a/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs b/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs index ff067e567f4a..0a44bdd49840 100644 --- a/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs +++ b/src/SignalR/common/Http.Connections/test/HttpConnectionManagerTests.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Metrics; using Microsoft.Extensions.Options; using Xunit; @@ -95,6 +94,7 @@ public async Task DisposingConnectionsClosesBothSidesOfThePipe(ConnectionStates { throw new Exception("Transport failed"); } + return false; }); } @@ -102,7 +102,7 @@ public async Task DisposingConnectionsClosesBothSidesOfThePipe(ConnectionStates { // If the transport is faulted then we want to make sure the transport task only completes after // the application completes - connection.TransportTask = Task.FromException(new Exception("Application failed")); + connection.TransportTask = Task.FromException(new Exception("Application failed")); connection.ApplicationTask = Task.Run(async () => { // Wait for the application to end @@ -113,7 +113,7 @@ public async Task DisposingConnectionsClosesBothSidesOfThePipe(ConnectionStates else { connection.ApplicationTask = Task.CompletedTask; - connection.TransportTask = Task.CompletedTask; + connection.TransportTask = Task.FromResult(true); } try @@ -271,6 +271,7 @@ public async Task CloseConnectionsEndsAllPendingConnections() { connection.Application.Input.AdvanceTo(result.Buffer.End); } + return true; }); connectionManager.CloseConnections(); @@ -286,7 +287,7 @@ public async Task DisposingConnectionMultipleTimesWaitsOnConnectionClose() { var connectionManager = CreateConnectionManager(LoggerFactory); var connection = connectionManager.CreateConnection(); - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); connection.ApplicationTask = tcs.Task; connection.TransportTask = tcs.Task; @@ -296,7 +297,7 @@ public async Task DisposingConnectionMultipleTimesWaitsOnConnectionClose() Assert.False(firstTask.IsCompleted); Assert.False(secondTask.IsCompleted); - tcs.TrySetResult(); + tcs.TrySetResult(true); await Task.WhenAll(firstTask, secondTask).DefaultTimeout(); } @@ -309,7 +310,7 @@ public async Task DisposingConnectionMultipleGetsExceptionFromTransportOrApp() { var connectionManager = CreateConnectionManager(LoggerFactory); var connection = connectionManager.CreateConnection(); - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); connection.ApplicationTask = tcs.Task; connection.TransportTask = tcs.Task; @@ -336,7 +337,7 @@ public async Task DisposingConnectionMultipleGetsCancellation() { var connectionManager = CreateConnectionManager(LoggerFactory); var connection = connectionManager.CreateConnection(); - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); connection.ApplicationTask = tcs.Task; connection.TransportTask = tcs.Task; @@ -430,8 +431,8 @@ public void Metrics() using (StartVerifiableLog()) { var testMeterFactory = new TestMeterFactory(); - using var connectionDuration = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), HttpConnectionsMetrics.MeterName, "connection-duration"); - using var currentConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), HttpConnectionsMetrics.MeterName, "current-connections"); + using var connectionDuration = new InstrumentRecorder(testMeterFactory, HttpConnectionsMetrics.MeterName, "connection-duration"); + using var currentConnections = new InstrumentRecorder(testMeterFactory, HttpConnectionsMetrics.MeterName, "current-connections"); var connectionManager = CreateConnectionManager(LoggerFactory, metrics: new HttpConnectionsMetrics(testMeterFactory)); var connection = connectionManager.CreateConnection(); @@ -459,8 +460,8 @@ public void Metrics_ListenStartAfterConnection_Empty() var connectionManager = CreateConnectionManager(LoggerFactory, metrics: new HttpConnectionsMetrics(testMeterFactory)); var connection = connectionManager.CreateConnection(); - using var connectionDuration = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), HttpConnectionsMetrics.MeterName, "connection-duration"); - using var currentConnections = new InstrumentRecorder(new TestMeterRegistry(testMeterFactory.Meters), HttpConnectionsMetrics.MeterName, "current-connections"); + using var connectionDuration = new InstrumentRecorder(testMeterFactory, HttpConnectionsMetrics.MeterName, "connection-duration"); + using var currentConnections = new InstrumentRecorder(testMeterFactory, HttpConnectionsMetrics.MeterName, "current-connections"); Assert.NotNull(connection.ConnectionId); diff --git a/src/SignalR/common/Http.Connections/test/Microsoft.AspNetCore.Http.Connections.Tests.csproj b/src/SignalR/common/Http.Connections/test/Microsoft.AspNetCore.Http.Connections.Tests.csproj index 552dad1c1e05..28671b344a4f 100644 --- a/src/SignalR/common/Http.Connections/test/Microsoft.AspNetCore.Http.Connections.Tests.csproj +++ b/src/SignalR/common/Http.Connections/test/Microsoft.AspNetCore.Http.Connections.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs b/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs index a3283f1b8a51..48163419726b 100644 --- a/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs +++ b/src/SignalR/common/Http.Connections/test/WebSocketsTests.cs @@ -110,7 +110,8 @@ public async Task WebSocketTransportSetsMessageTypeBasedOnTransferFormatFeature( private HttpConnectionContext CreateHttpConnectionContext(DuplexPipe.DuplexPipePair pair, string loggerName = null) { - return new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(loggerName ?? nameof(HttpConnectionContext)), metricsContext: default, pair.Transport, pair.Application, new()); + return new HttpConnectionContext("foo", connectionToken: null, LoggerFactory.CreateLogger(loggerName ?? nameof(HttpConnectionContext)), + metricsContext: default, pair.Transport, pair.Application, new(), useAcks: false); } [Fact] diff --git a/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs b/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs index 3feaefc13d10..01322cd1ecab 100644 --- a/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs +++ b/src/SignalR/common/Protocols.Json/src/Protocol/JsonHubProtocol.cs @@ -43,6 +43,8 @@ public sealed class JsonHubProtocol : IHubProtocol private static readonly JsonEncodedText ArgumentsPropertyNameBytes = JsonEncodedText.Encode(ArgumentsPropertyName); private const string HeadersPropertyName = "headers"; private static readonly JsonEncodedText HeadersPropertyNameBytes = JsonEncodedText.Encode(HeadersPropertyName); + private const string SequenceIdPropertyName = "sequenceId"; + private static readonly JsonEncodedText SequenceIdPropertyNameBytes = JsonEncodedText.Encode(SequenceIdPropertyName); private const string ProtocolName = "json"; private const int ProtocolVersion = 1; @@ -139,6 +141,7 @@ public ReadOnlyMemory GetMessageBytes(HubMessage message) Dictionary? headers = null; var completed = false; var allowReconnect = false; + long? sequenceId = null; var reader = new Utf8JsonReader(input, isFinalBlock: true, state: default); @@ -325,6 +328,10 @@ public ReadOnlyMemory GetMessageBytes(HubMessage message) reader.CheckRead(); headers = ReadHeaders(ref reader); } + else if (reader.ValueTextEquals(SequenceIdPropertyNameBytes.EncodedUtf8Bytes)) + { + sequenceId = reader.ReadAsInt64(SequenceIdPropertyName); + } else { reader.CheckRead(); @@ -452,6 +459,10 @@ public ReadOnlyMemory GetMessageBytes(HubMessage message) return PingMessage.Instance; case HubProtocolConstants.CloseMessageType: return BindCloseMessage(error, allowReconnect); + case HubProtocolConstants.AckMessageType: + return BindAckMessage(sequenceId); + case HubProtocolConstants.SequenceMessageType: + return BindSequenceMessage(sequenceId); case null: throw new InvalidDataException($"Missing required property '{TypePropertyName}'."); default: @@ -544,6 +555,14 @@ private void WriteMessageCore(HubMessage message, IBufferWriter stream) WriteMessageType(writer, HubProtocolConstants.CloseMessageType); WriteCloseMessage(m, writer); break; + case AckMessage m: + WriteMessageType(writer, HubProtocolConstants.AckMessageType); + WriteAckMessage(m, writer); + break; + case SequenceMessage m: + WriteMessageType(writer, HubProtocolConstants.SequenceMessageType); + WriteSequenceMessage(m, writer); + break; default: throw new InvalidOperationException($"Unsupported message type: {message.GetType().FullName}"); } @@ -651,6 +670,16 @@ private static void WriteCloseMessage(CloseMessage message, Utf8JsonWriter write } } + private static void WriteAckMessage(AckMessage message, Utf8JsonWriter writer) + { + writer.WriteNumber(SequenceIdPropertyName, message.SequenceId); + } + + private static void WriteSequenceMessage(SequenceMessage message, Utf8JsonWriter writer) + { + writer.WriteNumber(SequenceIdPropertyName, message.SequenceId); + } + private void WriteArguments(object?[] arguments, Utf8JsonWriter writer) { writer.WriteStartArray(ArgumentsPropertyNameBytes); @@ -741,7 +770,8 @@ private static HubMessage BindStreamItemMessage(string invocationId, object? ite return new StreamItemMessage(invocationId, item); } - private static HubMessage BindStreamInvocationMessage(string? invocationId, string target, object?[]? arguments, bool hasArguments, string[]? streamIds) + private static HubMessage BindStreamInvocationMessage(string? invocationId, string target, + object?[]? arguments, bool hasArguments, string[]? streamIds) { if (string.IsNullOrEmpty(invocationId)) { @@ -763,7 +793,8 @@ private static HubMessage BindStreamInvocationMessage(string? invocationId, stri return new StreamInvocationMessage(invocationId, target, arguments, streamIds); } - private static HubMessage BindInvocationMessage(string? invocationId, string target, object?[]? arguments, bool hasArguments, string[]? streamIds) + private static HubMessage BindInvocationMessage(string? invocationId, string target, + object?[]? arguments, bool hasArguments, string[]? streamIds) { if (string.IsNullOrEmpty(target)) { @@ -853,6 +884,26 @@ private static CloseMessage BindCloseMessage(string? error, bool allowReconnect) return new CloseMessage(error, allowReconnect); } + private static AckMessage BindAckMessage(long? sequenceId) + { + if (sequenceId is null) + { + throw new InvalidDataException("Missing 'sequenceId' in Ack message."); + } + + return new AckMessage(sequenceId.Value); + } + + private static SequenceMessage BindSequenceMessage(long? sequenceId) + { + if (sequenceId is null) + { + throw new InvalidDataException("Missing 'sequenceId' in Sequence message."); + } + + return new SequenceMessage(sequenceId.Value); + } + private static HubMessage ApplyHeaders(HubMessage message, Dictionary? headers) { if (headers != null && message is HubInvocationMessage invocationMessage) diff --git a/src/SignalR/common/Shared/ClientResultsManager.cs b/src/SignalR/common/Shared/ClientResultsManager.cs index 18fb5a950538..cc9738efd1ed 100644 --- a/src/SignalR/common/Shared/ClientResultsManager.cs +++ b/src/SignalR/common/Shared/ClientResultsManager.cs @@ -15,7 +15,7 @@ internal sealed class ClientResultsManager : IInvocationBinder { private readonly ConcurrentDictionary Complete)> _pendingInvocations = new(); - public Task AddInvocation(string connectionId, string invocationId, CancellationToken cancellationToken) + public Task AddInvocation([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string invocationId, CancellationToken cancellationToken) { var tcs = new TaskCompletionSourceWithCancellation(this, connectionId, invocationId, cancellationToken); var result = _pendingInvocations.TryAdd(invocationId, (typeof(T), connectionId, tcs, static (state, completionMessage) => @@ -38,7 +38,7 @@ public Task AddInvocation(string connectionId, string invocationId, Cancel return tcs.Task; } - public void AddInvocation(string invocationId, (Type Type, string ConnectionId, object Tcs, Action Complete) invocationInfo) + public void AddInvocation(string invocationId, (Type Type, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, object Tcs, Action Complete) invocationInfo) { var result = _pendingInvocations.TryAdd(invocationId, invocationInfo); Debug.Assert(result); @@ -49,7 +49,7 @@ public void AddInvocation(string invocationId, (Type Type, string ConnectionId, } } - public void TryCompleteResult(string connectionId, CompletionMessage message) + public void TryCompleteResult([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, CompletionMessage message) { if (_pendingInvocations.TryGetValue(message.InvocationId!, out var item)) { @@ -72,7 +72,7 @@ public void TryCompleteResult(string connectionId, CompletionMessage message) } } - public (Type Type, string ConnectionId, object Tcs, Action Completion)? RemoveInvocation(string invocationId) + public (Type Type, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, object Tcs, Action Completion)? RemoveInvocation(string invocationId) { _pendingInvocations.TryRemove(invocationId, out var item); return item; diff --git a/src/SignalR/common/Shared/MessageBuffer.cs b/src/SignalR/common/Shared/MessageBuffer.cs new file mode 100644 index 000000000000..01c3b2850349 --- /dev/null +++ b/src/SignalR/common/Shared/MessageBuffer.cs @@ -0,0 +1,512 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.SignalR.Protocol; + +namespace Microsoft.AspNetCore.SignalR.Internal; + +internal sealed class MessageBuffer : IDisposable +{ + private static readonly TaskCompletionSource _completedTCS = new TaskCompletionSource(); + + private readonly ConnectionContext _connection; + private readonly IHubProtocol _protocol; + private readonly AckMessage _ackMessage = new(0); + private readonly SequenceMessage _sequenceMessage = new(0); + private readonly Channel _waitForAck = Channel.CreateBounded(new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropOldest }); + private readonly int _bufferLimit = 100 * 1000; + +#if NET8_0_OR_GREATER + private readonly PeriodicTimer _timer = new(TimeSpan.FromSeconds(1)); +#else + private readonly TimerAwaitable _timer = new(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); +#endif + private readonly SemaphoreSlim _writeLock = new(1, 1); + + private long _totalMessageCount; + private bool _waitForSequenceMessage; + + // Message IDs start at 1 and always increment by 1 + private long _currentReceivingSequenceId = 1; + private long _latestReceivedSequenceId = long.MinValue; + private long _lastAckedId = long.MinValue; + + private TaskCompletionSource _resend = _completedTCS; + + private object Lock => _buffer; + + private LinkedBuffer _buffer; + private int _bufferedByteCount; + + static MessageBuffer() + { + _completedTCS.SetResult(new()); + } + + // TODO: pass in limits + public MessageBuffer(ConnectionContext connection, IHubProtocol protocol) + { + // TODO: pool + _buffer = new LinkedBuffer(); + + _connection = connection; + _protocol = protocol; + +#if !NET8_0_OR_GREATER + _timer.Start(); +#endif + _ = RunTimer(); + } + + private async Task RunTimer() + { + using (_timer) + { +#if NET8_0_OR_GREATER + while (await _timer.WaitForNextTickAsync().ConfigureAwait(false)) +#else + while (await _timer) +#endif + { + if (_lastAckedId < _latestReceivedSequenceId) + { + // TODO: consider a minimum time between sending these? + + var sequenceId = _latestReceivedSequenceId; + + _ackMessage.SequenceId = sequenceId; + + await _writeLock.WaitAsync().ConfigureAwait(false); + try + { + _protocol.WriteMessage(_ackMessage, _connection.Transport.Output); + await _connection.Transport.Output.FlushAsync().ConfigureAwait(false); + _lastAckedId = sequenceId; + } + finally + { + _writeLock.Release(); + } + } + } + } + } + + /// + /// Calling code is assumed to not call this method in parallel. Currently HubConnection and HubConnectionContext respect that. + /// + // TODO: WriteAsync(HubMessage) overload, so we don't allocate SerializedHubMessage for messages that aren't going to be buffered + public async ValueTask WriteAsync(SerializedHubMessage hubMessage, CancellationToken cancellationToken) + { + // TODO: Backpressure based on message count and total message size + if (_bufferedByteCount > _bufferLimit) + { + // primitive backpressure if buffer is full + while (await _waitForAck.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) + { + if (_waitForAck.Reader.TryRead(out var count) && count < _bufferLimit) + { + break; + } + } + } + + // Avoid condition where last Ack position is the position we're currently writing into the buffer + // If we wrote messages around the entire buffer before another Ack arrived we would end up reading the Ack position and writing over a buffered message + _waitForAck.Reader.TryRead(out _); + + // TODO: We could consider buffering messages until they hit backpressure in the case when the connection is down + await _resend.Task.ConfigureAwait(false); + + var waitForResend = false; + + await _writeLock.WaitAsync(cancellationToken: default).ConfigureAwait(false); + try + { + if (hubMessage.Message is HubInvocationMessage invocationMessage) + { + _totalMessageCount++; + } + else + { + // Non-ackable message, don't add to buffer + return await _connection.Transport.Output.WriteAsync(hubMessage.GetSerializedMessage(_protocol), cancellationToken).ConfigureAwait(false); + } + + var messageBytes = hubMessage.GetSerializedMessage(_protocol); + lock (Lock) + { + _bufferedByteCount += messageBytes.Length; + _buffer.AddMessage(hubMessage, _totalMessageCount); + } + + return await _connection.Transport.Output.WriteAsync(messageBytes, cancellationToken).ConfigureAwait(false); + } + // TODO: figure out what exception to use + catch (ConnectionResetException) + { + waitForResend = true; + } + finally + { + _writeLock.Release(); + } + + if (waitForResend) + { + var oldTcs = Interlocked.Exchange(ref _resend, new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)); + if (!oldTcs.Task.IsCompleted) + { + return await oldTcs.Task.ConfigureAwait(false); + } + return await _resend.Task.ConfigureAwait(false); + } + + throw new NotImplementedException("shouldn't reach here"); + } + + public void Ack(AckMessage ackMessage) + { + // TODO: what if ackMessage.SequenceId is larger than last sent message? + + var newCount = -1; + + lock (Lock) + { + var item = _buffer.RemoveMessages(ackMessage.SequenceId, _protocol); + _buffer = item.Item1; + _bufferedByteCount -= item.Item2; + + newCount = _bufferedByteCount; + } + + // Release potential backpressure + if (newCount >= 0) + { + _waitForAck.Writer.TryWrite(newCount); + } + } + + internal bool ShouldProcessMessage(HubMessage message) + { + // TODO: if we're expecting a sequence message but get here should we error or ignore or maybe even continue to process them? + if (_waitForSequenceMessage) + { + if (message is SequenceMessage) + { + _waitForSequenceMessage = false; + return true; + } + else + { + // ignore messages received while waiting for sequence message + return false; + } + } + + // Only care about messages implementing HubInvocationMessage currently (e.g. ignore ping, close, ack, sequence) + // Could expand in the future, but should probably rev the ack version if changes are made + if (message is not HubInvocationMessage) + { + return true; + } + + var currentId = _currentReceivingSequenceId; + _currentReceivingSequenceId++; + if (currentId <= _latestReceivedSequenceId) + { + // Ignore, this is a duplicate message + return false; + } + _latestReceivedSequenceId = currentId; + + return true; + } + + internal void ResetSequence(SequenceMessage sequenceMessage) + { + // TODO: is a sequence message expected right now? + + if (sequenceMessage.SequenceId > _currentReceivingSequenceId) + { + throw new InvalidOperationException("Sequence ID greater than amount of messages we've received."); + } + _currentReceivingSequenceId = sequenceMessage.SequenceId; + } + + internal void Resend() + { + _waitForSequenceMessage = true; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var oldTcs = Interlocked.Exchange(ref _resend, tcs); + // WriteAsync can also try to swap the TCS, we need to check if it's completed to know if it was swapped or not + if (!oldTcs.Task.IsCompleted) + { + // Swap back to the TCS created by WriteAsync since it's waiting on the result of that task + Interlocked.Exchange(ref _resend, oldTcs); + tcs = oldTcs; + } + + _ = DoResendAsync(tcs); + } + + private async Task DoResendAsync(TaskCompletionSource tcs) + { + FlushResult finalResult = new(); + await _writeLock.WaitAsync().ConfigureAwait(false); + try + { + _sequenceMessage.SequenceId = _totalMessageCount + 1; + + var isFirst = true; + foreach (var item in _buffer.GetMessages()) + { + if (item.SequenceId > 0) + { + if (isFirst) + { + _sequenceMessage.SequenceId = item.SequenceId; + _protocol.WriteMessage(_sequenceMessage, _connection.Transport.Output); + isFirst = false; + } + finalResult = await _connection.Transport.Output.WriteAsync(item.HubMessage!.GetSerializedMessage(_protocol)).ConfigureAwait(false); + } + } + + if (isFirst) + { + _protocol.WriteMessage(_sequenceMessage, _connection.Transport.Output); + finalResult = await _connection.Transport.Output.FlushAsync().ConfigureAwait(false); + } + } + catch (Exception ex) + { + tcs.SetException(ex); + } + finally + { + _writeLock.Release(); + tcs.TrySetResult(finalResult); + } + } + + public void Dispose() + { + ((IDisposable)_timer).Dispose(); + } + + // Linked list of SerializedHubMessage arrays, sort of like ReadOnlySequence + private sealed class LinkedBuffer + { + private const int BufferLength = 10; + + private int _currentIndex = -1; + private int _ackedIndex = -1; + private long _startingSequenceId = long.MinValue; + private LinkedBuffer? _next; + + private readonly SerializedHubMessage?[] _messages = new SerializedHubMessage?[BufferLength]; + + public void AddMessage(SerializedHubMessage hubMessage, long sequenceId) + { + if (_startingSequenceId < 0) + { + Debug.Assert(_currentIndex == -1); + _startingSequenceId = sequenceId; + } + + if (_currentIndex < BufferLength - 1) + { + Debug.Assert(_startingSequenceId + _currentIndex + 1 == sequenceId); + + _currentIndex++; + _messages[_currentIndex] = hubMessage; + } + else if (_next is null) + { + _next = new LinkedBuffer(); + _next.AddMessage(hubMessage, sequenceId); + } + else + { + // TODO: Should we avoid this path by keeping a tail pointer? + // Debug.Assert(false); + + var linkedBuffer = _next; + while (linkedBuffer._next is not null) + { + linkedBuffer = linkedBuffer._next; + } + + // TODO: verify no stack overflow potential + linkedBuffer.AddMessage(hubMessage, sequenceId); + } + } + + public (LinkedBuffer, int returnCredit) RemoveMessages(long sequenceId, IHubProtocol protocol) + { + return RemoveMessagesCore(this, sequenceId, protocol); + } + + private static (LinkedBuffer, int returnCredit) RemoveMessagesCore(LinkedBuffer linkedBuffer, long sequenceId, IHubProtocol protocol) + { + var returnCredit = 0; + while (linkedBuffer._startingSequenceId <= sequenceId) + { + var numElements = (int)Math.Min(BufferLength, Math.Max(1, sequenceId - (linkedBuffer._startingSequenceId - 1))); + Debug.Assert(numElements > 0 && numElements < BufferLength + 1); + + for (var i = 0; i < numElements; i++) + { + returnCredit += linkedBuffer._messages[i]?.GetSerializedMessage(protocol).Length ?? 0; + linkedBuffer._messages[i] = null; + } + + linkedBuffer._ackedIndex = numElements - 1; + + if (numElements == BufferLength) + { + if (linkedBuffer._next is null) + { + linkedBuffer.Reset(shouldPool: false); + return (linkedBuffer, returnCredit); + } + else + { + var tmp = linkedBuffer; + linkedBuffer = linkedBuffer._next; + tmp.Reset(shouldPool: true); + } + } + else + { + return (linkedBuffer, returnCredit); + } + } + + return (linkedBuffer, returnCredit); + } + + private void Reset(bool shouldPool) + { + _startingSequenceId = long.MinValue; + _currentIndex = -1; + _ackedIndex = -1; + _next = null; + + Array.Clear(_messages, 0, BufferLength); + + // TODO: Add back to pool + if (shouldPool) + { + } + } + + public IEnumerable<(SerializedHubMessage? HubMessage, long SequenceId)> GetMessages() + { + return new Enumerable(this); + } + + private struct Enumerable : IEnumerable<(SerializedHubMessage?, long)> + { + private readonly LinkedBuffer _linkedBuffer; + + public Enumerable(LinkedBuffer linkedBuffer) + { + _linkedBuffer = linkedBuffer; + } + + public IEnumerator<(SerializedHubMessage?, long)> GetEnumerator() + { + return new Enumerator(_linkedBuffer); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + } + + private struct Enumerator : IEnumerator<(SerializedHubMessage?, long)> + { + private LinkedBuffer? _linkedBuffer; + private int _index; + + public Enumerator(LinkedBuffer linkedBuffer) + { + _linkedBuffer = linkedBuffer; + } + + public (SerializedHubMessage?, long) Current + { + get + { + if (_linkedBuffer is null) + { + return (null, long.MinValue); + } + + var index = _index - 1; + var firstMessageIndex = _linkedBuffer._ackedIndex + 1; + if (firstMessageIndex + index < BufferLength) + { + return (_linkedBuffer._messages[firstMessageIndex + index], _linkedBuffer._startingSequenceId + firstMessageIndex + index); + } + + return (null, long.MinValue); + } + } + + object IEnumerator.Current => throw new NotImplementedException(); + + public void Dispose() + { + _linkedBuffer = null; + } + + public bool MoveNext() + { + if (_linkedBuffer is null) + { + return false; + } + + var firstMessageIndex = _linkedBuffer._ackedIndex + 1; + if (firstMessageIndex + _index >= BufferLength) + { + _linkedBuffer = _linkedBuffer._next; + _index = 1; + } + else + { + if (_linkedBuffer._messages[firstMessageIndex + _index] is null) + { + _linkedBuffer = null; + } + else + { + _index++; + } + } + + return _linkedBuffer is not null; + } + + public void Reset() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/SignalR/common/Shared/SystemTextJsonExtensions.cs b/src/SignalR/common/Shared/SystemTextJsonExtensions.cs index c28ba9e16398..30f1c9adc6ab 100644 --- a/src/SignalR/common/Shared/SystemTextJsonExtensions.cs +++ b/src/SignalR/common/Shared/SystemTextJsonExtensions.cs @@ -97,4 +97,21 @@ public static string ReadAsString(this ref Utf8JsonReader reader, string propert return reader.GetInt32(); } + + public static long? ReadAsInt64(this ref Utf8JsonReader reader, string propertyName) + { + reader.Read(); + + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType != JsonTokenType.Number) + { + throw new InvalidDataException($"Expected '{propertyName}' to be of type {JsonTokenType.Number}."); + } + + return reader.GetInt64(); + } } diff --git a/src/SignalR/common/SignalR.Common/src/Protocol/AckMessage.cs b/src/SignalR/common/SignalR.Common/src/Protocol/AckMessage.cs new file mode 100644 index 000000000000..33ef91881fa9 --- /dev/null +++ b/src/SignalR/common/SignalR.Common/src/Protocol/AckMessage.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.SignalR.Protocol; + +/// +/// Represents the ID being acknowledged so we can stop buffering older messages. +/// +public sealed class AckMessage : HubMessage +{ + /// + /// + /// + /// + public AckMessage(long sequenceId) + { + SequenceId = sequenceId; + } + + /// + /// + /// + public long SequenceId { get; set; } +} + +/// +/// Represents the restart of the sequence of messages being sent. is the starting ID of messages being sent, which might be duplicate messages. +/// +public sealed class SequenceMessage : HubMessage +{ + /// + /// + /// + /// + public SequenceMessage(long sequenceId) + { + SequenceId = sequenceId; + } + + /// + /// + /// + public long SequenceId { get; set; } +} diff --git a/src/SignalR/common/SignalR.Common/src/Protocol/HubProtocolConstants.cs b/src/SignalR/common/SignalR.Common/src/Protocol/HubProtocolConstants.cs index eb1e3914ac17..c5e67987ae92 100644 --- a/src/SignalR/common/SignalR.Common/src/Protocol/HubProtocolConstants.cs +++ b/src/SignalR/common/SignalR.Common/src/Protocol/HubProtocolConstants.cs @@ -42,4 +42,14 @@ public static class HubProtocolConstants /// Represents the close message type. /// public const int CloseMessageType = 7; + + /// + /// + /// + public const int AckMessageType = 8; + + /// + /// + /// + public const int SequenceMessageType = 9; } diff --git a/src/SignalR/common/SignalR.Common/src/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/SignalR/common/SignalR.Common/src/PublicAPI/net462/PublicAPI.Unshipped.txt index 7dc5c58110bf..29b26f2e7839 100644 --- a/src/SignalR/common/SignalR.Common/src/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/SignalR/common/SignalR.Common/src/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1 +1,11 @@ #nullable enable +const Microsoft.AspNetCore.SignalR.Protocol.HubProtocolConstants.AckMessageType = 8 -> int +const Microsoft.AspNetCore.SignalR.Protocol.HubProtocolConstants.SequenceMessageType = 9 -> int +Microsoft.AspNetCore.SignalR.Protocol.AckMessage +Microsoft.AspNetCore.SignalR.Protocol.AckMessage.AckMessage(long sequenceId) -> void +Microsoft.AspNetCore.SignalR.Protocol.AckMessage.SequenceId.get -> long +Microsoft.AspNetCore.SignalR.Protocol.AckMessage.SequenceId.set -> void +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage.SequenceId.get -> long +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage.SequenceId.set -> void +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage.SequenceMessage(long sequenceId) -> void diff --git a/src/SignalR/common/SignalR.Common/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/SignalR/common/SignalR.Common/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 7dc5c58110bf..29b26f2e7839 100644 --- a/src/SignalR/common/SignalR.Common/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/SignalR/common/SignalR.Common/src/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,11 @@ #nullable enable +const Microsoft.AspNetCore.SignalR.Protocol.HubProtocolConstants.AckMessageType = 8 -> int +const Microsoft.AspNetCore.SignalR.Protocol.HubProtocolConstants.SequenceMessageType = 9 -> int +Microsoft.AspNetCore.SignalR.Protocol.AckMessage +Microsoft.AspNetCore.SignalR.Protocol.AckMessage.AckMessage(long sequenceId) -> void +Microsoft.AspNetCore.SignalR.Protocol.AckMessage.SequenceId.get -> long +Microsoft.AspNetCore.SignalR.Protocol.AckMessage.SequenceId.set -> void +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage.SequenceId.get -> long +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage.SequenceId.set -> void +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage.SequenceMessage(long sequenceId) -> void diff --git a/src/SignalR/common/SignalR.Common/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/SignalR/common/SignalR.Common/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 7dc5c58110bf..29b26f2e7839 100644 --- a/src/SignalR/common/SignalR.Common/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/SignalR/common/SignalR.Common/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,11 @@ #nullable enable +const Microsoft.AspNetCore.SignalR.Protocol.HubProtocolConstants.AckMessageType = 8 -> int +const Microsoft.AspNetCore.SignalR.Protocol.HubProtocolConstants.SequenceMessageType = 9 -> int +Microsoft.AspNetCore.SignalR.Protocol.AckMessage +Microsoft.AspNetCore.SignalR.Protocol.AckMessage.AckMessage(long sequenceId) -> void +Microsoft.AspNetCore.SignalR.Protocol.AckMessage.SequenceId.get -> long +Microsoft.AspNetCore.SignalR.Protocol.AckMessage.SequenceId.set -> void +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage.SequenceId.get -> long +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage.SequenceId.set -> void +Microsoft.AspNetCore.SignalR.Protocol.SequenceMessage.SequenceMessage(long sequenceId) -> void diff --git a/src/SignalR/docs/specs/TransportProtocols.md b/src/SignalR/docs/specs/TransportProtocols.md index a4c10f4eadfa..e2fb3d28cb17 100644 --- a/src/SignalR/docs/specs/TransportProtocols.md +++ b/src/SignalR/docs/specs/TransportProtocols.md @@ -20,12 +20,20 @@ Throughout this document, the term `[endpoint-base]` is used to refer to the rou The `POST [endpoint-base]/negotiate` request is used to establish a connection between the client and the server. +*negotiateVersion:* + In the POST request the client sends a query string parameter with the key "negotiateVersion" and the value as the negotiate protocol version it would like to use. If the query string is omitted, the server treats the version as zero. The server will include a "negotiateVersion" property in the json response that says which version it will be using. The version is chosen as described below: * If the servers minimum supported protocol version is greater than the version requested by the client it will send an error response and close the connection * If the server supports the request version it will respond with the requested version * If the requested version is greater than the servers largest supported version the server will respond with its largest supported version The client may close the connection if the "negotiateVersion" in the response is not acceptable. +*useAck:* + +In the POST request the client may include a query string parameter with the key "useAck" and the value of "true". If this is included the server will decide if it supports/allows the [ack protocol](#todo) described below, and return "useAck": "true" as a json property in the negotiate response if it will use the ack protocol. If true, the client can reconnect using the same transport and reuse the connectionToken/connectionId. The server may still reject the reconnect if it takes too long or for any reason it chooses. If false, the client must not reuse connectionToken/connectionId. If the "useAck" property is missing from the negotiate response this also implies false, so the ack protocol should not be used. + +----------- + The content type of the response is `application/json` and is a JSON payload containing properties to assist the client in establishing a persistent connection. Extra JSON properties that the client does not know about should be ignored. This allows for future additions without breaking older clients. ### Version 1 diff --git a/src/SignalR/perf/Microbenchmarks/DefaultHubDispatcherBenchmark.cs b/src/SignalR/perf/Microbenchmarks/DefaultHubDispatcherBenchmark.cs index bdbf8443d752..d0efd3d4489c 100644 --- a/src/SignalR/perf/Microbenchmarks/DefaultHubDispatcherBenchmark.cs +++ b/src/SignalR/perf/Microbenchmarks/DefaultHubDispatcherBenchmark.cs @@ -35,6 +35,7 @@ public void GlobalSetup() new HubContext(hubLifetimeManager), enableDetailedErrors: false, disableImplicitFromServiceParameters: true, + useAcks: false, new Logger>(NullLoggerFactory.Instance), hubFilters: null, hubLifetimeManager); diff --git a/src/SignalR/perf/Microbenchmarks/DefaultHubLifetimeManagerBenchmark.cs b/src/SignalR/perf/Microbenchmarks/DefaultHubLifetimeManagerBenchmark.cs index 103717967b00..f612b025a9fd 100644 --- a/src/SignalR/perf/Microbenchmarks/DefaultHubLifetimeManagerBenchmark.cs +++ b/src/SignalR/perf/Microbenchmarks/DefaultHubLifetimeManagerBenchmark.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.SignalR.Microbenchmarks.Shared; using Microsoft.AspNetCore.SignalR.Protocol; @@ -61,7 +62,7 @@ public void GlobalSetup() } } - private void AddUnique(List list, string connectionId) + private void AddUnique(List list, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { if (!list.Contains(connectionId)) { diff --git a/src/SignalR/samples/ClientSample/HubSample.cs b/src/SignalR/samples/ClientSample/HubSample.cs index f77b9c3a9a8d..506a520b2238 100644 --- a/src/SignalR/samples/ClientSample/HubSample.cs +++ b/src/SignalR/samples/ClientSample/HubSample.cs @@ -99,7 +99,7 @@ public static async Task ExecuteAsync(string baseUrl) try { - await connection.InvokeAsync("Send", line); + await connection.InvokeAsync("Send", "C#", line); } catch when (closedTokenSource.IsCancellationRequested) { diff --git a/src/SignalR/samples/SignalRSamples/Hubs/Chat.cs b/src/SignalR/samples/SignalRSamples/Hubs/Chat.cs index 47484e2cf3ef..3d1f4df0667a 100644 --- a/src/SignalR/samples/SignalRSamples/Hubs/Chat.cs +++ b/src/SignalR/samples/SignalRSamples/Hubs/Chat.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.SignalR; +using System.Diagnostics.CodeAnalysis; namespace SignalRSamples.Hubs; @@ -29,7 +30,7 @@ public Task SendToOthers(string name, string message) return Clients.Others.SendAsync("Send", $"{name}: {message}"); } - public Task SendToConnection(string connectionId, string name, string message) + public Task SendToConnection([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string name, string message) { return Clients.Client(connectionId).SendAsync("Send", $"Private message from {name}: {message}"); } diff --git a/src/SignalR/samples/SignalRSamples/Program.cs b/src/SignalR/samples/SignalRSamples/Program.cs index 3675e5c32b37..c610b486315f 100644 --- a/src/SignalR/samples/SignalRSamples/Program.cs +++ b/src/SignalR/samples/SignalRSamples/Program.cs @@ -25,12 +25,12 @@ public static Task Main(string[] args) { factory.AddConfiguration(c.Configuration.GetSection("Logging")); factory.AddConsole(); - //factory.SetMinimumLevel(LogLevel.Trace); + factory.SetMinimumLevel(LogLevel.Debug); }) .UseKestrel(options => { // Default port - options.ListenAnyIP(0); + options.ListenAnyIP(5000); // Hub bound to TCP end point //options.Listen(IPAddress.Any, 9001, builder => diff --git a/src/SignalR/samples/SocialWeather/ConnectionList.cs b/src/SignalR/samples/SocialWeather/ConnectionList.cs index f8806edd68f5..da1959ddd763 100644 --- a/src/SignalR/samples/SocialWeather/ConnectionList.cs +++ b/src/SignalR/samples/SocialWeather/ConnectionList.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Connections; namespace SocialWeather; @@ -12,7 +13,7 @@ internal class ConnectionList : IReadOnlyCollection private readonly ConcurrentDictionary _connections = new ConcurrentDictionary(StringComparer.Ordinal); - public ConnectionContext this[string connectionId] + public ConnectionContext this[[StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId] { get { diff --git a/src/SignalR/samples/SocialWeather/PersistentConnectionLifeTimeManager.cs b/src/SignalR/samples/SocialWeather/PersistentConnectionLifeTimeManager.cs index ca0b398d8127..915faf193e9b 100644 --- a/src/SignalR/samples/SocialWeather/PersistentConnectionLifeTimeManager.cs +++ b/src/SignalR/samples/SocialWeather/PersistentConnectionLifeTimeManager.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Connections; @@ -51,7 +52,7 @@ public async Task SendToAllAsync(T data) } } - public Task InvokeConnectionAsync(string connectionId, object data) + public Task InvokeConnectionAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, object data) { throw new NotImplementedException(); } diff --git a/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs b/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs index eec5f9ea2546..59de27f9802b 100644 --- a/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs +++ b/src/SignalR/server/Core/src/DefaultHubLifetimeManager.cs @@ -31,7 +31,7 @@ public DefaultHubLifetimeManager(ILogger> logger } /// - public override Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) + public override Task AddToGroupAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(connectionId); ArgumentNullException.ThrowIfNull(groupName); @@ -53,7 +53,7 @@ public override Task AddToGroupAsync(string connectionId, string groupName, Canc } /// - public override Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) + public override Task RemoveFromGroupAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(connectionId); ArgumentNullException.ThrowIfNull(groupName); @@ -159,7 +159,7 @@ private static void SendToGroupConnections(string methodName, object?[] args, Co } /// - public override Task SendConnectionAsync(string connectionId, string methodName, object?[] args, CancellationToken cancellationToken = default) + public override Task SendConnectionAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string methodName, object?[] args, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(connectionId); @@ -302,7 +302,7 @@ public override Task SendUsersAsync(IReadOnlyList userIds, string method } /// - public override async Task InvokeConnectionAsync(string connectionId, string methodName, object?[] args, CancellationToken cancellationToken) + public override async Task InvokeConnectionAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string methodName, object?[] args, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(connectionId); @@ -352,7 +352,7 @@ public override async Task InvokeConnectionAsync(string connectionId, stri } /// - public override Task SetConnectionResultAsync(string connectionId, CompletionMessage result) + public override Task SetConnectionResultAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, CompletionMessage result) { _clientResultsManager.TryCompleteResult(connectionId, result); return Task.CompletedTask; diff --git a/src/SignalR/server/Core/src/DynamicHubClients.cs b/src/SignalR/server/Core/src/DynamicHubClients.cs index 6151e5b12296..6a1b2c586b4d 100644 --- a/src/SignalR/server/Core/src/DynamicHubClients.cs +++ b/src/SignalR/server/Core/src/DynamicHubClients.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.SignalR.Internal; namespace Microsoft.AspNetCore.SignalR; @@ -44,7 +45,7 @@ public DynamicHubClients(IHubCallerClients clients) /// /// The connection ID. /// An object that can be used to invoke methods. - public dynamic Client(string connectionId) => new DynamicClientProxy(_clients.Client(connectionId)); + public dynamic Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) => new DynamicClientProxy(_clients.Client(connectionId)); /// /// Gets an object that can be used to invoke methods on the specified connections. diff --git a/src/SignalR/server/Core/src/HubConnectionContext.cs b/src/SignalR/server/Core/src/HubConnectionContext.cs index a2a9f24429ef..d9dcc8beca2e 100644 --- a/src/SignalR/server/Core/src/HubConnectionContext.cs +++ b/src/SignalR/server/Core/src/HubConnectionContext.cs @@ -8,6 +8,7 @@ using System.IO.Pipelines; using System.Security.Claims; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Abstractions; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.SignalR.Internal; @@ -36,6 +37,7 @@ public partial class HubConnectionContext private readonly CancellationTokenRegistration _closedRegistration; private readonly CancellationTokenRegistration? _closedRequestedRegistration; + private MessageBuffer? _messageBuffer; private StreamTracker? _streamTracker; private long _lastSendTick; private ReadOnlyMemory _cachedPingMessage; @@ -48,6 +50,10 @@ public partial class HubConnectionContext private TimeSpan _receivedMessageElapsed; private long _receivedMessageTick; private ClaimsPrincipal? _user; + private bool _useAcks; + + [MemberNotNullWhen(true, nameof(_messageBuffer))] + internal bool UsingAcks() => _useAcks; /// /// Initializes a new instance of the class. @@ -254,11 +260,18 @@ private ValueTask WriteCore(HubMessage message, CancellationToken c { try { - // We know that we are only writing this message to one receiver, so we can - // write it without caching. - Protocol.WriteMessage(message, _connectionContext.Transport.Output); + if (UsingAcks()) + { + return _messageBuffer.WriteAsync(new SerializedHubMessage(message), cancellationToken); + } + else + { + // We know that we are only writing this message to one receiver, so we can + // write it without caching. + Protocol.WriteMessage(message, _connectionContext.Transport.Output); - return _connectionContext.Transport.Output.FlushAsync(cancellationToken); + return _connectionContext.Transport.Output.FlushAsync(cancellationToken); + } } catch (Exception ex) { @@ -275,10 +288,18 @@ private ValueTask WriteCore(SerializedHubMessage message, Cancellat { try { - // Grab a preserialized buffer for this protocol. - var buffer = message.GetSerializedMessage(Protocol); + if (UsingAcks()) + { + Debug.Assert(_messageBuffer is not null); + return _messageBuffer.WriteAsync(message, cancellationToken); + } + else + { + // Grab a potentially pre-serialized buffer for this protocol. + var buffer = message.GetSerializedMessage(Protocol); - return _connectionContext.Transport.Output.WriteAsync(buffer, cancellationToken); + return _connectionContext.Transport.Output.WriteAsync(buffer, cancellationToken); + } } catch (Exception ex) { @@ -550,6 +571,13 @@ await WriteHandshakeResponseAsync(new HandshakeResponseMessage( Log.HandshakeComplete(_logger, Protocol.Name); await WriteHandshakeResponseAsync(HandshakeResponseMessage.Empty); + + if (_connectionContext.Features.Get() is IReconnectFeature feature) + { + _useAcks = true; + _messageBuffer = new MessageBuffer(_connectionContext, Protocol); + feature.NotifyOnReconnect = _messageBuffer.Resend; + } return true; } else if (overLength) @@ -725,10 +753,36 @@ internal void StopClientTimeout() internal void Cleanup() { + _messageBuffer?.Dispose(); _closedRegistration.Dispose(); _closedRequestedRegistration?.Dispose(); // Use _streamTracker to avoid lazy init from StreamTracker getter if it doesn't exist _streamTracker?.CompleteAll(new OperationCanceledException("The underlying connection was closed.")); } + + internal void Ack(AckMessage ackMessage) + { + if (UsingAcks()) + { + _messageBuffer.Ack(ackMessage); + } + } + + internal bool ShouldProcessMessage(HubMessage message) + { + if (UsingAcks()) + { + return _messageBuffer.ShouldProcessMessage(message); + } + return true; + } + + internal void ResetSequence(SequenceMessage sequenceMessage) + { + if (UsingAcks()) + { + _messageBuffer.ResetSequence(sequenceMessage); + } + } } diff --git a/src/SignalR/server/Core/src/HubConnectionHandler.cs b/src/SignalR/server/Core/src/HubConnectionHandler.cs index ab3d0f5bbd7b..21e9061897e8 100644 --- a/src/SignalR/server/Core/src/HubConnectionHandler.cs +++ b/src/SignalR/server/Core/src/HubConnectionHandler.cs @@ -94,6 +94,8 @@ IServiceScopeFactory serviceScopeFactory new HubContext(lifetimeManager), _enableDetailedErrors, disableImplicitFromServiceParameters, + // TODO + useAcks: true, new Logger>(loggerFactory), hubFilters, lifetimeManager); diff --git a/src/SignalR/server/Core/src/HubLifetimeManager.cs b/src/SignalR/server/Core/src/HubLifetimeManager.cs index f1bc8b058074..ebcad242d297 100644 --- a/src/SignalR/server/Core/src/HubLifetimeManager.cs +++ b/src/SignalR/server/Core/src/HubLifetimeManager.cs @@ -54,7 +54,7 @@ public abstract class HubLifetimeManager where THub : Hub /// The invocation arguments. /// The token to monitor for cancellation requests. The default value is . /// A that represents the asynchronous send. - public abstract Task SendConnectionAsync(string connectionId, string methodName, object?[] args, CancellationToken cancellationToken = default); + public abstract Task SendConnectionAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string methodName, object?[] args, CancellationToken cancellationToken = default); /// /// Sends an invocation message to the specified connections. @@ -124,7 +124,7 @@ public abstract class HubLifetimeManager where THub : Hub /// The group name. /// The token to monitor for cancellation requests. The default value is . /// A that represents the asynchronous add. - public abstract Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default); + public abstract Task AddToGroupAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName, CancellationToken cancellationToken = default); /// /// Removes a connection from the specified group. @@ -133,7 +133,7 @@ public abstract class HubLifetimeManager where THub : Hub /// The group name. /// The token to monitor for cancellation requests. The default value is . /// A that represents the asynchronous remove. - public abstract Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default); + public abstract Task RemoveFromGroupAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName, CancellationToken cancellationToken = default); /// /// Sends an invocation message to the specified connection and waits for a response. @@ -144,7 +144,7 @@ public abstract class HubLifetimeManager where THub : Hub /// The invocation arguments. /// The token to monitor for cancellation requests. It is recommended to set a max wait for expecting a result. /// The response from the connection. - public virtual Task InvokeConnectionAsync(string connectionId, string methodName, object?[] args, CancellationToken cancellationToken) + public virtual Task InvokeConnectionAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string methodName, object?[] args, CancellationToken cancellationToken) { throw new NotImplementedException($"{GetType().Name} does not support client return values."); } @@ -155,7 +155,7 @@ public virtual Task InvokeConnectionAsync(string connectionId, string meth /// The connection ID. /// The result from the connection. /// A that represents the result being set or being forwarded to another server. - public virtual Task SetConnectionResultAsync(string connectionId, CompletionMessage result) + public virtual Task SetConnectionResultAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, CompletionMessage result) { throw new NotImplementedException($"{GetType().Name} does not support client return values."); } diff --git a/src/SignalR/server/Core/src/IGroupManager.cs b/src/SignalR/server/Core/src/IGroupManager.cs index f82d54e8dd88..906da1c99cad 100644 --- a/src/SignalR/server/Core/src/IGroupManager.cs +++ b/src/SignalR/server/Core/src/IGroupManager.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.SignalR; /// @@ -15,7 +16,7 @@ public interface IGroupManager /// The group name. /// The token to monitor for cancellation requests. The default value is . /// A that represents the asynchronous add. - Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default); + Task AddToGroupAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName, CancellationToken cancellationToken = default); /// /// Removes a connection from the specified group. @@ -24,5 +25,5 @@ public interface IGroupManager /// The group name. /// The token to monitor for cancellation requests. The default value is . /// A that represents the asynchronous remove. - Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default); + Task RemoveFromGroupAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName, CancellationToken cancellationToken = default); } diff --git a/src/SignalR/server/Core/src/IHubCallerClients.cs b/src/SignalR/server/Core/src/IHubCallerClients.cs index 688591a37cef..c29a8581cfde 100644 --- a/src/SignalR/server/Core/src/IHubCallerClients.cs +++ b/src/SignalR/server/Core/src/IHubCallerClients.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.SignalR.Internal; namespace Microsoft.AspNetCore.SignalR; @@ -15,7 +16,7 @@ public interface IHubCallerClients : IHubCallerClients /// /// The connection ID. /// A client caller. - new ISingleClientProxy Client(string connectionId) => new NonInvokingSingleClientProxy(((IHubCallerClients)this).Client(connectionId), "IHubCallerClients.Client(string connectionId)"); + new ISingleClientProxy Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) => new NonInvokingSingleClientProxy(((IHubCallerClients)this).Client(connectionId), "IHubCallerClients.Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId)"); /// /// Gets a proxy that can be used to invoke methods on the calling client and receive results. diff --git a/src/SignalR/server/Core/src/IHubClients.cs b/src/SignalR/server/Core/src/IHubClients.cs index 008b1df2d61f..bfc8f5b72c96 100644 --- a/src/SignalR/server/Core/src/IHubClients.cs +++ b/src/SignalR/server/Core/src/IHubClients.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.SignalR.Internal; namespace Microsoft.AspNetCore.SignalR; @@ -15,5 +16,5 @@ public interface IHubClients : IHubClients /// /// The connection ID. /// A client caller. - new ISingleClientProxy Client(string connectionId) => new NonInvokingSingleClientProxy(((IHubClients)this).Client(connectionId), "IHubClients.Client(string connectionId)"); + new ISingleClientProxy Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) => new NonInvokingSingleClientProxy(((IHubClients)this).Client(connectionId), "IHubClients.Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId)"); } diff --git a/src/SignalR/server/Core/src/IHubClients`T.cs b/src/SignalR/server/Core/src/IHubClients`T.cs index b6cc0634bdeb..e49086a9f74c 100644 --- a/src/SignalR/server/Core/src/IHubClients`T.cs +++ b/src/SignalR/server/Core/src/IHubClients`T.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.SignalR; /// @@ -27,7 +28,7 @@ public interface IHubClients /// /// The connection ID. /// A client caller. - T Client(string connectionId); + T Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId); /// /// Gets a that can be used to invoke methods on the specified client connections. diff --git a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs index 3458f6760a9f..a632f59a7538 100644 --- a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs +++ b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs @@ -28,15 +28,17 @@ internal sealed partial class DefaultHubDispatcher : HubDispatcher w private readonly Func? _onConnectedMiddleware; private readonly Func? _onDisconnectedMiddleware; private readonly HubLifetimeManager _hubLifetimeManager; + private readonly bool _useAcks; public DefaultHubDispatcher(IServiceScopeFactory serviceScopeFactory, IHubContext hubContext, bool enableDetailedErrors, - bool disableImplicitFromServiceParameters, ILogger> logger, List? hubFilters, HubLifetimeManager lifetimeManager) + bool disableImplicitFromServiceParameters, bool useAcks, ILogger> logger, List? hubFilters, HubLifetimeManager lifetimeManager) { _serviceScopeFactory = serviceScopeFactory; _hubContext = hubContext; _enableDetailedErrors = enableDetailedErrors; _logger = logger; _hubLifetimeManager = lifetimeManager; + _useAcks = useAcks; DiscoverHubMethods(disableImplicitFromServiceParameters); var count = hubFilters?.Count ?? 0; @@ -130,6 +132,12 @@ public override Task DispatchMessageAsync(HubConnectionContext connection, HubMe // With parallel invokes enabled, messages run sequentially until they go async and then the next message will be allowed to start running. + if (!connection.ShouldProcessMessage(hubMessage)) + { + Log.DroppingMessage(_logger, ((HubInvocationMessage)hubMessage).GetType().Name, ((HubInvocationMessage)hubMessage).InvocationId); + return Task.CompletedTask; + } + switch (hubMessage) { case InvocationBindingFailureMessage bindingFailureMessage: @@ -186,6 +194,16 @@ public override Task DispatchMessageAsync(HubConnectionContext connection, HubMe } break; + case AckMessage ackMessage: + Log.ReceivedAckMessage(_logger, ackMessage.SequenceId); + connection.Ack(ackMessage); + break; + + case SequenceMessage sequenceMessage: + Log.ReceivedSequenceMessage(_logger, sequenceMessage.SequenceId); + connection.ResetSequence(sequenceMessage); + break; + // Other kind of message we weren't expecting default: Log.UnsupportedMessageReceived(_logger, hubMessage.GetType().FullName!); @@ -374,7 +392,7 @@ await SendInvocationError(hubMethodInvocationMessage.InvocationId, connection, // No InvocationId - Send Async, no response expected if (!string.IsNullOrEmpty(hubMethodInvocationMessage.InvocationId)) { - // Invoke Async, one reponse expected + // Invoke Async, one response expected await connection.WriteAsync(CompletionMessage.WithResult(hubMethodInvocationMessage.InvocationId, result)); } } @@ -555,8 +573,7 @@ private async Task StreamAsync(string invocationId, HubConnectionContext connect } } - private static async Task SendInvocationError(string? invocationId, - HubConnectionContext connection, string errorMessage) + private static async Task SendInvocationError(string? invocationId, HubConnectionContext connection, string errorMessage) { if (string.IsNullOrEmpty(invocationId)) { diff --git a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcherLog.cs b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcherLog.cs index 24d6ee8b7fca..dce901a2d952 100644 --- a/src/SignalR/server/Core/src/Internal/DefaultHubDispatcherLog.cs +++ b/src/SignalR/server/Core/src/Internal/DefaultHubDispatcherLog.cs @@ -108,4 +108,13 @@ public static void ClosingStreamWithBindingError(ILogger logger, CompletionMessa [LoggerMessage(25, LogLevel.Error, "Invocation ID {InvocationId}: Failed while sending stream items from hub method {HubMethod}.", EventName = "FailedStreaming")] public static partial void FailedStreaming(ILogger logger, string invocationId, string hubMethod, Exception exception); + + [LoggerMessage(26, LogLevel.Trace, "Dropping {MessageType} with ID '{InvocationId}'.", EventName = "DroppingMessage")] + public static partial void DroppingMessage(ILogger logger, string messageType, string? invocationId); + + [LoggerMessage(27, LogLevel.Trace, "Received AckMessage with Sequence ID '{SequenceId}'.", EventName = "ReceivedAckMessage")] + public static partial void ReceivedAckMessage(ILogger logger, long sequenceId); + + [LoggerMessage(28, LogLevel.Trace, "Received SequenceMessage with Sequence ID '{SequenceId}'.", EventName = "ReceivedSequenceMessage")] + public static partial void ReceivedSequenceMessage(ILogger logger, long sequenceId); } diff --git a/src/SignalR/server/Core/src/Internal/GroupManager.cs b/src/SignalR/server/Core/src/Internal/GroupManager.cs index 9c17c3485406..96e5e6de33b0 100644 --- a/src/SignalR/server/Core/src/Internal/GroupManager.cs +++ b/src/SignalR/server/Core/src/Internal/GroupManager.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.SignalR.Internal; internal sealed class GroupManager : IGroupManager where THub : Hub @@ -12,12 +13,12 @@ public GroupManager(HubLifetimeManager lifetimeManager) _lifetimeManager = lifetimeManager; } - public Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) + public Task AddToGroupAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName, CancellationToken cancellationToken = default) { return _lifetimeManager.AddToGroupAsync(connectionId, groupName, cancellationToken); } - public Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) + public Task RemoveFromGroupAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName, CancellationToken cancellationToken = default) { return _lifetimeManager.RemoveFromGroupAsync(connectionId, groupName, cancellationToken); } diff --git a/src/SignalR/server/Core/src/Internal/HubCallerClients.cs b/src/SignalR/server/Core/src/Internal/HubCallerClients.cs index 0c5b0c7599a8..5163d0fbafb0 100644 --- a/src/SignalR/server/Core/src/Internal/HubCallerClients.cs +++ b/src/SignalR/server/Core/src/Internal/HubCallerClients.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.SignalR.Internal; internal sealed class HubCallerClients : IHubCallerClients @@ -16,7 +17,7 @@ internal sealed class HubCallerClients : IHubCallerClients // so we can prevent users from making blocking client calls by returning a custom ISingleClientProxy instance internal bool InvokeAllowed { get; set; } - public HubCallerClients(IHubClients hubClients, string connectionId, ChannelBasedSemaphore parallelInvokes) + public HubCallerClients(IHubClients hubClients, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, ChannelBasedSemaphore parallelInvokes) { _connectionId = connectionId; _hubClients = hubClients; @@ -45,8 +46,8 @@ public IClientProxy AllExcept(IReadOnlyList excludedConnectionIds) return _hubClients.AllExcept(excludedConnectionIds); } - IClientProxy IHubClients.Client(string connectionId) => Client(connectionId); - public ISingleClientProxy Client(string connectionId) + IClientProxy IHubClients.Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) => Client(connectionId); + public ISingleClientProxy Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { if (!InvokeAllowed) { diff --git a/src/SignalR/server/Core/src/Internal/HubClients.cs b/src/SignalR/server/Core/src/Internal/HubClients.cs index 943080388840..d3c7e4ca5dc2 100644 --- a/src/SignalR/server/Core/src/Internal/HubClients.cs +++ b/src/SignalR/server/Core/src/Internal/HubClients.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.SignalR.Internal; internal sealed class HubClients : IHubClients where THub : Hub @@ -20,8 +21,8 @@ public IClientProxy AllExcept(IReadOnlyList excludedConnectionIds) return new AllClientsExceptProxy(_lifetimeManager, excludedConnectionIds); } - IClientProxy IHubClients.Client(string connectionId) => Client(connectionId); - public ISingleClientProxy Client(string connectionId) + IClientProxy IHubClients.Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) => Client(connectionId); + public ISingleClientProxy Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { return new SingleClientProxy(_lifetimeManager, connectionId); } diff --git a/src/SignalR/server/Core/src/Internal/HubClients`T.cs b/src/SignalR/server/Core/src/Internal/HubClients`T.cs index e168174b6464..dad5e5b53c39 100644 --- a/src/SignalR/server/Core/src/Internal/HubClients`T.cs +++ b/src/SignalR/server/Core/src/Internal/HubClients`T.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.SignalR.Internal; internal sealed class HubClients : IHubClients where THub : Hub @@ -20,7 +21,7 @@ public T AllExcept(IReadOnlyList excludedConnectionIds) return TypedClientBuilder.Build(new AllClientsExceptProxy(_lifetimeManager, excludedConnectionIds)); } - public T Client(string connectionId) + public T Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { return TypedClientBuilder.Build(new SingleClientProxy(_lifetimeManager, connectionId)); } diff --git a/src/SignalR/server/Core/src/Internal/HubGroupList.cs b/src/SignalR/server/Core/src/Internal/HubGroupList.cs index 138595d5be07..3e3a142b160f 100644 --- a/src/SignalR/server/Core/src/Internal/HubGroupList.cs +++ b/src/SignalR/server/Core/src/Internal/HubGroupList.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Microsoft.AspNetCore.SignalR.Internal; @@ -28,7 +29,7 @@ public void Add(HubConnectionContext connection, string groupName) CreateOrUpdateGroupWithConnection(groupName, connection); } - public void Remove(string connectionId, string groupName) + public void Remove([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName) { if (_groups.TryGetValue(groupName, out var connections)) { @@ -43,7 +44,7 @@ public void Remove(string connectionId, string groupName) } } - public void RemoveDisconnectedConnection(string connectionId) + public void RemoveDisconnectedConnection([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { var groupNames = _groups.Where(x => x.Value.ContainsKey(connectionId)).Select(x => x.Key); foreach (var groupName in groupNames) diff --git a/src/SignalR/server/Core/src/Internal/Proxies.cs b/src/SignalR/server/Core/src/Internal/Proxies.cs index 46f42beead3c..5eec2fcbeee7 100644 --- a/src/SignalR/server/Core/src/Internal/Proxies.cs +++ b/src/SignalR/server/Core/src/Internal/Proxies.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.SignalR.Internal; internal sealed class UserProxy : IClientProxy where THub : Hub @@ -144,7 +145,7 @@ internal sealed class SingleClientProxy : ISingleClientProxy where THub : private readonly string _connectionId; private readonly HubLifetimeManager _lifetimeManager; - public SingleClientProxy(HubLifetimeManager lifetimeManager, string connectionId) + public SingleClientProxy(HubLifetimeManager lifetimeManager, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { _lifetimeManager = lifetimeManager; _connectionId = connectionId; diff --git a/src/SignalR/server/Core/src/Internal/TypedHubClients.cs b/src/SignalR/server/Core/src/Internal/TypedHubClients.cs index 723d09e7fbd8..79f031fe8264 100644 --- a/src/SignalR/server/Core/src/Internal/TypedHubClients.cs +++ b/src/SignalR/server/Core/src/Internal/TypedHubClients.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.SignalR.Internal; internal sealed class TypedHubClients : IHubCallerClients @@ -12,7 +13,7 @@ public TypedHubClients(IHubCallerClients dynamicContext) _hubClients = dynamicContext; } - public T Client(string connectionId) => TypedClientBuilder.Build(_hubClients.Client(connectionId)); + public T Client([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) => TypedClientBuilder.Build(_hubClients.Client(connectionId)); public T All => TypedClientBuilder.Build(_hubClients.All); diff --git a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj index db4c7d7e81df..b4552ea0c9f6 100644 --- a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj +++ b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj @@ -17,6 +17,7 @@ + diff --git a/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs b/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs index ed613ec73f96..628fee8b5f12 100644 --- a/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs +++ b/src/SignalR/server/SignalR/src/SignalRDependencyInjectionExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.Metrics; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; diff --git a/src/SignalR/server/SignalR/test/DefaultTransportFactoryTests.cs b/src/SignalR/server/SignalR/test/DefaultTransportFactoryTests.cs index 3daaaa4e4477..3f954b0e37a2 100644 --- a/src/SignalR/server/SignalR/test/DefaultTransportFactoryTests.cs +++ b/src/SignalR/server/SignalR/test/DefaultTransportFactoryTests.cs @@ -53,7 +53,7 @@ public void DefaultTransportFactoryCreatesRequestedTransportIfAvailable(HttpTran { var transportFactory = new DefaultTransportFactory(requestedTransport, loggerFactory: null, httpClient: new HttpClient(), httpConnectionOptions: null, accessTokenProvider: null); Assert.IsType(expectedTransportType, - transportFactory.CreateTransport(AllTransportTypes)); + transportFactory.CreateTransport(AllTransportTypes, useAck: true)); } [Theory] @@ -66,7 +66,7 @@ public void DefaultTransportFactoryThrowsIfItCannotCreateRequestedTransport(Http var transportFactory = new DefaultTransportFactory(requestedTransport, loggerFactory: null, httpClient: new HttpClient(), httpConnectionOptions: null, accessTokenProvider: null); var ex = Assert.Throws( - () => transportFactory.CreateTransport(~requestedTransport)); + () => transportFactory.CreateTransport(~requestedTransport, useAck: true)); Assert.Equal("No requested transports available on the server.", ex.Message); } @@ -77,7 +77,7 @@ public void DefaultTransportFactoryCreatesWebSocketsTransportIfAvailable() { Assert.IsType( new DefaultTransportFactory(AllTransportTypes, loggerFactory: null, httpClient: new HttpClient(), httpConnectionOptions: null, accessTokenProvider: null) - .CreateTransport(AllTransportTypes)); + .CreateTransport(AllTransportTypes, useAck: true)); } [Theory] @@ -90,7 +90,7 @@ public void DefaultTransportFactoryCreatesRequestedTransportIfAvailable_Win7(Htt { var transportFactory = new DefaultTransportFactory(requestedTransport, loggerFactory: null, httpClient: new HttpClient(), httpConnectionOptions: null, accessTokenProvider: null); Assert.IsType(expectedTransportType, - transportFactory.CreateTransport(AllTransportTypes)); + transportFactory.CreateTransport(AllTransportTypes, useAck: true)); } } @@ -103,7 +103,7 @@ public void DefaultTransportFactoryThrowsIfItCannotCreateRequestedTransport_Win7 var transportFactory = new DefaultTransportFactory(requestedTransport, loggerFactory: null, httpClient: new HttpClient(), httpConnectionOptions: null, accessTokenProvider: null); var ex = Assert.Throws( - () => transportFactory.CreateTransport(AllTransportTypes)); + () => transportFactory.CreateTransport(AllTransportTypes, useAck: true)); Assert.Equal("No requested transports available on the server.", ex.Message); } diff --git a/src/SignalR/server/SignalR/test/EndToEndTests.cs b/src/SignalR/server/SignalR/test/EndToEndTests.cs index a6e85b802510..e280f20af9ef 100644 --- a/src/SignalR/server/SignalR/test/EndToEndTests.cs +++ b/src/SignalR/server/SignalR/test/EndToEndTests.cs @@ -341,7 +341,7 @@ async Task ReceiveMessage() { logger.LogInformation("Receiving message"); // Big timeout here because it can take a while to receive all the bytes - var receivedData = await connection.Transport.Input.ReadAsync(bytes.Length).DefaultTimeout(TimeSpan.FromMinutes(2)); + var receivedData = await connection.Transport.Input.ReadAsync(bytes.Length).DefaultTimeout(); Assert.Equal(message, Encoding.UTF8.GetString(receivedData)); logger.LogInformation("Completed receive"); } @@ -685,7 +685,7 @@ private class TestTransportFactory : ITransportFactory { private ITransport _transport; - public ITransport CreateTransport(HttpTransportType availableServerTransports) + public ITransport CreateTransport(HttpTransportType availableServerTransports, bool useAck) { if (_transport == null) { diff --git a/src/SignalR/server/SignalR/test/Internal/MessageBufferTests.cs b/src/SignalR/server/SignalR/test/Internal/MessageBufferTests.cs new file mode 100644 index 000000000000..823470c29d81 --- /dev/null +++ b/src/SignalR/server/SignalR/test/Internal/MessageBufferTests.cs @@ -0,0 +1,323 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Pipelines; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.SignalR.Internal; +using Microsoft.AspNetCore.SignalR.Protocol; +using Microsoft.AspNetCore.Testing; + +namespace Microsoft.AspNetCore.SignalR.Tests.Internal; + +public class MessageBufferTests +{ + [Fact] + public async Task CanWriteNonBufferedMessagesWithoutBlocking() + { + var protocol = new JsonHubProtocol(); + var connection = new TestConnectionContext(); + var pipes = DuplexPipe.CreateConnectionPair(new PipeOptions(), new PipeOptions()); + connection.Transport = pipes.Transport; + using var messageBuffer = new MessageBuffer(connection, protocol); + + for (var i = 0; i < 100; i++) + { + await messageBuffer.WriteAsync(new SerializedHubMessage(PingMessage.Instance), default).DefaultTimeout(); + } + + var count = 0; + while (count < 100) + { + var res = await pipes.Application.Input.ReadAsync().DefaultTimeout(); + + var buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out var message)); + Assert.IsType(message); + + pipes.Application.Input.AdvanceTo(buffer.Start); + count++; + } + } + + [Fact] + public async Task WriteBlocksOnAckWhenBufferFull() + { + var protocol = new JsonHubProtocol(); + var connection = new TestConnectionContext(); + var pipes = DuplexPipe.CreateConnectionPair(new PipeOptions(), new PipeOptions(pauseWriterThreshold: 200000, resumeWriterThreshold: 100000)); + connection.Transport = pipes.Transport; + using var messageBuffer = new MessageBuffer(connection, protocol); + + await messageBuffer.WriteAsync(new SerializedHubMessage(new InvocationMessage("t", new object[] { new byte[100000] })), default); + + var writeTask = messageBuffer.WriteAsync(new SerializedHubMessage(new StreamItemMessage("id", null)), default); + Assert.False(writeTask.IsCompleted); + + var res = await pipes.Application.Input.ReadAsync(); + + var buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out var message)); + Assert.IsType(message); + + pipes.Application.Input.AdvanceTo(buffer.Start); + + // Write not unblocked by read, only unblocked after ack received + Assert.False(writeTask.IsCompleted); + + messageBuffer.Ack(new AckMessage(1)); + await writeTask.DefaultTimeout(); + + res = await pipes.Application.Input.ReadAsync().DefaultTimeout(); + + buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out message)); + Assert.IsType(message); + + pipes.Application.Input.AdvanceTo(buffer.Start); + } + + [Fact] + public async Task UnAckedMessageResentOnReconnect() + { + var protocol = new JsonHubProtocol(); + var connection = new TestConnectionContext(); + var pipes = DuplexPipe.CreateConnectionPair(new PipeOptions(), new PipeOptions()); + connection.Transport = pipes.Transport; + using var messageBuffer = new MessageBuffer(connection, protocol); + + await messageBuffer.WriteAsync(new SerializedHubMessage(new StreamItemMessage("id", null)), default); + + var res = await pipes.Application.Input.ReadAsync(); + + var buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out var message)); + Assert.IsType(message); + + pipes.Application.Input.AdvanceTo(buffer.Start); + + DuplexPipe.UpdateConnectionPair(ref pipes, connection); + messageBuffer.Resend(); + + // Any message except SequenceMessage will be ignored until a SequenceMessage is received + Assert.False(messageBuffer.ShouldProcessMessage(PingMessage.Instance)); + Assert.False(messageBuffer.ShouldProcessMessage(CompletionMessage.WithResult("1", null))); + Assert.True(messageBuffer.ShouldProcessMessage(new SequenceMessage(1))); + + res = await pipes.Application.Input.ReadAsync(); + + buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out message)); + var seqMessage = Assert.IsType(message); + Assert.Equal(1, seqMessage.SequenceId); + + pipes.Application.Input.AdvanceTo(buffer.Start); + + res = await pipes.Application.Input.ReadAsync(); + + buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out message)); + Assert.IsType(message); + + pipes.Application.Input.AdvanceTo(buffer.Start); + + messageBuffer.ResetSequence(new SequenceMessage(1)); + + Assert.True(messageBuffer.ShouldProcessMessage(PingMessage.Instance)); + Assert.True(messageBuffer.ShouldProcessMessage(CompletionMessage.WithResult("1", null))); + } + + [Fact] + public async Task AckedMessageNotResentOnReconnect() + { + var protocol = new JsonHubProtocol(); + var connection = new TestConnectionContext(); + var pipes = DuplexPipe.CreateConnectionPair(new PipeOptions(), new PipeOptions()); + connection.Transport = pipes.Transport; + using var messageBuffer = new MessageBuffer(connection, protocol); + + await messageBuffer.WriteAsync(new SerializedHubMessage(new StreamItemMessage("id", null)), default); + + var res = await pipes.Application.Input.ReadAsync(); + + var buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out var message)); + Assert.IsType(message); + + pipes.Application.Input.AdvanceTo(buffer.Start); + + messageBuffer.Ack(new AckMessage(1)); + + DuplexPipe.UpdateConnectionPair(ref pipes, connection); + messageBuffer.Resend(); + + res = await pipes.Application.Input.ReadAsync(); + + buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out message)); + var seqMessage = Assert.IsType(message); + Assert.Equal(2, seqMessage.SequenceId); + + pipes.Application.Input.AdvanceTo(buffer.Start); + + await messageBuffer.WriteAsync(new SerializedHubMessage(CompletionMessage.WithResult("1", null)), default); + + res = await pipes.Application.Input.ReadAsync(); + + buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out message)); + Assert.IsType(message); + + pipes.Application.Input.AdvanceTo(buffer.Start); + } + + [Fact] + public async Task ReceiveSequenceMessageWithLargerIDThanMessagesReceived() + { + var protocol = new JsonHubProtocol(); + var connection = new TestConnectionContext(); + var pipes = DuplexPipe.CreateConnectionPair(new PipeOptions(), new PipeOptions()); + connection.Transport = pipes.Transport; + using var messageBuffer = new MessageBuffer(connection, protocol); + + DuplexPipe.UpdateConnectionPair(ref pipes, connection); + messageBuffer.Resend(); + + var res = await pipes.Application.Input.ReadAsync(); + + var buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out var message)); + var seqMessage = Assert.IsType(message); + Assert.Equal(1, seqMessage.SequenceId); + + pipes.Application.Input.AdvanceTo(buffer.Start); + + Assert.Throws(() => messageBuffer.ResetSequence(new SequenceMessage(2))); + } + + [Fact] + public async Task WriteManyMessagesAckSomeProperlyBuffers() + { + var protocol = new JsonHubProtocol(); + var connection = new TestConnectionContext(); + var pipes = DuplexPipe.CreateConnectionPair(new PipeOptions(), new PipeOptions()); + connection.Transport = pipes.Transport; + using var messageBuffer = new MessageBuffer(connection, protocol); + + for (var i = 0; i < 1000; i++) + { + await messageBuffer.WriteAsync(new SerializedHubMessage(new StreamItemMessage("1", null)), default); + } + + var ackNum = Random.Shared.Next(0, 1000); + messageBuffer.Ack(new AckMessage(ackNum)); + + DuplexPipe.UpdateConnectionPair(ref pipes, connection); + messageBuffer.Resend(); + + var res = await pipes.Application.Input.ReadAsync(); + + var buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out var message)); + var seqMessage = Assert.IsType(message); + Assert.Equal(ackNum + 1, seqMessage.SequenceId); + + pipes.Application.Input.AdvanceTo(buffer.Start); + + for (var i = 0; i < 1000 - ackNum; i++) + { + res = await pipes.Application.Input.ReadAsync(); + + buffer = res.Buffer; + Assert.True(protocol.TryParseMessage(ref buffer, new TestBinder(), out message)); + Assert.IsType(message); + + pipes.Application.Input.AdvanceTo(buffer.Start); + } + } +} + +internal sealed class TestConnectionContext : ConnectionContext +{ + public override string ConnectionId { get; set; } + public override IFeatureCollection Features { get; } = new FeatureCollection(); + public override IDictionary Items { get; set; } + public override IDuplexPipe Transport { get; set; } +} + +internal sealed class DuplexPipe : IDuplexPipe +{ + public DuplexPipe(PipeReader reader, PipeWriter writer) + { + Input = reader; + Output = writer; + } + + public PipeReader Input { get; } + + public PipeWriter Output { get; } + + public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions) + { + var input = new Pipe(inputOptions); + var output = new Pipe(outputOptions); + + var transportToApplication = new DuplexPipe(output.Reader, input.Writer); + var applicationToTransport = new DuplexPipe(input.Reader, output.Writer); + + return new DuplexPipePair(applicationToTransport, transportToApplication); + } + + // This class exists to work around issues with value tuple on .NET Framework + public struct DuplexPipePair + { + public IDuplexPipe Transport { get; set; } + public IDuplexPipe Application { get; set; } + + public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application) + { + Transport = transport; + Application = application; + } + } + + public static void UpdateConnectionPair(ref DuplexPipePair duplexPipePair, ConnectionContext connection) + { + var prevPipe = duplexPipePair.Application.Input; + var input = new Pipe(); + + // Add new pipe for reading from and writing to transport from app code + var transportToApplication = new DuplexPipe(duplexPipePair.Transport.Input, input.Writer); + var applicationToTransport = new DuplexPipe(input.Reader, duplexPipePair.Application.Output); + + duplexPipePair.Application = applicationToTransport; + duplexPipePair.Transport = transportToApplication; + + connection.Transport = duplexPipePair.Transport; + + // Close previous pipe with specific error that application code can catch to know a restart is occurring + prevPipe.Complete(new ConnectionResetException("")); + } +} + +internal sealed class TestBinder : IInvocationBinder +{ + public IReadOnlyList GetParameterTypes(string methodName) + { + var list = new List + { + typeof(object) + }; + return list; + } + + public Type GetReturnType(string invocationId) + { + return typeof(object); + } + + public Type GetStreamItemType(string streamId) + { + return typeof(object); + } +} diff --git a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs index 676e07bc7f1a..a076f75540cc 100644 --- a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs +++ b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Internal; @@ -42,7 +43,7 @@ public RedisChannels(string prefix, string serverName) /// /// The ID of the connection to get the channel for. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string Connection(string connectionId) + public string Connection([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { return _prefix + ":connection:" + connectionId; } diff --git a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisGroupCommand.cs b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisGroupCommand.cs index c5cfa0c4fbf2..a495e4ae9125 100644 --- a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisGroupCommand.cs +++ b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisGroupCommand.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Internal; internal readonly struct RedisGroupCommand @@ -30,7 +31,7 @@ internal readonly struct RedisGroupCommand /// public string ConnectionId { get; } - public RedisGroupCommand(int id, string serverName, GroupAction action, string groupName, string connectionId) + public RedisGroupCommand(int id, string serverName, GroupAction action, string groupName, [StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId) { Id = id; ServerName = serverName; diff --git a/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs b/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs index 86137de71d10..fbda1508f6d2 100644 --- a/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs +++ b/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs @@ -163,7 +163,7 @@ public override Task SendAllExceptAsync(string methodName, object?[] args, IRead } /// - public override Task SendConnectionAsync(string connectionId, string methodName, object?[] args, CancellationToken cancellationToken = default) + public override Task SendConnectionAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string methodName, object?[] args, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(connectionId); @@ -205,7 +205,7 @@ public override Task SendUserAsync(string userId, string methodName, object?[] a } /// - public override Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) + public override Task AddToGroupAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(connectionId); ArgumentNullException.ThrowIfNull(groupName); @@ -221,7 +221,7 @@ public override Task AddToGroupAsync(string connectionId, string groupName, Canc } /// - public override Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) + public override Task RemoveFromGroupAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(connectionId); ArgumentNullException.ThrowIfNull(groupName); @@ -342,7 +342,7 @@ await _groups.RemoveSubscriptionAsync(groupChannel, connection, this, static (st } } - private async Task SendGroupActionAndWaitForAck(string connectionId, string groupName, GroupAction action) + private async Task SendGroupActionAndWaitForAck([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string groupName, GroupAction action) { var id = Interlocked.Increment(ref _internalAckId); var ack = _ackHandler.CreateAck(id); @@ -376,7 +376,7 @@ public void Dispose() } /// - public override async Task InvokeConnectionAsync(string connectionId, string methodName, object?[] args, CancellationToken cancellationToken) + public override async Task InvokeConnectionAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, string methodName, object?[] args, CancellationToken cancellationToken) { // send thing ArgumentNullException.ThrowIfNull(connectionId); @@ -433,7 +433,7 @@ public override async Task InvokeConnectionAsync(string connectionId, stri } /// - public override Task SetConnectionResultAsync(string connectionId, CompletionMessage result) + public override Task SetConnectionResultAsync([StringSyntax(StringSyntaxAttribute.GuidFormat)] string connectionId, CompletionMessage result) { _clientResultsManager.TryCompleteResult(connectionId, result); return Task.CompletedTask; diff --git a/src/Tools/Extensions.ApiDescription.Server/src/build/Microsoft.Extensions.ApiDescription.Server.targets b/src/Tools/Extensions.ApiDescription.Server/src/build/Microsoft.Extensions.ApiDescription.Server.targets index d01604fb59e2..353c64bd72b9 100644 --- a/src/Tools/Extensions.ApiDescription.Server/src/build/Microsoft.Extensions.ApiDescription.Server.targets +++ b/src/Tools/Extensions.ApiDescription.Server/src/build/Microsoft.Extensions.ApiDescription.Server.targets @@ -57,7 +57,7 @@ <_Command Condition=" '$(ProjectAssetsFile)' != '' ">$(_Command) --assets-file "$(ProjectAssetsFile)" <_Command Condition=" '$(PlatformTarget)' != '' ">$(_Command) --platform "$(PlatformTarget)" <_Command Condition=" '$(PlatformTarget)' == '' AND '$(Platform)' != '' ">$(_Command) --platform "$(Platform)" - <_Command Condition=" '$(RuntimeIdentifier)' != '' ">$(_Command) --runtime "$(RuntimeIdentifier)" + <_Command Condition=" '$(RuntimeIdentifier)' != '' ">$(_Command) --runtime "$(RuntimeIdentifier) --self-contained" <_Command>$(_Command) $(OpenApiGenerateDocumentsOptions) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index 7fd48b57baae..2b451704f432 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -109,7 +109,8 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( CommandOption claimsOption) { var isValid = true; - var project = DevJwtCliHelpers.GetProject(projectOption.Value()); + var finder = new MsBuildProjectFinder(projectOption.Value() ?? Directory.GetCurrentDirectory()); + var project = finder.FindMsBuildProject(projectOption.Value()); if (project == null) { diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs index fdd6abff2a86..dd1811a0db1f 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -25,26 +25,10 @@ public static string GetOrSetUserSecretsId(string projectFilePath) return id; } - public static string GetProject(string projectPath = null) - { - if (projectPath is not null) - { - return projectPath; - } - - var csprojFiles = Directory.EnumerateFileSystemEntries(Directory.GetCurrentDirectory(), "*.*proj", SearchOption.TopDirectoryOnly) - .Where(f => !".xproj".Equals(Path.GetExtension(f), StringComparison.OrdinalIgnoreCase)) - .ToList(); - if (csprojFiles is [var path]) - { - return path; - } - return null; - } - public static bool GetProjectAndSecretsId(string projectPath, IReporter reporter, out string project, out string userSecretsId) { - project = GetProject(projectPath); + var finder = new MsBuildProjectFinder(projectPath ?? Directory.GetCurrentDirectory()); + project = finder.FindMsBuildProject(projectPath); userSecretsId = null; if (project == null) { diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs index 63f16cb76587..b4b814347d21 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs @@ -57,7 +57,12 @@ public void Save(string filePath) }; } - using var writer = new FileStream(filePath, FileMode.Open, FileAccess.Write); + var streamOptions = new FileStreamOptions { Access = FileAccess.Write, Mode = FileMode.Create }; + if (!OperatingSystem.IsWindows()) + { + streamOptions.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite; + } + using var writer = new FileStream(filePath, streamOptions); JsonSerializer.Serialize(writer, config, _jsonSerializerOptions); } diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs index a64fb67443c4..22789206b3fa 100644 --- a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs @@ -1,23 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Text; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Configuration.UserSecrets; using Microsoft.Extensions.Tools.Internal; -using Microsoft.AspNetCore.Authentication.JwtBearer.Tools; -using Xunit; using Xunit.Abstractions; using System.Text.RegularExpressions; using System.Text.Json; using System.Text.Json.Nodes; using System.IdentityModel.Tokens.Jwt; -using System.Reflection; -using System.Numerics; namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools.Tests; @@ -80,6 +71,25 @@ public void Create_WritesGeneratedTokenToDisk() Assert.Contains("dotnet-user-jwts", File.ReadAllText(appsettings)); } + [Fact] + public void Create_CanModifyExistingScheme() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project }); + Assert.Contains("New JWT saved", _console.GetOutput()); + var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'"); + var id = matches.SingleOrDefault().Groups[1].Value; + + var appSettings = JsonSerializer.Deserialize(File.ReadAllText(appsettings)); + Assert.Equal("dotnet-user-jwts", appSettings["Authentication"]["Schemes"]["Bearer"]["ValidIssuer"].GetValue()); + app.Run(new[] { "create", "--project", project, "--issuer", "new-issuer" }); + appSettings = JsonSerializer.Deserialize(File.ReadAllText(appsettings)); + Assert.Equal("new-issuer", appSettings["Authentication"]["Schemes"]["Bearer"]["ValidIssuer"].GetValue()); + } + [Fact] public void Print_ReturnsNothingForMissingToken() { @@ -594,7 +604,7 @@ public void Create_CanHandleNoProjectOptionProvided_WithNoProjects() var app = new Program(_console); app.Run(new[] { "create" }); - Assert.Contains("No project found at `-p|--project` path or current directory.", _console.GetOutput()); + Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput()); Assert.DoesNotContain(Resources.CreateCommand_NoAudience_Error, _console.GetOutput()); } @@ -607,7 +617,7 @@ public void Delete_CanHandleNoProjectOptionProvided_WithNoProjects() var app = new Program(_console); app.Run(new[] { "remove", "some-id" }); - Assert.Contains("No project found at `-p|--project` path or current directory.", _console.GetOutput()); + Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput()); } [Fact] @@ -619,7 +629,7 @@ public void Clear_CanHandleNoProjectOptionProvided_WithNoProjects() var app = new Program(_console); app.Run(new[] { "clear" }); - Assert.Contains("No project found at `-p|--project` path or current directory.", _console.GetOutput()); + Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput()); } [Fact] @@ -631,7 +641,19 @@ public void List_CanHandleNoProjectOptionProvided_WithNoProjects() var app = new Program(_console); app.Run(new[] { "list" }); - Assert.Contains("No project found at `-p|--project` path or current directory.", _console.GetOutput()); + Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput()); + } + + [Fact] + public void List_CanHandleProjectOptionAsPath() + { + var projectPath = _fixture.CreateProject(); + var project = Path.Combine(projectPath, "TestProject.csproj"); + + var app = new Program(_console); + app.Run(new[] { "list", "--project", projectPath }); + + Assert.Contains(Path.Combine(projectPath, project), _console.GetOutput()); } [ConditionalFact] diff --git a/src/submodules/googletest b/src/submodules/googletest index cb455a71fb23..06f44bc95104 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit cb455a71fb23303e37ce8ee5b1cde6a2c18f66a5 +Subproject commit 06f44bc951046150f1348598854b211afdcf37fc diff --git a/src/submodules/spa-templates b/src/submodules/spa-templates index 3881d5aaec1e..d60cf22dc709 160000 --- a/src/submodules/spa-templates +++ b/src/submodules/spa-templates @@ -1 +1 @@ -Subproject commit 3881d5aaec1e6eee847e516cb88e8525a3e97917 +Subproject commit d60cf22dc709aae6e651fd490a78e4b5f60a5168