diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index caa6557d49..67d59a247f 100644 --- a/.azure/pipelines/ci.yml +++ b/.azure/pipelines/ci.yml @@ -29,7 +29,9 @@ variables: - ${{ if or(eq(variables['System.TeamProject'], 'public'), in(variables['Build.Reason'], 'PullRequest')) }}: - name: _BuildArgs value: '' + jobs: +# Code check - template: jobs/default-build.yml parameters: jobName: Code_check @@ -38,6 +40,10 @@ jobs: steps: - powershell: ./eng/scripts/CodeCheck.ps1 -ci displayName: Run eng/scripts/CodeCheck.ps1 + artifacts: + - name: Code_Check_Logs + path: artifacts/log/ + publishOnError: true # Build Windows (x64/x86) - template: jobs/default-build.yml @@ -171,6 +177,7 @@ jobs: -bl:artifacts/log/build.macos.binlog $(_BuildArgs) installNodeJs: false + installJdk: false artifacts: - name: MacOS_x64_Packages path: artifacts/packages/ @@ -191,7 +198,6 @@ jobs: jobName: Linux_x64_build jobDisplayName: "Build: Linux x64" agentOs: Linux - installNodeJs: false steps: - script: ./build.sh --ci @@ -211,6 +217,7 @@ jobs: --arch x64 \ --build-installers \ --no-build-deps \ + --no-build-nodejs \ -p:OnlyPackPlatformSpecificPackages=true \ -p:BuildRuntimeArchive=false \ -p:LinuxInstallerType=deb \ @@ -224,12 +231,15 @@ jobs: --arch x64 \ --build-installers \ --no-build-deps \ + --no-build-nodejs \ -p:OnlyPackPlatformSpecificPackages=true \ -p:BuildRuntimeArchive=false \ -p:LinuxInstallerType=rpm \ -bl:artifacts/log/build.rpm.binlog \ $(_BuildArgs) displayName: Build RPM installers + installNodeJs: false + installJdk: false artifacts: - name: Linux_x64_Packages path: artifacts/packages/ @@ -260,6 +270,7 @@ jobs: -bl:artifacts/log/build.linux-arm.binlog $(_BuildArgs) installNodeJs: false + installJdk: false artifacts: - name: Linux_arm_Packages path: artifacts/packages/ @@ -290,6 +301,7 @@ jobs: -bl:artifacts/log/build.arm64.binlog $(_BuildArgs) installNodeJs: false + installJdk: false artifacts: - name: Linux_arm64_Packages path: artifacts/packages/ @@ -323,6 +335,7 @@ jobs: -bl:artifacts/log/build.musl.binlog $(_BuildArgs) installNodeJs: false + installJdk: false artifacts: - name: Linux_musl_x64_Packages path: artifacts/packages/ @@ -337,7 +350,7 @@ jobs: parameters: inputName: Linux_musl_x64 -# Build Linux Musl arm64 +# Build Linux Musl ARM64 - template: jobs/default-build.yml parameters: jobName: Linux_musl_arm64_build @@ -356,6 +369,7 @@ jobs: -bl:artifacts/log/build.musl.binlog $(_BuildArgs) installNodeJs: false + installJdk: false artifacts: - name: Linux_musl_arm64_Packages path: artifacts/packages/ @@ -499,7 +513,7 @@ jobs: version: 3.0.x installationPath: $(DotNetCoreSdkDir) includePreviewVersions: true - - script: ./eng/scripts/ci-source-build.sh --ci --configuration Release /p:BuildManaged=true + - script: ./eng/scripts/ci-source-build.sh --ci --configuration Release /p:BuildManaged=true /p:BuildNodeJs=false displayName: Run ci-source-build.sh - task: PublishBuildArtifacts@1 displayName: Upload logs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 86a87beb59..3acc059820 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,7 +14,7 @@ /src/Hosting/ @tratcher @anurse /src/Http/ @tratcher @jkotalik @anurse /src/Middleware/ @tratcher @anurse -/src/ProjectTemplates/ @ryanbrandenburg +# /src/ProjectTemplates/ @ryanbrandenburg /src/Security/ @tratcher @anurse /src/Servers/ @tratcher @jkotalik @anurse @halter73 /src/Middleware/Rewrite @jkotalik @anurse diff --git a/NuGet.config b/NuGet.config index eda7e2c3e9..d0a37a8d8e 100644 --- a/NuGet.config +++ b/NuGet.config @@ -3,11 +3,15 @@ - + + + + + diff --git a/build.ps1 b/build.ps1 index 018c3970b7..17020044ed 100644 --- a/build.ps1 +++ b/build.ps1 @@ -183,7 +183,7 @@ elseif ($Projects) { } # When adding new sub-group build flags, add them to this check. elseif((-not $BuildNative) -and (-not $BuildManaged) -and (-not $BuildNodeJS) -and (-not $BuildInstallers) -and (-not $BuildJava)) { - Write-Warning "No default group of projects was specified, so building the 'managed' subsets of projects. Run ``build.cmd -help`` for more details." + Write-Warning "No default group of projects was specified, so building the 'managed' and its dependent subsets of projects. Run ``build.cmd -help`` for more details." # This goal of this is to pick a sensible default for `build.cmd` with zero arguments. # Now that we support subfolder invokations of build.cmd, we will be pushing to have build.cmd build everything (-all) by default @@ -191,6 +191,25 @@ elseif((-not $BuildNative) -and (-not $BuildManaged) -and (-not $BuildNodeJS) -a $BuildManaged = $true } +if ($BuildManaged -or ($All -and (-not $NoBuildManaged))) { + if ((-not $BuildNodeJS) -and (-not $NoBuildNodeJS)) { + $node = Get-Command node -ErrorAction Ignore -CommandType Application + + if ($node) { + $nodeHome = Split-Path -Parent (Split-Path -Parent $node.Path) + Write-Host -f Magenta "Building of C# project is enabled and has dependencies on NodeJS projects. Building of NodeJS projects is enabled since node is detected in $nodeHome." + } + else { + Write-Host -f Magenta "Building of NodeJS projects is disabled since node is not detected on Path and no BuildNodeJs or NoBuildNodeJs setting is set explicitly." + $NoBuildNodeJS = $true + } + } + + if ($NoBuildNodeJS){ + Write-Warning "Some managed projects depend on NodeJS projects. Building NodeJS is disabled so the managed projects will fallback to using the output from previous builds. The output may not be correct or up to date." + } +} + if ($BuildInstallers) { $MSBuildArguments += "/p:BuildInstallers=true" } if ($BuildManaged) { $MSBuildArguments += "/p:BuildManaged=true" } if ($BuildNative) { $MSBuildArguments += "/p:BuildNative=true" } diff --git a/build.sh b/build.sh index c170ac1844..ad4ce2c1c8 100755 --- a/build.sh +++ b/build.sh @@ -213,7 +213,7 @@ elif [ ! -z "$build_projects" ]; then elif [ -z "$build_managed" ] && [ -z "$build_nodejs" ] && [ -z "$build_java" ] && [ -z "$build_native" ] && [ -z "$build_installers" ]; then # This goal of this is to pick a sensible default for `build.sh` with zero arguments. # We believe the most common thing our contributors will work on is C#, so if no other build group was picked, build the C# projects. - __warn "No default group of projects was specified, so building the 'managed' subset of projects. Run ``build.sh --help`` for more details." + __warn "No default group of projects was specified, so building the 'managed' and its dependent subset of projects. Run ``build.sh --help`` for more details." build_managed=true fi @@ -221,6 +221,21 @@ if [ "$build_deps" = false ]; then msbuild_args[${#msbuild_args[*]}]="-p:BuildProjectReferences=false" fi +if [ "$build_managed" = true ] || (["$build_all" = true ] && [ "$build_managed" != false ]); then + if [ -z "$build_nodejs" ]; then + if [ -x "$(command -v node)" ]; then + __warn "Building of C# project is enabled and has dependencies on NodeJS projects. Building of NodeJS projects is enabled since node is detected on PATH." + else + __warn "Building of NodeJS projects is disabled since node is not detected on Path and no BuildNodeJs or NoBuildNodeJs setting is set explicitly." + build_nodejs=false + fi + fi + + if [ "$build_nodejs" = false ]; then + __warn "Some managed projects depend on NodeJS projects. Building NodeJS is disabled so the managed projects will fallback to using the output from previous builds. The output may not be correct or up to date." + fi +fi + # Only set these MSBuild properties if they were explicitly set by build parameters. [ ! -z "$build_java" ] && msbuild_args[${#msbuild_args[*]}]="-p:BuildJava=$build_java" [ ! -z "$build_native" ] && msbuild_args[${#msbuild_args[*]}]="-p:BuildNative=$build_native" diff --git a/docs/BuildFromSource.md b/docs/BuildFromSource.md index 53938c189b..7740ec6140 100644 --- a/docs/BuildFromSource.md +++ b/docs/BuildFromSource.md @@ -97,14 +97,14 @@ The cause of this problem is that the solution you are using does not include th ``` ### Common error: Unable to locate the .NET Core SDK - + Executing `.\restore.cmd` or `.\build.cmd` may produce these errors: > error : Unable to locate the .NET Core SDK. Check that it is installed and that the version specified in global.json (if any) matches the installed version. > error MSB4236: The SDK 'Microsoft.NET.Sdk' specified could not be found. In most cases, this is because the option _Use previews of the .NET Core SDK_ in VS2019 is not checked. Start Visual Studio, go to _Tools > Options_ and check _Use previews of the .NET Core SDK_ under _Environment > Preview Features_. - + ## Building with Visual Studio Code Using Visual Studio Code with this repo requires setting environment variables on command line first. @@ -138,6 +138,8 @@ On macOS/Linux: ./build.sh ``` +By default, all of the C# projects are built. Some C# projects requires NodeJS to be installed to compile JavaScript assets which are then checked in as source. If NodeJS is detected on the path, the NodeJS projects will be compiled as part of building C# projects. If NodeJS is not detected on the path, the JavaScript assets checked in previously will be used instead. To disable building NodeJS projects, specify /p:BuildNodeJs=false on the command line. + ### Using `dotnet` on command line in this repo Because we are using pre-release versions of .NET Core, you have to set a handful of environment variables diff --git a/eng/Build.props b/eng/Build.props index eba7a2ac91..ef1a7776b6 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -4,14 +4,16 @@ - true - true - true - true + true + true + true + true + + - @@ -102,6 +104,7 @@ + Microsoft.AspNetCore.Hosting; diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 1e7f2fa1cf..2c8002b211 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -9,289 +9,289 @@ --> - + https://github.com/aspnet/Blazor - dd092c2236cf9375b50e19295dd2faf36e6221f6 + b2c48dd8c9099f71908fac26089cbea2c76d06a1 - + https://github.com/aspnet/AspNetCore-Tooling - 65994cb6ffd2d8da87db74e2b3e34cb5e350aff0 + 448a88e86d20fd9315901f663318d64c9c6841bf - + https://github.com/aspnet/AspNetCore-Tooling - 65994cb6ffd2d8da87db74e2b3e34cb5e350aff0 + 448a88e86d20fd9315901f663318d64c9c6841bf - + https://github.com/aspnet/AspNetCore-Tooling - 65994cb6ffd2d8da87db74e2b3e34cb5e350aff0 + 448a88e86d20fd9315901f663318d64c9c6841bf - + https://github.com/aspnet/AspNetCore-Tooling - 65994cb6ffd2d8da87db74e2b3e34cb5e350aff0 + 448a88e86d20fd9315901f663318d64c9c6841bf - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/EntityFrameworkCore - 49f9f7632c742108e5652f182922cc35c19c9162 + 07ed34e80585ca9575ea0265921d42a203193b21 - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe https://github.com/dotnet/corefx @@ -412,25 +412,25 @@ https://github.com/dotnet/corefx 80f411d58df8338ccd9430900b541a037a9cb383 - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe - + https://github.com/dotnet/arcade - a190d4865fe3c86a168ec49c4fc61c90c96ae051 + b1c2f33f0cef32d1df6e7f388017fd6761d3fcad - + https://github.com/dotnet/arcade - a190d4865fe3c86a168ec49c4fc61c90c96ae051 + b1c2f33f0cef32d1df6e7f388017fd6761d3fcad - + https://github.com/dotnet/arcade - a190d4865fe3c86a168ec49c4fc61c90c96ae051 + b1c2f33f0cef32d1df6e7f388017fd6761d3fcad - + https://github.com/aspnet/Extensions - 54d000fda95c2c1f05b13a2e910fc91994da8eb8 + 86469ee35cf718e0122f16f52b486303dcfbb1fe https://github.com/dotnet/roslyn diff --git a/eng/Versions.props b/eng/Versions.props index 4f48ab6d95..d1befbd14a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -55,7 +55,7 @@ --> - 1.0.0-beta.19369.2 + 1.0.0-beta.19404.1 3.3.0-beta3-19401-01 @@ -90,82 +90,82 @@ 3.0.0-preview8.19378.8 - 5.0.0-alpha1.19403.2 + 5.0.0-alpha1.19405.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 - 3.0.0-preview9.19401.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 + 3.0.0-preview9.19405.2 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 - 3.0.0-preview9.19402.9 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 + 3.0.0-preview9.19405.13 - 5.0.0-alpha1.19381.2 - 5.0.0-alpha1.19381.2 - 5.0.0-alpha1.19381.2 - 5.0.0-alpha1.19381.2 + 5.0.0-alpha1.19407.1 + 5.0.0-alpha1.19407.1 + 5.0.0-alpha1.19407.1 + 5.0.0-alpha1.19407.1 - - $(RestoreSources); - https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json; - https://dotnetfeed.blob.core.windows.net/aspnet-blazor/index.json; - https://dotnetfeed.blob.core.windows.net/aspnet-extensions/index.json; - https://dotnetfeed.blob.core.windows.net/aspnet-entityframeworkcore/index.json; - https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore-tooling/index.json; - https://grpc.jfrog.io/grpc/api/nuget/v3/grpc-nuget-dev; - - - $(RestoreSources); - https://dotnet.myget.org/F/roslyn/api/v3/index.json; - - - - $(RestoreSources); - https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; - - + https://dotnetcli.blob.core.windows.net/dotnet/ diff --git a/eng/Workarounds.props b/eng/Workarounds.props index 0fa6edaeab..a56cf44b46 100644 --- a/eng/Workarounds.props +++ b/eng/Workarounds.props @@ -8,25 +8,6 @@ portable - - - $(RepoRoot)NuGet.config - - - - - - $(RestoreSources); - https://dotnet.myget.org/F/roslyn-tools/api/v3/index.json; - - - false diff --git a/eng/Workarounds.targets b/eng/Workarounds.targets index ed0324f744..f5c74cdd43 100644 --- a/eng/Workarounds.targets +++ b/eng/Workarounds.targets @@ -29,12 +29,6 @@ - - - $(RestoreSources); - https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; - - diff --git a/eng/common/init-tools-native.ps1 b/eng/common/init-tools-native.ps1 index 9d18645f45..8cf18bcfeb 100644 --- a/eng/common/init-tools-native.ps1 +++ b/eng/common/init-tools-native.ps1 @@ -98,12 +98,18 @@ try { } Write-Verbose "Installing $ToolName version $ToolVersion" - Write-Verbose "Executing '$InstallerPath $LocalInstallerArguments'" + Write-Verbose "Executing '$InstallerPath $($LocalInstallerArguments.Keys.ForEach({"-$_ '$($LocalInstallerArguments.$_)'"}) -join ' ')'" & $InstallerPath @LocalInstallerArguments if ($LASTEXITCODE -Ne "0") { $errMsg = "$ToolName installation failed" if ((Get-Variable 'DoNotAbortNativeToolsInstallationOnFailure' -ErrorAction 'SilentlyContinue') -and $DoNotAbortNativeToolsInstallationOnFailure) { - Write-Warning $errMsg + $showNativeToolsWarning = $true + if ((Get-Variable 'DoNotDisplayNativeToolsInstallationWarnings' -ErrorAction 'SilentlyContinue') -and $DoNotDisplayNativeToolsInstallationWarnings) { + $showNativeToolsWarning = $false + } + if ($showNativeToolsWarning) { + Write-Warning $errMsg + } $toolInstallationFailure = $true } else { Write-Error $errMsg diff --git a/eng/common/init-tools-native.sh b/eng/common/init-tools-native.sh index 5f2e77f448..4dafaaca13 100755 --- a/eng/common/init-tools-native.sh +++ b/eng/common/init-tools-native.sh @@ -70,8 +70,7 @@ function ReadGlobalJsonNativeTools { # Only extract the contents of the object. local native_tools_list=$(echo $native_tools_section | awk -F"[{}]" '{print $2}') native_tools_list=${native_tools_list//[\" ]/} - native_tools_list=${native_tools_list//,/$'\n'} - native_tools_list="$(echo -e "${native_tools_list}" | tr -d '[[:space:]]')" + native_tools_list=$( echo "$native_tools_list" | sed 's/\s//g' | sed 's/,/\n/g' ) local old_IFS=$IFS while read -r line; do @@ -108,6 +107,7 @@ else installer_command+=" --baseuri $base_uri" installer_command+=" --installpath $install_bin" installer_command+=" --version $tool_version" + echo $installer_command if [[ $force = true ]]; then installer_command+=" --force" diff --git a/eng/common/internal-feed-operations.sh b/eng/common/internal-feed-operations.sh old mode 100644 new mode 100755 diff --git a/eng/common/native/CommonLibrary.psm1 b/eng/common/native/CommonLibrary.psm1 index 7a34c7e8a4..2a08d5246e 100644 --- a/eng/common/native/CommonLibrary.psm1 +++ b/eng/common/native/CommonLibrary.psm1 @@ -59,9 +59,38 @@ function DownloadAndExtract { -Verbose:$Verbose if ($UnzipStatus -Eq $False) { - Write-Error "Unzip failed" - return $False + # Retry Download one more time with Force=true + $DownloadRetryStatus = CommonLibrary\Get-File -Uri $Uri ` + -Path $TempToolPath ` + -DownloadRetries 1 ` + -RetryWaitTimeInSeconds $RetryWaitTimeInSeconds ` + -Force:$True ` + -Verbose:$Verbose + + if ($DownloadRetryStatus -Eq $False) { + Write-Error "Last attempt of download failed as well" + return $False + } + + # Retry unzip again one more time with Force=true + $UnzipRetryStatus = CommonLibrary\Expand-Zip -ZipPath $TempToolPath ` + -OutputDirectory $InstallDirectory ` + -Force:$True ` + -Verbose:$Verbose + if ($UnzipRetryStatus -Eq $False) + { + Write-Error "Last attempt of unzip failed as well" + # Clean up partial zips and extracts + if (Test-Path $TempToolPath) { + Remove-Item $TempToolPath -Force + } + if (Test-Path $InstallDirectory) { + Remove-Item $InstallDirectory -Force -Recurse + } + return $False + } } + return $True } diff --git a/eng/common/native/install-cmake-test.sh b/eng/common/native/install-cmake-test.sh new file mode 100755 index 0000000000..53ddf4e686 --- /dev/null +++ b/eng/common/native/install-cmake-test.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +. $scriptroot/common-library.sh + +base_uri= +install_path= +version= +clean=false +force=false +download_retries=5 +retry_wait_time_seconds=30 + +while (($# > 0)); do + lowerI="$(echo $1 | awk '{print tolower($0)}')" + case $lowerI in + --baseuri) + base_uri=$2 + shift 2 + ;; + --installpath) + install_path=$2 + shift 2 + ;; + --version) + version=$2 + shift 2 + ;; + --clean) + clean=true + shift 1 + ;; + --force) + force=true + shift 1 + ;; + --downloadretries) + download_retries=$2 + shift 2 + ;; + --retrywaittimeseconds) + retry_wait_time_seconds=$2 + shift 2 + ;; + --help) + echo "Common settings:" + echo " --baseuri Base file directory or Url wrom which to acquire tool archives" + echo " --installpath Base directory to install native tool to" + echo " --clean Don't install the tool, just clean up the current install of the tool" + echo " --force Force install of tools even if they previously exist" + echo " --help Print help and exit" + echo "" + echo "Advanced settings:" + echo " --downloadretries Total number of retry attempts" + echo " --retrywaittimeseconds Wait time between retry attempts in seconds" + echo "" + exit 0 + ;; + esac +done + +tool_name="cmake-test" +tool_os=$(GetCurrentOS) +tool_folder=$(echo $tool_os | awk '{print tolower($0)}') +tool_arch="x86_64" +tool_name_moniker="$tool_name-$version-$tool_os-$tool_arch" +tool_install_directory="$install_path/$tool_name/$version" +tool_file_path="$tool_install_directory/$tool_name_moniker/bin/$tool_name" +shim_path="$install_path/$tool_name.sh" +uri="${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz" + +# Clean up tool and installers +if [[ $clean = true ]]; then + echo "Cleaning $tool_install_directory" + if [[ -d $tool_install_directory ]]; then + rm -rf $tool_install_directory + fi + + echo "Cleaning $shim_path" + if [[ -f $shim_path ]]; then + rm -rf $shim_path + fi + + tool_temp_path=$(GetTempPathFileName $uri) + echo "Cleaning $tool_temp_path" + if [[ -f $tool_temp_path ]]; then + rm -rf $tool_temp_path + fi + + exit 0 +fi + +# Install tool +if [[ -f $tool_file_path ]] && [[ $force = false ]]; then + echo "$tool_name ($version) already exists, skipping install" + exit 0 +fi + +DownloadAndExtract $uri $tool_install_directory $force $download_retries $retry_wait_time_seconds + +if [[ $? != 0 ]]; then + echo "Installation failed" >&2 + exit 1 +fi + +# Generate Shim +# Always rewrite shims so that we are referencing the expected version +NewScriptShim $shim_path $tool_file_path true + +if [[ $? != 0 ]]; then + echo "Shim generation failed" >&2 + exit 1 +fi + +exit 0 \ No newline at end of file diff --git a/eng/common/native/install-cmake.sh b/eng/common/native/install-cmake.sh index 293af6017d..5f1a182fa9 100755 --- a/eng/common/native/install-cmake.sh +++ b/eng/common/native/install-cmake.sh @@ -69,7 +69,7 @@ tool_name_moniker="$tool_name-$version-$tool_os-$tool_arch" tool_install_directory="$install_path/$tool_name/$version" tool_file_path="$tool_install_directory/$tool_name_moniker/bin/$tool_name" shim_path="$install_path/$tool_name.sh" -uri="${base_uri}/$tool_folder/cmake/$tool_name_moniker.tar.gz" +uri="${base_uri}/$tool_folder/$tool_name/$tool_name_moniker.tar.gz" # Clean up tool and installers if [[ $clean = true ]]; then diff --git a/eng/common/performance/performance-setup.sh b/eng/common/performance/performance-setup.sh old mode 100644 new mode 100755 diff --git a/eng/common/pipeline-logging-functions.sh b/eng/common/pipeline-logging-functions.sh old mode 100644 new mode 100755 diff --git a/eng/common/post-build/darc-gather-drop.ps1 b/eng/common/post-build/darc-gather-drop.ps1 new file mode 100644 index 0000000000..93a0bd8328 --- /dev/null +++ b/eng/common/post-build/darc-gather-drop.ps1 @@ -0,0 +1,35 @@ +param( + [Parameter(Mandatory=$true)][int] $BarBuildId, # ID of the build which assets should be downloaded + [Parameter(Mandatory=$true)][string] $DropLocation, # Where the assets should be downloaded to + [Parameter(Mandatory=$true)][string] $MaestroApiAccessToken, # Token used to access Maestro API + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = "https://maestro-prod.westus2.cloudapp.azure.com", # Maestro API URL + [Parameter(Mandatory=$false)][string] $MaestroApiVersion = "2019-01-16" # Version of Maestro API to use +) + +. $PSScriptRoot\post-build-utils.ps1 + +try { + Write-Host "Installing DARC ..." + + . $PSScriptRoot\..\darc-init.ps1 + $exitCode = $LASTEXITCODE + + if ($exitCode -ne 0) { + Write-PipelineTaskError "Something failed while running 'darc-init.ps1'. Check for errors above. Exiting now..." + ExitWithExitCode $exitCode + } + + darc gather-drop --non-shipping ` + --continue-on-error ` + --id $BarBuildId ` + --output-dir $DropLocation ` + --bar-uri $MaestroApiEndpoint ` + --password $MaestroApiAccessToken ` + --latest-location +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/nuget-validation.ps1 b/eng/common/post-build/nuget-validation.ps1 index 1bdced1e30..78ed0d540f 100644 --- a/eng/common/post-build/nuget-validation.ps1 +++ b/eng/common/post-build/nuget-validation.ps1 @@ -6,10 +6,7 @@ param( [Parameter(Mandatory=$true)][string] $ToolDestinationPath # Where the validation tool should be downloaded to ) -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2.0 - -. $PSScriptRoot\..\tools.ps1 +. $PSScriptRoot\post-build-utils.ps1 try { $url = "https://raw.githubusercontent.com/NuGet/NuGetGallery/jver-verify/src/VerifyMicrosoftPackage/verify.ps1" diff --git a/eng/common/post-build/post-build-utils.ps1 b/eng/common/post-build/post-build-utils.ps1 new file mode 100644 index 0000000000..551ae113f8 --- /dev/null +++ b/eng/common/post-build/post-build-utils.ps1 @@ -0,0 +1,90 @@ +# Most of the functions in this file require the variables `MaestroApiEndPoint`, +# `MaestroApiVersion` and `MaestroApiAccessToken` to be globally available. + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 + +# `tools.ps1` checks $ci to perform some actions. Since the post-build +# scripts don't necessarily execute in the same agent that run the +# build.ps1/sh script this variable isn't automatically set. +$ci = $true +. $PSScriptRoot\..\tools.ps1 + +function Create-MaestroApiRequestHeaders([string]$ContentType = "application/json") { + Validate-MaestroVars + + $headers = New-Object 'System.Collections.Generic.Dictionary[[String],[String]]' + $headers.Add('Accept', $ContentType) + $headers.Add('Authorization',"Bearer $MaestroApiAccessToken") + return $headers +} + +function Get-MaestroChannel([int]$ChannelId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders + $apiEndpoint = "$MaestroApiEndPoint/api/channels/${ChannelId}?api-version=$MaestroApiVersion" + + $result = try { Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Get-MaestroBuild([int]$BuildId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/builds/${BuildId}?api-version=$MaestroApiVersion" + + $result = try { return Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Get-MaestroSubscriptions([string]$SourceRepository, [int]$ChannelId) { + Validate-MaestroVars + + $SourceRepository = [System.Web.HttpUtility]::UrlEncode($SourceRepository) + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/subscriptions?sourceRepository=$SourceRepository&channelId=$ChannelId&api-version=$MaestroApiVersion" + + $result = try { Invoke-WebRequest -Method Get -Uri $apiEndpoint -Headers $apiHeaders | ConvertFrom-Json } catch { Write-Host "Error: $_" } + return $result +} + +function Trigger-Subscription([string]$SubscriptionId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/subscriptions/$SubscriptionId/trigger?api-version=$MaestroApiVersion" + Invoke-WebRequest -Uri $apiEndpoint -Headers $apiHeaders -Method Post | Out-Null +} + +function Assign-BuildToChannel([int]$BuildId, [int]$ChannelId) { + Validate-MaestroVars + + $apiHeaders = Create-MaestroApiRequestHeaders -AuthToken $MaestroApiAccessToken + $apiEndpoint = "$MaestroApiEndPoint/api/channels/${ChannelId}/builds/${BuildId}?api-version=$MaestroApiVersion" + Invoke-WebRequest -Method Post -Uri $apiEndpoint -Headers $apiHeaders | Out-Null +} + +function Validate-MaestroVars { + try { + Get-Variable MaestroApiEndPoint -Scope Global | Out-Null + Get-Variable MaestroApiVersion -Scope Global | Out-Null + Get-Variable MaestroApiAccessToken -Scope Global | Out-Null + + if (!($MaestroApiEndPoint -Match "^http[s]?://maestro-(int|prod).westus2.cloudapp.azure.com$")) { + Write-PipelineTaskError "MaestroApiEndPoint is not a valid Maestro URL. '$MaestroApiEndPoint'" + ExitWithExitCode 1 + } + + if (!($MaestroApiVersion -Match "^[0-9]{4}-[0-9]{2}-[0-9]{2}$")) { + Write-PipelineTaskError "MaestroApiVersion does not match a version string in the format yyyy-MM-DD. '$MaestroApiVersion'" + ExitWithExitCode 1 + } + } + catch { + Write-PipelineTaskError "Error: Variables `MaestroApiEndPoint`, `MaestroApiVersion` and `MaestroApiAccessToken` are required while using this script." + Write-Host $_ + ExitWithExitCode 1 + } +} diff --git a/eng/common/post-build/promote-build.ps1 b/eng/common/post-build/promote-build.ps1 index 84a608fa56..e5ae85f251 100644 --- a/eng/common/post-build/promote-build.ps1 +++ b/eng/common/post-build/promote-build.ps1 @@ -1,30 +1,25 @@ param( [Parameter(Mandatory=$true)][int] $BuildId, [Parameter(Mandatory=$true)][int] $ChannelId, - [Parameter(Mandatory=$true)][string] $BarToken, - [string] $MaestroEndpoint = "https://maestro-prod.westus2.cloudapp.azure.com", - [string] $ApiVersion = "2019-01-16" + [Parameter(Mandatory=$true)][string] $MaestroApiAccessToken, + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = "https://maestro-prod.westus2.cloudapp.azure.com", + [Parameter(Mandatory=$false)][string] $MaestroApiVersion = "2019-01-16" ) -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2.0 - -. $PSScriptRoot\..\tools.ps1 - -function Get-Headers([string]$accept, [string]$barToken) { - $headers = New-Object 'System.Collections.Generic.Dictionary[[String],[String]]' - $headers.Add('Accept',$accept) - $headers.Add('Authorization',"Bearer $barToken") - return $headers -} +. $PSScriptRoot\post-build-utils.ps1 try { - $maestroHeaders = Get-Headers 'application/json' $BarToken + # Check that the channel we are going to promote the build to exist + $channelInfo = Get-MaestroChannel -ChannelId $ChannelId + + if (!$channelInfo) { + Write-Host "Channel with BAR ID $ChannelId was not found in BAR!" + ExitWithExitCode 1 + } # Get info about which channels the build has already been promoted to - $getBuildApiEndpoint = "$MaestroEndpoint/api/builds/${BuildId}?api-version=$ApiVersion" - $buildInfo = Invoke-WebRequest -Method Get -Uri $getBuildApiEndpoint -Headers $maestroHeaders | ConvertFrom-Json - + $buildInfo = Get-MaestroBuild -BuildId $BuildId + if (!$buildInfo) { Write-Host "Build with BAR ID $BuildId was not found in BAR!" ExitWithExitCode 1 @@ -40,10 +35,10 @@ try { } } - Write-Host "Build not present in channel $ChannelId. Promoting build ... " + Write-Host "Promoting build '$BuildId' to channel '$ChannelId'." + + Assign-BuildToChannel -BuildId $BuildId -ChannelId $ChannelId - $promoteBuildApiEndpoint = "$maestroEndpoint/api/channels/${ChannelId}/builds/${BuildId}?api-version=$ApiVersion" - Invoke-WebRequest -Method Post -Uri $promoteBuildApiEndpoint -Headers $maestroHeaders Write-Host "done." } catch { diff --git a/eng/common/post-build/setup-maestro-vars.ps1 b/eng/common/post-build/setup-maestro-vars.ps1 new file mode 100644 index 0000000000..d7f64dc63c --- /dev/null +++ b/eng/common/post-build/setup-maestro-vars.ps1 @@ -0,0 +1,26 @@ +param( + [Parameter(Mandatory=$true)][string] $ReleaseConfigsPath # Full path to ReleaseConfigs.txt asset +) + +. $PSScriptRoot\post-build-utils.ps1 + +try { + $Content = Get-Content $ReleaseConfigsPath + + $BarId = $Content | Select -Index 0 + + $Channels = "" + $Content | Select -Index 1 | ForEach-Object { $Channels += "$_ ," } + + $IsStableBuild = $Content | Select -Index 2 + + Write-PipelineSetVariable -Name 'BARBuildId' -Value $BarId + Write-PipelineSetVariable -Name 'InitialChannels' -Value "$Channels" + Write-PipelineSetVariable -Name 'IsStableBuild' -Value $IsStableBuild +} +catch { + Write-Host $_ + Write-Host $_.Exception + Write-Host $_.ScriptStackTrace + ExitWithExitCode 1 +} diff --git a/eng/common/post-build/sourcelink-validation.ps1 b/eng/common/post-build/sourcelink-validation.ps1 index 8abd684e9e..41e01ae6e6 100644 --- a/eng/common/post-build/sourcelink-validation.ps1 +++ b/eng/common/post-build/sourcelink-validation.ps1 @@ -6,10 +6,7 @@ param( [Parameter(Mandatory=$true)][string] $SourcelinkCliVersion # Version of SourceLink CLI to use ) -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2.0 - -. $PSScriptRoot\..\tools.ps1 +. $PSScriptRoot\post-build-utils.ps1 # Cache/HashMap (File -> Exist flag) used to consult whether a file exist # in the repository at a specific commit point. This is populated by inserting @@ -200,21 +197,27 @@ function ValidateSourceLinkLinks { } } -function CheckExitCode ([string]$stage) { - $exitCode = $LASTEXITCODE - if ($exitCode -ne 0) { - Write-PipelineTaskError "Something failed while '$stage'. Check for errors above. Exiting now..." - ExitWithExitCode $exitCode +function InstallSourcelinkCli { + $sourcelinkCliPackageName = "sourcelink" + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list --global + + if (($toolList -like "*$sourcelinkCliPackageName*") -and ($toolList -like "*$sourcelinkCliVersion*")) { + Write-Host "SourceLink CLI version $sourcelinkCliVersion is already installed." + } + else { + Write-Host "Installing SourceLink CLI version $sourcelinkCliVersion..." + Write-Host "You may need to restart your command window if this is the first dotnet tool you have installed." + & "$dotnet" tool install $sourcelinkCliPackageName --version $sourcelinkCliVersion --verbosity "minimal" --global } } try { - Write-Host "Installing SourceLink CLI..." - Get-Location - . $PSScriptRoot\sourcelink-cli-init.ps1 -sourcelinkCliVersion $SourcelinkCliVersion - CheckExitCode "Running sourcelink-cli-init" + InstallSourcelinkCli - Measure-Command { ValidateSourceLinkLinks } + ValidateSourceLinkLinks } catch { Write-Host $_ diff --git a/eng/common/post-build/symbols-validation.ps1 b/eng/common/post-build/symbols-validation.ps1 index 69456854e0..d5ec51b150 100644 --- a/eng/common/post-build/symbols-validation.ps1 +++ b/eng/common/post-build/symbols-validation.ps1 @@ -4,10 +4,7 @@ param( [Parameter(Mandatory=$true)][string] $DotnetSymbolVersion # Version of dotnet symbol to use ) -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2.0 - -. $PSScriptRoot\..\tools.ps1 +. $PSScriptRoot\post-build-utils.ps1 Add-Type -AssemblyName System.IO.Compression.FileSystem @@ -162,19 +159,25 @@ function CheckSymbolsAvailable { } } -function CheckExitCode ([string]$stage) { - $exitCode = $LASTEXITCODE - if ($exitCode -ne 0) { - Write-PipelineTaskError "Something failed while '$stage'. Check for errors above. Exiting now..." - ExitWithExitCode $exitCode +function Installdotnetsymbol { + $dotnetsymbolPackageName = "dotnet-symbol" + + $dotnetRoot = InitializeDotNetCli -install:$true + $dotnet = "$dotnetRoot\dotnet.exe" + $toolList = & "$dotnet" tool list --global + + if (($toolList -like "*$dotnetsymbolPackageName*") -and ($toolList -like "*$dotnetsymbolVersion*")) { + Write-Host "dotnet-symbol version $dotnetsymbolVersion is already installed." + } + else { + Write-Host "Installing dotnet-symbol version $dotnetsymbolVersion..." + Write-Host "You may need to restart your command window if this is the first dotnet tool you have installed." + & "$dotnet" tool install $dotnetsymbolPackageName --version $dotnetsymbolVersion --verbosity "minimal" --global } } try { - Write-Host "Installing dotnet symbol ..." - Get-Location - . $PSScriptRoot\dotnetsymbol-init.ps1 -dotnetsymbolVersion $DotnetSymbolVersion - CheckExitCode "Running dotnetsymbol-init" + Installdotnetsymbol CheckSymbolsAvailable } diff --git a/eng/common/post-build/trigger-subscriptions.ps1 b/eng/common/post-build/trigger-subscriptions.ps1 index 1a91dab037..926d5b4551 100644 --- a/eng/common/post-build/trigger-subscriptions.ps1 +++ b/eng/common/post-build/trigger-subscriptions.ps1 @@ -1,33 +1,20 @@ -param( +param( [Parameter(Mandatory=$true)][string] $SourceRepo, [Parameter(Mandatory=$true)][int] $ChannelId, - [string] $MaestroEndpoint = "https://maestro-prod.westus2.cloudapp.azure.com", - [string] $BarToken, - [string] $ApiVersion = "2019-01-16" + [Parameter(Mandatory=$true)][string] $MaestroApiAccessToken, + [Parameter(Mandatory=$false)][string] $MaestroApiEndPoint = "https://maestro-prod.westus2.cloudapp.azure.com", + [Parameter(Mandatory=$false)][string] $MaestroApiVersion = "2019-01-16" ) -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2.0 - -. $PSScriptRoot\..\tools.ps1 - -function Get-Headers([string]$accept, [string]$barToken) { - $headers = New-Object 'System.Collections.Generic.Dictionary[[String],[String]]' - $headers.Add('Accept',$accept) - $headers.Add('Authorization',"Bearer $barToken") - return $headers -} +. $PSScriptRoot\post-build-utils.ps1 # Get all the $SourceRepo subscriptions $normalizedSourceRepo = $SourceRepo.Replace('dnceng@', '') -$getSubscriptionsApiEndpoint = "$maestroEndpoint/api/subscriptions?sourceRepository=$normalizedSourceRepo&api-version=$apiVersion" -$headers = Get-Headers 'application/json' $barToken - -$subscriptions = Invoke-WebRequest -Uri $getSubscriptionsApiEndpoint -Headers $headers | ConvertFrom-Json +$subscriptions = Get-MaestroSubscriptions -SourceRepository $normalizedSourceRepo -ChannelId $ChannelId if (!$subscriptions) { Write-Host "No subscriptions found for source repo '$normalizedSourceRepo' in channel '$ChannelId'" - return + ExitWithExitCode 0 } $subscriptionsToTrigger = New-Object System.Collections.Generic.List[string] @@ -36,21 +23,18 @@ $failedTriggeredSubscription = $false # Get all enabled subscriptions that need dependency flow on 'everyBuild' foreach ($subscription in $subscriptions) { if ($subscription.enabled -and $subscription.policy.updateFrequency -like 'everyBuild' -and $subscription.channel.id -eq $ChannelId) { - Write-Host "$subscription.id" + Write-Host "Should trigger this subscription: $subscription.id" [void]$subscriptionsToTrigger.Add($subscription.id) } } foreach ($subscriptionToTrigger in $subscriptionsToTrigger) { try { - $triggerSubscriptionApiEndpoint = "$maestroEndpoint/api/subscriptions/$subscriptionToTrigger/trigger?api-version=$apiVersion" - $headers = Get-Headers 'application/json' $BarToken - - Write-Host "Triggering subscription '$subscriptionToTrigger'..." + Write-Host "Triggering subscription '$subscriptionToTrigger'." - Invoke-WebRequest -Uri $triggerSubscriptionApiEndpoint -Headers $headers -Method Post + Trigger-Subscription -SubscriptionId $subscriptionToTrigger - Write-Host "Subscription '$subscriptionToTrigger' triggered!" + Write-Host "done." } catch { @@ -61,9 +45,13 @@ foreach ($subscriptionToTrigger in $subscriptionsToTrigger) { } } -if ($failedTriggeredSubscription) { +if ($subscriptionsToTrigger.Count -eq 0) { + Write-Host "No subscription matched source repo '$normalizedSourceRepo' and channel ID '$ChannelId'." +} +elseif ($failedTriggeredSubscription) { Write-Host "At least one subscription failed to be triggered..." ExitWithExitCode 1 } - -Write-Host "All subscriptions were triggered successfully!" +else { + Write-Host "All subscriptions were triggered successfully!" +} diff --git a/eng/common/sdl/packages.config b/eng/common/sdl/packages.config index fb9b712863..3f97ac2f16 100644 --- a/eng/common/sdl/packages.config +++ b/eng/common/sdl/packages.config @@ -1,4 +1,4 @@  - + diff --git a/eng/common/templates/job/execute-sdl.yml b/eng/common/templates/job/execute-sdl.yml index 5837f3d56d..f657a4dc91 100644 --- a/eng/common/templates/job/execute-sdl.yml +++ b/eng/common/templates/job/execute-sdl.yml @@ -46,7 +46,7 @@ jobs: continueOnError: ${{ parameters.continueOnError }} - ${{ if eq(parameters.overrideParameters, '') }}: - powershell: eng/common/sdl/execute-all-sdl-tools.ps1 - -GuardianPackageName Microsoft.Guardian.Cli.0.6.0 + -GuardianPackageName Microsoft.Guardian.Cli.0.7.1 -NugetPackageDirectory $(Build.SourcesDirectory)\.packages -AzureDevOpsAccessToken $(dn-bot-dotnet-build-rw-code-rw) ${{ parameters.additionalParameters }} diff --git a/eng/common/templates/job/job.yml b/eng/common/templates/job/job.yml index 1814e0ab61..8db456bb7f 100644 --- a/eng/common/templates/job/job.yml +++ b/eng/common/templates/job/job.yml @@ -37,6 +37,9 @@ parameters: # Optional: Enable publishing to the build asset registry enablePublishBuildAssets: false + # Optional: Prevent gather/push manifest from executing when using publishing pipelines + enablePublishUsingPipelines: false + # Optional: Include PublishTestResults task enablePublishTestResults: false @@ -187,7 +190,7 @@ jobs: continueOnError: true condition: always() - - ${{ if and(eq(parameters.enablePublishBuildAssets, true), ne(variables['_PublishUsingPipelines'], 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: + - ${{ if and(eq(parameters.enablePublishBuildAssets, true), ne(parameters.enablePublishUsingPipelines, 'true'), eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - task: CopyFiles@2 displayName: Gather Asset Manifests inputs: @@ -195,6 +198,7 @@ jobs: TargetFolder: '$(Build.StagingDirectory)/AssetManifests' continueOnError: ${{ parameters.continueOnError }} condition: and(succeeded(), eq(variables['_DotNetPublishToBlobFeed'], 'true')) + - task: PublishBuildArtifacts@1 displayName: Push Asset Manifests inputs: diff --git a/eng/common/templates/post-build/channels/internal-servicing.yml b/eng/common/templates/post-build/channels/internal-servicing.yml index 648e854e0e..dc065ab308 100644 --- a/eng/common/templates/post-build/channels/internal-servicing.yml +++ b/eng/common/templates/post-build/channels/internal-servicing.yml @@ -13,7 +13,7 @@ stages: - job: displayName: Symbol Publishing dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.InternalServicing_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.InternalServicing_30_Channel_Id)) variables: - group: DotNet-Symbol-Server-Pats pool: @@ -41,13 +41,12 @@ stages: dependsOn: setupMaestroVars variables: - group: DotNet-Blob-Feed - - group: Publish-Build-Assets - group: AzureDevOps-Artifact-Feeds-Pats - name: BARBuildId value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - name: IsStableBuild value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.InternalServicing_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.InternalServicing_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -87,8 +86,8 @@ stages: /p:StaticInternalFeed=$(dotnetfeed-internal-nonstable-feed-url) /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe /p:BARBuildId=$(BARBuildId) - /p:MaestroApiEndpoint='https://maestro-prod.westus2.cloudapp.azure.com' - /p:BuildAssetRegistryToken='$(MaestroAccessToken)' + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' /p:BlobBasePath='$(Build.ArtifactStagingDirectory)\BlobArtifacts' /p:PackageBasePath='$(Build.ArtifactStagingDirectory)\PackageArtifacts' @@ -127,7 +126,7 @@ stages: - job: displayName: Symbol Availability dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.InternalServicing_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.InternalServicing_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -143,29 +142,6 @@ stages: filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) - - job: - displayName: Gather Drop - dependsOn: setupMaestroVars - variables: - BARBuildId: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.InternalServicing_30_Channel_Id) - pool: - vmImage: 'windows-2019' - steps: - - task: PowerShell@2 - displayName: Setup Darc CLI - inputs: - targetType: filePath - filePath: '$(Build.SourcesDirectory)/eng/common/darc-init.ps1' - - - task: PowerShell@2 - displayName: Run Darc gather-drop - inputs: - targetType: inline - script: | - darc gather-drop --non-shipping --continue-on-error --id $(BARBuildId) --output-dir $(Agent.BuildDirectory)/Temp/Drop/ --bar-uri https://maestro-prod.westus2.cloudapp.azure.com/ --password $(MaestroAccessToken) --latest-location - enabled: false - - template: ../promote-build.yml parameters: ChannelId: ${{ variables.InternalServicing_30_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/netcore-dev-5.yml b/eng/common/templates/post-build/channels/netcore-dev-5.yml new file mode 100644 index 0000000000..f2b0cfb269 --- /dev/null +++ b/eng/common/templates/post-build/channels/netcore-dev-5.yml @@ -0,0 +1,148 @@ +parameters: + enableSymbolValidation: true + +stages: +- stage: NetCore_Dev5_Publish + dependsOn: validate + variables: + - template: ../common-variables.yml + displayName: .NET Core 5 Dev Channel + jobs: + - template: ../setup-maestro-vars.yml + + - job: + displayName: Symbol Publishing + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_5_Dev_Channel_Id)) + variables: + - group: DotNet-Symbol-Server-Pats + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Artifacts + inputs: + downloadType: specific files + matchingPattern: "*Artifacts*" + + - task: PowerShell@2 + displayName: Publish + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishToSymbolServers -restore -msbuildEngine dotnet + /p:DotNetSymbolServerTokenMsdl=$(microsoft-symbol-server-pat) + /p:DotNetSymbolServerTokenSymWeb=$(symweb-symbol-server-pat) + /p:PDBArtifactsDirectory='$(Build.ArtifactStagingDirectory)/PDBArtifacts/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:Configuration=Release + + - job: + displayName: Publish Assets + dependsOn: setupMaestroVars + variables: + - group: DotNet-Blob-Feed + - group: AzureDevOps-Artifact-Feeds-Pats + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + - name: IsStableBuild + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_5_Dev_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: current + artifactName: BlobArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Asset Manifests + inputs: + buildType: current + artifactName: AssetManifests + + - task: PowerShell@2 + displayName: Add Assets Location + env: + AZURE_DEVOPS_EXT_PAT: $(dn-bot-dnceng-unviersal-packages-rw) + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet + /p:ChannelId=$(NetCore_5_Dev_Channel_Id) + /p:ArtifactsCategory=$(_DotNetArtifactsCategory) + /p:IsStableBuild=$(IsStableBuild) + /p:IsInternalBuild=$(IsInternalBuild) + /p:RepositoryName=$(Build.Repository.Name) + /p:CommitSha=$(Build.SourceVersion) + /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe + /p:AzdoTargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' + /p:BARBuildId=$(BARBuildId) + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' + /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts/' + /p:Configuration=Release + + - task: NuGetCommand@2 + displayName: Publish Packages to AzDO Feed + condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') + inputs: + command: push + vstsFeed: $(AzDoFeedName) + packagesToPush: $(Build.ArtifactStagingDirectory)\PackageArtifacts\*.nupkg + publishVstsFeed: $(AzDoFeedName) + + - task: PowerShell@2 + displayName: Publish Blobs to AzDO Feed + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-blobs-to-azdo.ps1 + arguments: -FeedName $(AzDoFeedName) + -SourceFolderCollection $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -PersonalAccessToken $(dn-bot-dnceng-unviersal-packages-rw) + enabled: false + + +- stage: NetCore_Dev5_PublishValidation + displayName: Publish Validation + variables: + - template: ../common-variables.yml + jobs: + - template: ../setup-maestro-vars.yml + + - ${{ if eq(parameters.enableSymbolValidation, 'true') }}: + - job: + displayName: Symbol Availability + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_5_Dev_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: PowerShell@2 + displayName: Check Symbol Availability + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) + + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.NetCore_5_Dev_Channel_Id }} + + - template: ../promote-build.yml + parameters: + ChannelId: ${{ variables.NetCore_5_Dev_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/netcore-tools-latest.yml b/eng/common/templates/post-build/channels/netcore-tools-latest.yml new file mode 100644 index 0000000000..fd6c09b227 --- /dev/null +++ b/eng/common/templates/post-build/channels/netcore-tools-latest.yml @@ -0,0 +1,148 @@ +parameters: + enableSymbolValidation: true + +stages: +- stage: NetCore_Tools_Latest_Publish + dependsOn: validate + variables: + - template: ../common-variables.yml + displayName: .NET Tools - Latest + jobs: + - template: ../setup-maestro-vars.yml + + - job: + displayName: Symbol Publishing + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_Tools_Latest_Channel_Id)) + variables: + - group: DotNet-Symbol-Server-Pats + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Artifacts + inputs: + downloadType: specific files + matchingPattern: "*Artifacts*" + + - task: PowerShell@2 + displayName: Publish + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishToSymbolServers -restore -msbuildEngine dotnet + /p:DotNetSymbolServerTokenMsdl=$(microsoft-symbol-server-pat) + /p:DotNetSymbolServerTokenSymWeb=$(symweb-symbol-server-pat) + /p:PDBArtifactsDirectory='$(Build.ArtifactStagingDirectory)/PDBArtifacts/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:Configuration=Release + + - job: + displayName: Publish Assets + dependsOn: setupMaestroVars + variables: + - group: DotNet-Blob-Feed + - group: AzureDevOps-Artifact-Feeds-Pats + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + - name: IsStableBuild + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_Tools_Latest_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: current + artifactName: BlobArtifacts + + - task: DownloadBuildArtifacts@0 + displayName: Download Asset Manifests + inputs: + buildType: current + artifactName: AssetManifests + + - task: PowerShell@2 + displayName: Add Assets Location + env: + AZURE_DEVOPS_EXT_PAT: $(dn-bot-dnceng-unviersal-packages-rw) + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet + /p:ChannelId=$(NetCore_Tools_Latest_Channel_Id) + /p:ArtifactsCategory=$(_DotNetArtifactsCategory) + /p:IsStableBuild=$(IsStableBuild) + /p:IsInternalBuild=$(IsInternalBuild) + /p:RepositoryName=$(Build.Repository.Name) + /p:CommitSha=$(Build.SourceVersion) + /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe + /p:AzdoTargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' + /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' + /p:BARBuildId=$(BARBuildId) + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' + /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' + /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts/' + /p:Configuration=Release + + - task: NuGetCommand@2 + displayName: Publish Packages to AzDO Feed + condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') + inputs: + command: push + vstsFeed: $(AzDoFeedName) + packagesToPush: $(Build.ArtifactStagingDirectory)\PackageArtifacts\*.nupkg + publishVstsFeed: $(AzDoFeedName) + + - task: PowerShell@2 + displayName: Publish Blobs to AzDO Feed + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/publish-blobs-to-azdo.ps1 + arguments: -FeedName $(AzDoFeedName) + -SourceFolderCollection $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -PersonalAccessToken $(dn-bot-dnceng-unviersal-packages-rw) + enabled: false + + +- stage: NetCore_Tools_Latest_PublishValidation + displayName: Publish Validation + variables: + - template: ../common-variables.yml + jobs: + - template: ../setup-maestro-vars.yml + + - ${{ if eq(parameters.enableSymbolValidation, 'true') }}: + - job: + displayName: Symbol Availability + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.NetCore_Tools_Latest_Channel_Id)) + pool: + vmImage: 'windows-2019' + steps: + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: current + artifactName: PackageArtifacts + + - task: PowerShell@2 + displayName: Check Symbol Availability + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) + + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.NetCore_Tools_Latest_Channel_Id }} + + - template: ../promote-build.yml + parameters: + ChannelId: ${{ variables.NetCore_Tools_Latest_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/public-dev-release.yml b/eng/common/templates/post-build/channels/public-dev-release.yml index bdc631016b..771dcf4ef8 100644 --- a/eng/common/templates/post-build/channels/public-dev-release.yml +++ b/eng/common/templates/post-build/channels/public-dev-release.yml @@ -13,7 +13,7 @@ stages: - job: displayName: Symbol Publishing dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicDevRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicDevRelease_30_Channel_Id)) variables: - group: DotNet-Symbol-Server-Pats pool: @@ -41,13 +41,12 @@ stages: dependsOn: setupMaestroVars variables: - group: DotNet-Blob-Feed - - group: Publish-Build-Assets - group: AzureDevOps-Artifact-Feeds-Pats - name: BARBuildId value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - name: IsStableBuild value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicDevRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicDevRelease_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -77,7 +76,7 @@ stages: filePath: eng\common\sdk-task.ps1 arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet /p:ChannelId=$(PublicDevRelease_30_Channel_Id) - /p:ArtifactsCategory=.NetCore + /p:ArtifactsCategory=$(_DotNetArtifactsCategory) /p:IsStableBuild=$(IsStableBuild) /p:IsInternalBuild=$(IsInternalBuild) /p:RepositoryName=$(Build.Repository.Name) @@ -87,8 +86,8 @@ stages: /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' /p:BARBuildId=$(BARBuildId) - /p:MaestroApiEndpoint='https://maestro-prod.westus2.cloudapp.azure.com' - /p:BuildAssetRegistryToken='$(MaestroAccessToken)' + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' /p:BlobBasePath='$(Build.ArtifactStagingDirectory)/BlobArtifacts/' /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts/' @@ -124,7 +123,7 @@ stages: - job: displayName: Symbol Availability dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicDevRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicDevRelease_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -140,27 +139,9 @@ stages: filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) - - job: - displayName: Gather Drop - dependsOn: setupMaestroVars - variables: - BARBuildId: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicDevRelease_30_Channel_Id) - pool: - vmImage: 'windows-2019' - steps: - - task: PowerShell@2 - displayName: Setup Darc CLI - inputs: - targetType: filePath - filePath: '$(Build.SourcesDirectory)/eng/common/darc-init.ps1' - - - task: PowerShell@2 - displayName: Run Darc gather-drop - inputs: - targetType: inline - script: | - darc gather-drop --non-shipping --continue-on-error --id $(BARBuildId) --output-dir $(Agent.BuildDirectory)/Temp/Drop/ --bar-uri https://maestro-prod.westus2.cloudapp.azure.com/ --password $(MaestroAccessToken) --latest-location + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.PublicDevRelease_30_Channel_Id }} - template: ../promote-build.yml parameters: diff --git a/eng/common/templates/post-build/channels/public-release.yml b/eng/common/templates/post-build/channels/public-release.yml index f6a7efdfe9..00108bd3f8 100644 --- a/eng/common/templates/post-build/channels/public-release.yml +++ b/eng/common/templates/post-build/channels/public-release.yml @@ -13,7 +13,7 @@ stages: - job: displayName: Symbol Publishing dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicRelease_30_Channel_Id)) variables: - group: DotNet-Symbol-Server-Pats pool: @@ -41,13 +41,12 @@ stages: dependsOn: setupMaestroVars variables: - group: DotNet-Blob-Feed - - group: Publish-Build-Assets - group: AzureDevOps-Artifact-Feeds-Pats - name: BARBuildId value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - name: IsStableBuild value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicRelease_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -87,8 +86,8 @@ stages: /p:StaticInternalFeed=$(dotnetfeed-internal-nonstable-feed-url) /p:NugetPath=$(Agent.BuildDirectory)\Nuget\NuGet.exe /p:BARBuildId=$(BARBuildId) - /p:MaestroApiEndpoint='https://maestro-prod.westus2.cloudapp.azure.com' - /p:BuildAssetRegistryToken='$(MaestroAccessToken)' + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' /p:BlobBasePath='$(Build.ArtifactStagingDirectory)\BlobArtifacts' /p:PackageBasePath='$(Build.ArtifactStagingDirectory)\PackageArtifacts' @@ -127,7 +126,7 @@ stages: - job: displayName: Symbol Availability dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicRelease_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -143,29 +142,6 @@ stages: filePath: $(Build.SourcesDirectory)/eng/common/post-build/symbols-validation.ps1 arguments: -InputPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ -ExtractPath $(Agent.BuildDirectory)/Temp/ -DotnetSymbolVersion $(SymbolToolVersion) - - job: - displayName: Gather Drop - dependsOn: setupMaestroVars - variables: - BARBuildId: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicRelease_30_Channel_Id) - pool: - vmImage: 'windows-2019' - steps: - - task: PowerShell@2 - displayName: Setup Darc CLI - inputs: - targetType: filePath - filePath: '$(Build.SourcesDirectory)/eng/common/darc-init.ps1' - - - task: PowerShell@2 - displayName: Run Darc gather-drop - inputs: - targetType: inline - script: | - darc gather-drop --non-shipping --continue-on-error --id $(BARBuildId) --output-dir $(Agent.BuildDirectory)/Temp/Drop/ --bar-uri https://maestro-prod.westus2.cloudapp.azure.com/ --password $(MaestroAccessToken) --latest-location - enabled: false - - template: ../promote-build.yml parameters: ChannelId: ${{ variables.PublicRelease_30_Channel_Id }} diff --git a/eng/common/templates/post-build/channels/public-validation-release.yml b/eng/common/templates/post-build/channels/public-validation-release.yml index f12f402ad9..f64184da9f 100644 --- a/eng/common/templates/post-build/channels/public-validation-release.yml +++ b/eng/common/templates/post-build/channels/public-validation-release.yml @@ -12,13 +12,12 @@ stages: dependsOn: setupMaestroVars variables: - group: DotNet-Blob-Feed - - group: Publish-Build-Assets - group: AzureDevOps-Artifact-Feeds-Pats - name: BARBuildId value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - name: IsStableBuild value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.IsStableBuild'] ] - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicValidationRelease_30_Channel_Id) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', variables.PublicValidationRelease_30_Channel_Id)) pool: vmImage: 'windows-2019' steps: @@ -48,7 +47,7 @@ stages: filePath: eng\common\sdk-task.ps1 arguments: -task PublishArtifactsInManifest -restore -msbuildEngine dotnet /p:ChannelId=$(PublicValidationRelease_30_Channel_Id) - /p:ArtifactsCategory=.NetCoreValidation + /p:ArtifactsCategory=$(_DotNetValidationArtifactsCategory) /p:IsStableBuild=$(IsStableBuild) /p:IsInternalBuild=$(IsInternalBuild) /p:RepositoryName=$(Build.Repository.Name) @@ -58,13 +57,13 @@ stages: /p:TargetFeedPAT='$(dn-bot-dnceng-unviersal-packages-rw)' /p:AzureStorageTargetFeedPAT='$(dotnetfeed-storage-access-key-1)' /p:BARBuildId=$(BARBuildId) - /p:MaestroApiEndpoint='https://maestro-prod.westus2.cloudapp.azure.com' - /p:BuildAssetRegistryToken='$(MaestroAccessToken)' + /p:MaestroApiEndpoint='$(MaestroApiEndPoint)' + /p:BuildAssetRegistryToken='$(MaestroApiAccessToken)' /p:ManifestsBasePath='$(Build.ArtifactStagingDirectory)/AssetManifests/' /p:BlobBasePath='$(Build.ArtifactStagingDirectory)\BlobArtifacts' /p:PackageBasePath='$(Build.ArtifactStagingDirectory)\PackageArtifacts' /p:Configuration=Release - + - task: NuGetCommand@2 displayName: Publish Packages to AzDO Feed condition: contains(variables['TargetAzDOFeed'], 'pkgs.visualstudio.com') @@ -91,29 +90,9 @@ stages: jobs: - template: ../setup-maestro-vars.yml - - job: - displayName: Gather Drop - dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], variables.PublicValidationRelease_30_Channel_Id) - variables: - - name: BARBuildId - value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - - group: Publish-Build-Assets - pool: - vmImage: 'windows-2019' - steps: - - task: PowerShell@2 - displayName: Setup Darc CLI - inputs: - targetType: filePath - filePath: '$(Build.SourcesDirectory)/eng/common/darc-init.ps1' - - - task: PowerShell@2 - displayName: Run Darc gather-drop - inputs: - targetType: inline - script: | - darc gather-drop --non-shipping --continue-on-error --id $(BARBuildId) --output-dir $(Agent.BuildDirectory)/Temp/Drop/ --bar-uri https://maestro-prod.westus2.cloudapp.azure.com --password $(MaestroAccessToken) --latest-location + - template: ../darc-gather-drop.yml + parameters: + ChannelId: ${{ variables.PublicValidationRelease_30_Channel_Id }} - template: ../promote-build.yml parameters: diff --git a/eng/common/templates/post-build/common-variables.yml b/eng/common/templates/post-build/common-variables.yml index 42df4ae77e..52a74487fd 100644 --- a/eng/common/templates/post-build/common-variables.yml +++ b/eng/common/templates/post-build/common-variables.yml @@ -1,21 +1,47 @@ variables: + - group: Publish-Build-Assets + # .NET Core 3 Dev - PublicDevRelease_30_Channel_Id: 3 + - name: PublicDevRelease_30_Channel_Id + value: 3 + + # .NET Core 5 Dev + - name: NetCore_5_Dev_Channel_Id + value: 131 # .NET Tools - Validation - PublicValidationRelease_30_Channel_Id: 9 + - name: PublicValidationRelease_30_Channel_Id + value: 9 + + # .NET Tools - Latest + - name: NetCore_Tools_Latest_Channel_Id + value: 2 # .NET Core 3.0 Internal Servicing - InternalServicing_30_Channel_Id: 184 + - name: InternalServicing_30_Channel_Id + value: 184 # .NET Core 3.0 Release - PublicRelease_30_Channel_Id: 19 + - name: PublicRelease_30_Channel_Id + value: 19 # Whether the build is internal or not - IsInternalBuild: ${{ and(ne(variables['System.TeamProject'], 'public'), contains(variables['Build.SourceBranch'], 'internal')) }} + - name: IsInternalBuild + value: ${{ and(ne(variables['System.TeamProject'], 'public'), contains(variables['Build.SourceBranch'], 'internal')) }} # Storage account name for proxy-backed feeds - ProxyBackedFeedsAccountName: dotnetfeed + - name: ProxyBackedFeedsAccountName + value: dotnetfeed - SourceLinkCLIVersion: 3.0.0 - SymbolToolVersion: 1.0.1 + # Default Maestro++ API Endpoint and API Version + - name: MaestroApiEndPoint + value: "https://maestro-prod.westus2.cloudapp.azure.com" + - name: MaestroApiAccessToken + value: $(MaestroAccessToken) + - name: MaestroApiVersion + value: "2019-01-16" + + - name: SourceLinkCLIVersion + value: 3.0.0 + - name: SymbolToolVersion + value: 1.0.1 diff --git a/eng/common/templates/post-build/darc-gather-drop.yml b/eng/common/templates/post-build/darc-gather-drop.yml new file mode 100644 index 0000000000..3268ccaa55 --- /dev/null +++ b/eng/common/templates/post-build/darc-gather-drop.yml @@ -0,0 +1,23 @@ +parameters: + ChannelId: 0 + +jobs: +- job: gatherDrop + displayName: Gather Drop + dependsOn: setupMaestroVars + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', ${{ parameters.ChannelId }})) + variables: + - name: BARBuildId + value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] + pool: + vmImage: 'windows-2019' + steps: + - task: PowerShell@2 + displayName: Darc gather-drop + inputs: + filePath: $(Build.SourcesDirectory)/eng/common/post-build/darc-gather-drop.ps1 + arguments: -BarBuildId $(BARBuildId) + -DropLocation $(Agent.BuildDirectory)/Temp/Drop/ + -MaestroApiAccessToken $(MaestroApiAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/templates/post-build/post-build.yml b/eng/common/templates/post-build/post-build.yml index daa799259c..33db50ce26 100644 --- a/eng/common/templates/post-build/post-build.yml +++ b/eng/common/templates/post-build/post-build.yml @@ -7,9 +7,12 @@ parameters: enable: false params: '' + # Which stages should finish execution before post-build stages start + dependsOn: [build] + stages: - stage: validate - dependsOn: build + dependsOn: ${{ parameters.dependsOn }} displayName: Validate jobs: - ${{ if eq(parameters.enableNugetValidation, 'true') }}: @@ -80,10 +83,18 @@ stages: parameters: additionalParameters: ${{ parameters.SDLValidationParameters.params }} +- template: \eng\common\templates\post-build\channels\netcore-dev-5.yml + parameters: + enableSymbolValidation: ${{ parameters.enableSymbolValidation }} + - template: \eng\common\templates\post-build\channels\public-dev-release.yml parameters: enableSymbolValidation: ${{ parameters.enableSymbolValidation }} +- template: \eng\common\templates\post-build\channels\netcore-tools-latest.yml + parameters: + enableSymbolValidation: ${{ parameters.enableSymbolValidation }} + - template: \eng\common\templates\post-build\channels\public-validation-release.yml - template: \eng\common\templates\post-build\channels\public-release.yml diff --git a/eng/common/templates/post-build/promote-build.yml b/eng/common/templates/post-build/promote-build.yml index af48b0b339..6b479c3b82 100644 --- a/eng/common/templates/post-build/promote-build.yml +++ b/eng/common/templates/post-build/promote-build.yml @@ -5,13 +5,12 @@ jobs: - job: displayName: Promote Build dependsOn: setupMaestroVars - condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], ${{ parameters.ChannelId }}) + condition: contains(dependencies.setupMaestroVars.outputs['setReleaseVars.InitialChannels'], format('[{0}]', ${{ parameters.ChannelId }})) variables: - name: BARBuildId value: $[ dependencies.setupMaestroVars.outputs['setReleaseVars.BARBuildId'] ] - name: ChannelId value: ${{ parameters.ChannelId }} - - group: Publish-Build-Assets pool: vmImage: 'windows-2019' steps: @@ -21,4 +20,6 @@ jobs: filePath: $(Build.SourcesDirectory)/eng/common/post-build/promote-build.ps1 arguments: -BuildId $(BARBuildId) -ChannelId $(ChannelId) - -BarToken $(MaestroAccessToken) + -MaestroApiAccessToken $(MaestroApiAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/templates/post-build/setup-maestro-vars.yml b/eng/common/templates/post-build/setup-maestro-vars.yml index f6120dc1e1..56242b068e 100644 --- a/eng/common/templates/post-build/setup-maestro-vars.yml +++ b/eng/common/templates/post-build/setup-maestro-vars.yml @@ -14,22 +14,5 @@ jobs: name: setReleaseVars displayName: Set Release Configs Vars inputs: - targetType: inline - script: | - # This is needed to make Write-PipelineSetVariable works in this context - $ci = $true - - . "$(Build.SourcesDirectory)/eng/common/tools.ps1" - - $Content = Get-Content "$(Build.StagingDirectory)/ReleaseConfigs/ReleaseConfigs.txt" - - $BarId = $Content | Select -Index 0 - - $Channels = "" - $Content | Select -Index 1 | ForEach-Object { $Channels += "$_ ," } - - $IsStableBuild = $Content | Select -Index 2 - - Write-PipelineSetVariable -Name 'BARBuildId' -Value $BarId - Write-PipelineSetVariable -Name 'InitialChannels' -Value "$Channels" - Write-PipelineSetVariable -Name 'IsStableBuild' -Value $IsStableBuild + filePath: $(Build.SourcesDirectory)/eng/common/post-build/setup-maestro-vars.ps1 + arguments: -ReleaseConfigsPath '$(Build.StagingDirectory)/ReleaseConfigs/ReleaseConfigs.txt' diff --git a/eng/common/templates/post-build/trigger-subscription.yml b/eng/common/templates/post-build/trigger-subscription.yml index 65259d4e68..da669030da 100644 --- a/eng/common/templates/post-build/trigger-subscription.yml +++ b/eng/common/templates/post-build/trigger-subscription.yml @@ -8,4 +8,6 @@ steps: filePath: $(Build.SourcesDirectory)/eng/common/post-build/trigger-subscriptions.ps1 arguments: -SourceRepo $(Build.Repository.Uri) -ChannelId ${{ parameters.ChannelId }} - -BarToken $(MaestroAccessTokenInt) \ No newline at end of file + -MaestroApiAccessToken $(MaestroAccessToken) + -MaestroApiEndPoint $(MaestroApiEndPoint) + -MaestroApiVersion $(MaestroApiVersion) diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 8fe2b11ad2..9c12b1b4fd 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -39,6 +39,10 @@ # installed on the machine instead of downloading one. [bool]$useInstalledDotNetCli = if (Test-Path variable:useInstalledDotNetCli) { $useInstalledDotNetCli } else { $true } +# Enable repos to use a particular version of the on-line dotnet-install scripts. +# default URL: https://dot.net/v1/dotnet-install.ps1 +[string]$dotnetInstallScriptVersion = if (Test-Path variable:dotnetInstallScriptVersion) { $dotnetInstallScriptVersion } else { "v1" } + # True to use global NuGet cache instead of restoring packages to repository-local directory. [bool]$useGlobalNuGetCache = if (Test-Path variable:useGlobalNuGetCache) { $useGlobalNuGetCache } else { !$ci } @@ -159,7 +163,7 @@ function GetDotNetInstallScript([string] $dotnetRoot) { $installScript = Join-Path $dotnetRoot "dotnet-install.ps1" if (!(Test-Path $installScript)) { Create-Directory $dotnetRoot - Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile $installScript + Invoke-WebRequest "https://dot.net/$dotnetInstallScriptVersion/dotnet-install.ps1" -OutFile $installScript } return $installScript @@ -518,6 +522,9 @@ function MSBuild-Core() { if ($warnAsError) { $cmdArgs += " /warnaserror /p:TreatWarningsAsErrors=true" } + else { + $cmdArgs += " /p:TreatWarningsAsErrors=false" + } foreach ($arg in $args) { if ($arg -ne $null -and $arg.Trim() -ne "") { diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 0deb01c480..3af9be6157 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -45,6 +45,10 @@ warn_as_error=${warn_as_error:-true} # installed on the machine instead of downloading one. use_installed_dotnet_cli=${use_installed_dotnet_cli:-true} +# Enable repos to use a particular version of the on-line dotnet-install scripts. +# default URL: https://dot.net/v1/dotnet-install.sh +dotnetInstallScriptVersion=${dotnetInstallScriptVersion:-'v1'} + # True to use global NuGet cache instead of restoring packages to repository-local directory. if [[ "$ci" == true ]]; then use_global_nuget_cache=${use_global_nuget_cache:-false} @@ -77,7 +81,7 @@ function ReadGlobalVersion { local pattern="\"$key\" *: *\"(.*)\"" if [[ ! $line =~ $pattern ]]; then - Write-PipelineTelemetryError -category 'InitializeTools' "Error: Cannot find \"$key\" in $global_json_file" + Write-PipelineTelemetryError -category 'InitializeToolset' "Error: Cannot find \"$key\" in $global_json_file" ExitWithExitCode 1 fi @@ -195,7 +199,7 @@ function InstallDotNet { function GetDotNetInstallScript { local root=$1 local install_script="$root/dotnet-install.sh" - local install_script_url="https://dot.net/v1/dotnet-install.sh" + local install_script_url="https://dot.net/$dotnetInstallScriptVersion/dotnet-install.sh" if [[ ! -a "$install_script" ]]; then mkdir -p "$root" @@ -245,7 +249,7 @@ function InitializeNativeTools() { then local nativeArgs="" if [[ "$ci" == true ]]; then - nativeArgs="-InstallDirectory $tools_dir" + nativeArgs="--installDirectory $tools_dir" fi "$_script_dir/init-tools-native.sh" $nativeArgs fi diff --git a/eng/scripts/CodeCheck.ps1 b/eng/scripts/CodeCheck.ps1 index 669b56c21f..5ed823f083 100644 --- a/eng/scripts/CodeCheck.ps1 +++ b/eng/scripts/CodeCheck.ps1 @@ -166,11 +166,6 @@ try { & dotnet run -p "$repoRoot/eng/tools/BaselineGenerator/" } - Write-Host "Re-generating Web.JS files" - Invoke-Block { - & dotnet build "$repoRoot\src\Components\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj" - } - Write-Host "Run git diff to check for pending changes" # Redirect stderr to stdout because PowerShell does not consistently handle output to stderr diff --git a/eng/targets/Npm.Common.targets b/eng/targets/Npm.Common.targets index c290e39756..204e14d01f 100644 --- a/eng/targets/Npm.Common.targets +++ b/eng/targets/Npm.Common.targets @@ -11,15 +11,28 @@ $([MSBuild]::NormalizeDirectory('$(BaseIntermediateOutputPath)'))$(Configuration)\ --frozen-lockfile <_BackupPackageJson>$(IntermediateOutputPath)$(MSBuildProjectName).package.json.bak + + PrepareForBuild; + ResolveProjectReferences; + _Build; + + run build + + + + + + + @@ -36,13 +49,13 @@ BuildInParallel="true" /> - + - + diff --git a/global.json b/global.json index 4f83cb7a89..64afa6336e 100644 --- a/global.json +++ b/global.json @@ -24,7 +24,7 @@ }, "msbuild-sdks": { "Yarn.MSBuild": "1.15.2", - "Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.19369.2", - "Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.19369.2" + "Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.19404.1", + "Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.19404.1" } } diff --git a/src/Components/Analyzers/src/ComponentInternalUsageDiagnosticAnalzyer.cs b/src/Components/Analyzers/src/ComponentInternalUsageDiagnosticAnalzyer.cs new file mode 100644 index 0000000000..3f0c765614 --- /dev/null +++ b/src/Components/Analyzers/src/ComponentInternalUsageDiagnosticAnalzyer.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using Microsoft.AspNetCore.Components.Analyzers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.Extensions.Internal +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ComponentInternalUsageDiagnosticAnalyzer : DiagnosticAnalyzer + { + private readonly InternalUsageAnalyzer _inner; + + public ComponentInternalUsageDiagnosticAnalyzer() + { + // We don't have in *internal* attribute in Blazor. + _inner = new InternalUsageAnalyzer(IsInInternalNamespace, hasInternalAttribute: null, DiagnosticDescriptors.DoNotUseRenderTreeTypes); + } + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.DoNotUseRenderTreeTypes); + + public override void Initialize(AnalysisContext context) + { + _inner.Register(context); + } + + private static bool IsInInternalNamespace(ISymbol symbol) + { + if (symbol?.ContainingNamespace?.ToDisplayString() is string ns) + { + return string.Equals(ns, "Microsoft.AspNetCore.Components.RenderTree"); + } + + return false; + } + } +} diff --git a/src/Components/Analyzers/src/DiagnosticDescriptors.cs b/src/Components/Analyzers/src/DiagnosticDescriptors.cs index e0ecbce055..8eb86b3212 100644 --- a/src/Components/Analyzers/src/DiagnosticDescriptors.cs +++ b/src/Components/Analyzers/src/DiagnosticDescriptors.cs @@ -55,5 +55,14 @@ namespace Microsoft.AspNetCore.Components.Analyzers DiagnosticSeverity.Warning, isEnabledByDefault: true, description: new LocalizableResourceString(nameof(Resources.ComponentParameterShouldNotBeSetOutsideOfTheirDeclaredComponent_Description), Resources.ResourceManager, typeof(Resources))); + + public static readonly DiagnosticDescriptor DoNotUseRenderTreeTypes = new DiagnosticDescriptor( + "BL0006", + new LocalizableResourceString(nameof(Resources.DoNotUseRenderTreeTypes_Title), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.DoNotUseRenderTreeTypes_Description), Resources.ResourceManager, typeof(Resources)), + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: new LocalizableResourceString(nameof(Resources.DoNotUseRenderTreeTypes_Description), Resources.ResourceManager, typeof(Resources))); } } diff --git a/src/Components/Analyzers/src/InternalUsageAnalyzer.cs b/src/Components/Analyzers/src/InternalUsageAnalyzer.cs new file mode 100644 index 0000000000..92b07a7ab2 --- /dev/null +++ b/src/Components/Analyzers/src/InternalUsageAnalyzer.cs @@ -0,0 +1,126 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.Extensions.Internal +{ + internal class InternalUsageAnalyzer + { + private readonly Func _isInternalNamespace; + private readonly Func _hasInternalAttribute; + private readonly DiagnosticDescriptor _descriptor; + + /// + /// Creates a new instance of . The creator should provide delegates to help determine whether + /// a given symbol is internal or not, and a to create errors. + /// + /// The delegate used to check if a symbol belongs to an internal namespace. + /// The delegate used to check if a symbol has an internal attribute. + /// + /// The used to create errors. The error message should expect a single parameter + /// used for the display name of the member. + /// + public InternalUsageAnalyzer(Func isInInternalNamespace, Func hasInternalAttribute, DiagnosticDescriptor descriptor) + { + _isInternalNamespace = isInInternalNamespace ?? new Func((_) => false); + _hasInternalAttribute = hasInternalAttribute ?? new Func((_) => false); + _descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor)); + } + + public void Register(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeNode, + SyntaxKind.SimpleMemberAccessExpression, + SyntaxKind.ObjectCreationExpression, + SyntaxKind.ClassDeclaration, + SyntaxKind.Parameter); + } + + private void AnalyzeNode(SyntaxNodeAnalysisContext context) + { + switch (context.Node) + { + case MemberAccessExpressionSyntax memberAccessSyntax: + { + if (context.SemanticModel.GetSymbolInfo(context.Node, context.CancellationToken).Symbol is ISymbol symbol && + symbol.ContainingAssembly != context.Compilation.Assembly) + { + var containingType = symbol.ContainingType; + + if (HasInternalAttribute(symbol)) + { + context.ReportDiagnostic(Diagnostic.Create(_descriptor, memberAccessSyntax.Name.GetLocation(), $"{containingType}.{symbol.Name}")); + return; + } + + if (IsInInternalNamespace(containingType) || HasInternalAttribute(containingType)) + { + context.ReportDiagnostic(Diagnostic.Create(_descriptor, memberAccessSyntax.Name.GetLocation(), containingType)); + return; + } + } + return; + } + + case ObjectCreationExpressionSyntax creationSyntax: + { + if (context.SemanticModel.GetSymbolInfo(context.Node, context.CancellationToken).Symbol is ISymbol symbol && + symbol.ContainingAssembly != context.Compilation.Assembly) + { + var containingType = symbol.ContainingType; + + if (HasInternalAttribute(symbol)) + { + context.ReportDiagnostic(Diagnostic.Create(_descriptor, creationSyntax.GetLocation(), containingType)); + return; + } + + if (IsInInternalNamespace(containingType) || HasInternalAttribute(containingType)) + { + context.ReportDiagnostic(Diagnostic.Create(_descriptor, creationSyntax.Type.GetLocation(), containingType)); + return; + } + } + + return; + } + + case ClassDeclarationSyntax declarationSyntax: + { + if (context.SemanticModel.GetDeclaredSymbol(declarationSyntax)?.BaseType is ISymbol symbol && + symbol.ContainingAssembly != context.Compilation.Assembly && + (IsInInternalNamespace(symbol) || HasInternalAttribute(symbol)) && + declarationSyntax.BaseList?.Types.Count > 0) + { + context.ReportDiagnostic(Diagnostic.Create(_descriptor, declarationSyntax.BaseList.Types[0].GetLocation(), symbol)); + } + + return; + } + + case ParameterSyntax parameterSyntax: + { + if (context.SemanticModel.GetDeclaredSymbol(parameterSyntax)?.Type is ISymbol symbol && + symbol.ContainingAssembly != context.Compilation.Assembly && + (IsInInternalNamespace(symbol) || HasInternalAttribute(symbol))) + { + + context.ReportDiagnostic(Diagnostic.Create(_descriptor, parameterSyntax.GetLocation(), symbol)); + } + + return; + } + } + } + + private bool HasInternalAttribute(ISymbol symbol) => _hasInternalAttribute(symbol); + + private bool IsInInternalNamespace(ISymbol symbol) => _isInternalNamespace(symbol); + } +} diff --git a/src/Components/Analyzers/src/Resources.resx b/src/Components/Analyzers/src/Resources.resx index 648adb2c3b..ed4f36c531 100644 --- a/src/Components/Analyzers/src/Resources.resx +++ b/src/Components/Analyzers/src/Resources.resx @@ -1,17 +1,17 @@ - @@ -165,4 +165,13 @@ Component parameter should not be set outside of its component. + + The types in 'Microsoft.AspNetCore.Components.RenderTree' are not recommended for use outside of the Blazor framework. These type definitions will change in future releases. + + + The type or member {0} is is not recommended for use outside of the Blazor frameworks. Types defined in 'Microsoft.AspNetCore.Components.RenderTree' will change in future releases. + + + Do not use RenderTree types + \ No newline at end of file diff --git a/src/Components/Analyzers/test/AnalyzerTestBase.cs b/src/Components/Analyzers/test/AnalyzerTestBase.cs new file mode 100644 index 0000000000..e174b4f667 --- /dev/null +++ b/src/Components/Analyzers/test/AnalyzerTestBase.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Analyzer.Testing; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Components.Analyzers +{ + public abstract class AnalyzerTestBase + { + private static readonly string ProjectDirectory = GetProjectDirectory(); + + public TestSource Read(string source) + { + if (!source.EndsWith(".cs")) + { + source = source + ".cs"; + } + + var filePath = Path.Combine(ProjectDirectory, "TestFiles", GetType().Name, source); + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"TestFile {source} could not be found at {filePath}.", filePath); + } + + var fileContent = File.ReadAllText(filePath); + return TestSource.Read(fileContent); + } + + public Project CreateProject(string source) + { + if (!source.EndsWith(".cs")) + { + source = source + ".cs"; + } + + var read = Read(source); + return DiagnosticProject.Create(GetType().Assembly, new[] { read.Source, }); + } + + public Task CreateCompilationAsync(string source) + { + return CreateProject(source).GetCompilationAsync(); + } + + private static string GetProjectDirectory() + { + // On helix we use the published test files + if (SkipOnHelixAttribute.OnHelix()) + { + return AppContext.BaseDirectory; + } + + // This test code needs to be updated to support distributed testing. + // See https://github.com/aspnet/AspNetCore/issues/10422 +#pragma warning disable 0618 + var solutionDirectory = TestPathUtilities.GetSolutionRootDirectory("Components"); +#pragma warning restore 0618 + var projectDirectory = Path.Combine(solutionDirectory, "Analyzers", "test"); + return projectDirectory; + } + } +} diff --git a/src/Components/Analyzers/test/ComponentAnalyzerDiagnosticAnalyzerRunner.cs b/src/Components/Analyzers/test/ComponentAnalyzerDiagnosticAnalyzerRunner.cs new file mode 100644 index 0000000000..727f060bd4 --- /dev/null +++ b/src/Components/Analyzers/test/ComponentAnalyzerDiagnosticAnalyzerRunner.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Analyzer.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.AspNetCore.Components.Analyzers +{ + internal class ComponentAnalyzerDiagnosticAnalyzerRunner : DiagnosticAnalyzerRunner + { + public ComponentAnalyzerDiagnosticAnalyzerRunner(DiagnosticAnalyzer analyzer) + { + Analyzer = analyzer; + } + + public DiagnosticAnalyzer Analyzer { get; } + + public Task GetDiagnosticsAsync(string source) + { + return GetDiagnosticsAsync(sources: new[] { source }, Analyzer, Array.Empty()); + } + + public Task GetDiagnosticsAsync(Project project) + { + return GetDiagnosticsAsync(new[] { project }, Analyzer, Array.Empty()); + } + } +} diff --git a/src/Components/Analyzers/test/ComponentInternalUsageDiagnoticsAnalyzerTest.cs b/src/Components/Analyzers/test/ComponentInternalUsageDiagnoticsAnalyzerTest.cs new file mode 100644 index 0000000000..92e2252304 --- /dev/null +++ b/src/Components/Analyzers/test/ComponentInternalUsageDiagnoticsAnalyzerTest.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Analyzer.Testing; +using Microsoft.Extensions.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Analyzers +{ + public class ComponentInternalUsageDiagnoticsAnalyzerTest : AnalyzerTestBase + { + public ComponentInternalUsageDiagnoticsAnalyzerTest() + { + Analyzer = new ComponentInternalUsageDiagnosticAnalyzer(); + Runner = new ComponentAnalyzerDiagnosticAnalyzerRunner(Analyzer); + } + + private ComponentInternalUsageDiagnosticAnalyzer Analyzer { get; } + private ComponentAnalyzerDiagnosticAnalyzerRunner Runner { get; } + + [Fact] + public async Task InternalUsage_FindsUseOfRenderTreeFrameAsParameter() + { + // Arrange + var source = Read("UsesRenderTreeFrameAsParameter"); + + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + Assert.Collection( + diagnostics, + diagnostic => + { + Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + }); + } + + [Fact] + public async Task InternalUsage_FindsUseOfRenderTreeType() + { + // Arrange + var source = Read("UsesRenderTreeFrameTypeAsLocal"); + + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + Assert.Collection( + diagnostics, + diagnostic => + { + Assert.Same(DiagnosticDescriptors.DoNotUseRenderTreeTypes, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + }); + } + } +} diff --git a/src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj b/src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj index a31c1a8f0e..45b379c65c 100644 --- a/src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj +++ b/src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj @@ -2,16 +2,24 @@ netcoreapp3.0 - - - - - + + + false + + + + + + + + + + diff --git a/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameAsParameter.cs b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameAsParameter.cs new file mode 100644 index 0000000000..415030a011 --- /dev/null +++ b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameAsParameter.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Components.RenderTree; + +namespace Microsoft.AspNetCore.Components.Analyzers.Tests.TestFiles.ComponentInternalUsageDiagnoticsAnalyzerTest +{ + class UsesRenderTreeFrameAsParameter + { + private void Test(/*MM*/RenderTreeFrame frame) + { + } + } +} diff --git a/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameTypeAsLocal.cs b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameTypeAsLocal.cs new file mode 100644 index 0000000000..bdd40c2df1 --- /dev/null +++ b/src/Components/Analyzers/test/TestFiles/ComponentInternalUsageDiagnoticsAnalyzerTest/UsesRenderTreeFrameTypeAsLocal.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.AspNetCore.Components.RenderTree; + +namespace Microsoft.AspNetCore.Components.Analyzers.Tests.TestFiles.ComponentInternalUsageDiagnoticsAnalyzerTest +{ + class UsesRenderTreeFrameTypeAsLocal + { + private void Test() + { + var test = RenderTreeFrameType./*MM*/Attribute; + GC.KeepAlive(test); + } + + } +} diff --git a/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.0.cs b/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.0.cs index d56360f9aa..a2feb9c39c 100644 --- a/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.0.cs +++ b/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.0.cs @@ -65,16 +65,10 @@ namespace Microsoft.AspNetCore.Blazor.Http } namespace Microsoft.AspNetCore.Blazor.Rendering { - public partial class WebAssemblyRenderer : Microsoft.AspNetCore.Components.Rendering.Renderer + public static partial class WebAssemblyEventDispatcher { - public WebAssemblyRenderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) : base (default(System.IServiceProvider), default(Microsoft.Extensions.Logging.ILoggerFactory)) { } - public override Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get { throw null; } } - public System.Threading.Tasks.Task AddComponentAsync(System.Type componentType, string domElementSelector) { throw null; } - public System.Threading.Tasks.Task AddComponentAsync(string domElementSelector) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } - public override System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo eventFieldInfo, System.EventArgs eventArgs) { throw null; } - protected override void Dispose(bool disposing) { } - protected override void HandleException(System.Exception exception) { } - protected override System.Threading.Tasks.Task UpdateDisplayAsync(in Microsoft.AspNetCore.Components.Rendering.RenderBatch batch) { throw null; } + [Microsoft.JSInterop.JSInvokableAttribute("DispatchEvent")] + public static System.Threading.Tasks.Task DispatchEvent(Microsoft.AspNetCore.Components.Web.WebEventDescriptor eventDescriptor, string eventArgsJson) { throw null; } } } namespace Microsoft.AspNetCore.Components.Builder diff --git a/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj b/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj index 1a0e49e112..c571f776f0 100644 --- a/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj +++ b/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -11,4 +11,10 @@ + + + + + + diff --git a/src/Components/Blazor/Blazor/src/Rendering/RendererRegistry.cs b/src/Components/Blazor/Blazor/src/Rendering/RendererRegistry.cs new file mode 100644 index 0000000000..c0f1f98e29 --- /dev/null +++ b/src/Components/Blazor/Blazor/src/Rendering/RendererRegistry.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Blazor.Rendering +{ + internal static class RendererRegistry + { + // In case there are multiple concurrent Blazor renderers in the same .NET WebAssembly + // process, we track them by ID. This allows events to be dispatched to the correct one, + // as well as rooting them for GC purposes, since nothing would otherwise be referencing + // them even though we might still receive incoming events from JS. + + private static int _nextId; + private static Dictionary _renderers = new Dictionary(); + + internal static WebAssemblyRenderer Find(int rendererId) + { + return _renderers.ContainsKey(rendererId) + ? _renderers[rendererId] + : throw new ArgumentException($"There is no renderer with ID {rendererId}."); + } + + public static int Add(WebAssemblyRenderer renderer) + { + var id = _nextId++; + _renderers.Add(id, renderer); + return id; + } + + public static bool TryRemove(int rendererId) + { + if (_renderers.ContainsKey(rendererId)) + { + _renderers.Remove(rendererId); + return true; + } + else + { + return false; + } + } + } +} diff --git a/src/Components/Blazor/Blazor/src/Rendering/WebAssemblyEventDispatcher.cs b/src/Components/Blazor/Blazor/src/Rendering/WebAssemblyEventDispatcher.cs new file mode 100644 index 0000000000..d46500bae7 --- /dev/null +++ b/src/Components/Blazor/Blazor/src/Rendering/WebAssemblyEventDispatcher.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; + +namespace Microsoft.AspNetCore.Blazor.Rendering +{ + /// + /// Dispatches events from JavaScript to a Blazor WebAssembly renderer. + /// Intended for internal use only. + /// + public static class WebAssemblyEventDispatcher + { + /// + /// For framework use only. + /// + [JSInvokable(nameof(DispatchEvent))] + public static Task DispatchEvent(WebEventDescriptor eventDescriptor, string eventArgsJson) + { + var webEvent = WebEventData.Parse(eventDescriptor, eventArgsJson); + var renderer = RendererRegistry.Find(eventDescriptor.BrowserRendererId); + return renderer.DispatchEventAsync( + webEvent.EventHandlerId, + webEvent.EventFieldInfo, + webEvent.EventArgs); + } + } +} diff --git a/src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs b/src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs index d4e867621e..5964637090 100644 --- a/src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Blazor.Services; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.Extensions.Logging; @@ -16,7 +15,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering /// Provides mechanisms for rendering instances in a /// web browser, dispatching events to them, and refreshing the UI as required. /// - public class WebAssemblyRenderer : Renderer + internal class WebAssemblyRenderer : Renderer { private readonly int _webAssemblyRendererId; @@ -31,10 +30,8 @@ namespace Microsoft.AspNetCore.Blazor.Rendering public WebAssemblyRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) : base(serviceProvider, loggerFactory) { - // The browser renderer registers and unregisters itself with the static - // registry. This works well with the WebAssembly runtime, and is simple for the - // case where Blazor is running in process. - _webAssemblyRendererId = RendererRegistry.Current.Add(this); + // The WebAssembly renderer registers and unregisters itself with the static registry + _webAssemblyRendererId = RendererRegistry.Add(this); } public override Dispatcher Dispatcher => NullDispatcher.Instance; @@ -77,9 +74,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering WebAssemblyJSRuntime.Instance.Invoke( "Blazor._internal.attachRootComponentToElement", - _webAssemblyRendererId, domElementSelector, - componentId); + componentId, + _webAssemblyRendererId); return RenderRootComponentAsync(componentId); } @@ -88,7 +85,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering protected override void Dispose(bool disposing) { base.Dispose(disposing); - RendererRegistry.Current.TryRemove(_webAssemblyRendererId); + RendererRegistry.TryRemove(_webAssemblyRendererId); } /// diff --git a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj index ab757d5c82..adfa71ef6b 100644 --- a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj +++ b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets index 96a844817e..42ef903f15 100644 --- a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets +++ b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets @@ -436,9 +436,15 @@ + + + + <_MonoLinkerDotNetPath>$(DOTNET_HOST_PATH) + <_MonoLinkerDotNetPath Condition="'$(_MonoLinkerDotNetPath)' == ''">dotnet + - + diff --git a/src/Components/Blazor/Build/test/ChildContentRazorIntegrationTest.cs b/src/Components/Blazor/Build/test/ChildContentRazorIntegrationTest.cs index 90090cb070..720c00fe9b 100644 --- a/src/Components/Blazor/Build/test/ChildContentRazorIntegrationTest.cs +++ b/src/Components/Blazor/Build/test/ChildContentRazorIntegrationTest.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.CodeAnalysis.CSharp; using Xunit; @@ -14,7 +13,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test { private readonly CSharpSyntaxTree RenderChildContentComponent = Parse(@" using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Test { public class RenderChildContent : ComponentBase @@ -32,7 +31,7 @@ namespace Test private readonly CSharpSyntaxTree RenderChildContentStringComponent = Parse(@" using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Test { public class RenderChildContentString : ComponentBase @@ -53,7 +52,7 @@ namespace Test private readonly CSharpSyntaxTree RenderMultipleChildContent = Parse(@" using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Test { public class RenderMultipleChildContent : ComponentBase diff --git a/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs b/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs index 6c0a7fab96..6513224c05 100644 --- a/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs +++ b/src/Components/Blazor/Build/test/ComponentRenderingRazorIntegrationTest.cs @@ -565,7 +565,7 @@ namespace Test // Arrange AdditionalSyntaxTrees.Add(Parse(@" using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Test { diff --git a/src/Components/Blazor/Build/test/GenericComponentRazorIntegrationTest.cs b/src/Components/Blazor/Build/test/GenericComponentRazorIntegrationTest.cs index 45c178e4bb..299683b68f 100644 --- a/src/Components/Blazor/Build/test/GenericComponentRazorIntegrationTest.cs +++ b/src/Components/Blazor/Build/test/GenericComponentRazorIntegrationTest.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.CodeAnalysis.CSharp; using Xunit; @@ -19,7 +18,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test using System; using System.Collections.Generic; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Test { public class GenericContext : ComponentBase @@ -57,7 +56,7 @@ namespace Test private readonly CSharpSyntaxTree MultipleGenericParameterComponent = Parse(@" using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Test { public class MultipleGenericParameter : ComponentBase diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor index c5ee6a53e2..1c360b7121 100644 --- a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor +++ b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor @@ -1,5 +1,10 @@ - + + + + -

Sorry, there's nothing at this address.

+ +

Sorry, there's nothing at this address.

+
diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Pages/_Imports.razor b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Pages/_Imports.razor deleted file mode 100644 index 0f24edaf1d..0000000000 --- a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Pages/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@layout MainLayout diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Shared/SurveyPrompt.razor b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Shared/SurveyPrompt.razor index 5baad704a9..d392ed3eeb 100644 --- a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Shared/SurveyPrompt.razor +++ b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Shared/SurveyPrompt.razor @@ -4,7 +4,7 @@ Please take our - brief survey + brief survey and tell us what you think. diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor b/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor index 7f4cf93fbb..fe885300e7 100644 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor +++ b/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor @@ -1 +1,8 @@ - + + + + + + Sorry, there's nothing here. + + diff --git a/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj b/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj index e40ea493bd..b186c39194 100644 --- a/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj +++ b/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj @@ -11,7 +11,4 @@ true - - - diff --git a/src/Components/Blazor/testassets/StandaloneApp/App.razor b/src/Components/Blazor/testassets/StandaloneApp/App.razor index 4b8a0ffa42..fe6830bb01 100644 --- a/src/Components/Blazor/testassets/StandaloneApp/App.razor +++ b/src/Components/Blazor/testassets/StandaloneApp/App.razor @@ -1,5 +1,11 @@ - - + + + + + + +

Not found

+ Sorry, there's nothing at this address. +
+
+
diff --git a/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor b/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor deleted file mode 100644 index 5e11c2a20c..0000000000 --- a/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@layout MainLayout diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs index 4f317be0ff..e3d29e6a52 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -16,6 +16,15 @@ namespace Microsoft.AspNetCore.Components public abstract System.Threading.Tasks.Task GetAuthenticationStateAsync(); protected void NotifyAuthenticationStateChanged(System.Threading.Tasks.Task task) { } } + public sealed partial class AuthorizeRouteView : Microsoft.AspNetCore.Components.RouteView + { + public AuthorizeRouteView() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + protected override void Render(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } + } public partial class AuthorizeView : Microsoft.AspNetCore.Components.AuthorizeViewCore { public AuthorizeView() { } @@ -38,7 +47,7 @@ namespace Microsoft.AspNetCore.Components public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] public object Resource { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } protected abstract Microsoft.AspNetCore.Authorization.IAuthorizeData[] GetAuthorizeData(); [System.Diagnostics.DebuggerStepThroughAttribute] protected override System.Threading.Tasks.Task OnParametersSetAsync() { throw null; } @@ -95,7 +104,7 @@ namespace Microsoft.AspNetCore.Components public CascadingAuthenticationState() { } [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } protected override void OnInitialized() { } void System.IDisposable.Dispose() { } } @@ -105,7 +114,7 @@ namespace Microsoft.AspNetCore.Components public CascadingParameterAttribute() { } public string Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } - public partial class CascadingValue : Microsoft.AspNetCore.Components.IComponent + public partial class CascadingValue : Microsoft.AspNetCore.Components.IComponent { public CascadingValue() { } [Microsoft.AspNetCore.Components.ParameterAttribute] @@ -115,7 +124,7 @@ namespace Microsoft.AspNetCore.Components [Microsoft.AspNetCore.Components.ParameterAttribute] public string Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] - public T Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } } @@ -127,7 +136,7 @@ namespace Microsoft.AspNetCore.Components public abstract partial class ComponentBase : Microsoft.AspNetCore.Components.IComponent, Microsoft.AspNetCore.Components.IHandleAfterRender, Microsoft.AspNetCore.Components.IHandleEvent { public ComponentBase() { } - protected virtual void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + protected virtual void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } protected System.Threading.Tasks.Task InvokeAsync(System.Action workItem) { throw null; } protected System.Threading.Tasks.Task InvokeAsync(System.Func workItem) { throw null; } void Microsoft.AspNetCore.Components.IComponent.Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } @@ -179,17 +188,17 @@ namespace Microsoft.AspNetCore.Components public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Func callback) { throw null; } public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Func callback) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - public Microsoft.AspNetCore.Components.EventCallback CreateInferred(object receiver, System.Action callback, T value) { throw null; } + public Microsoft.AspNetCore.Components.EventCallback CreateInferred(object receiver, System.Action callback, TValue value) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - public Microsoft.AspNetCore.Components.EventCallback CreateInferred(object receiver, System.Func callback, T value) { throw null; } + public Microsoft.AspNetCore.Components.EventCallback CreateInferred(object receiver, System.Func callback, TValue value) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, Microsoft.AspNetCore.Components.EventCallback callback) { throw null; } + public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, Microsoft.AspNetCore.Components.EventCallback callback) { throw null; } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] - public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, Microsoft.AspNetCore.Components.EventCallback callback) { throw null; } - public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Action callback) { throw null; } - public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Action callback) { throw null; } - public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Func callback) { throw null; } - public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Func callback) { throw null; } + public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, Microsoft.AspNetCore.Components.EventCallback callback) { throw null; } + public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Action callback) { throw null; } + public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Action callback) { throw null; } + public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Func callback) { throw null; } + public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Func callback) { throw null; } } public static partial class EventCallbackFactoryBinderExtensions { @@ -232,13 +241,13 @@ namespace Microsoft.AspNetCore.Components public System.Threading.Tasks.Task InvokeAsync(object arg) { throw null; } } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] - public readonly partial struct EventCallback + public readonly partial struct EventCallback { private readonly object _dummy; - public static readonly Microsoft.AspNetCore.Components.EventCallback Empty; + public static readonly Microsoft.AspNetCore.Components.EventCallback Empty; public EventCallback(Microsoft.AspNetCore.Components.IHandleEvent receiver, System.MulticastDelegate @delegate) { throw null; } public bool HasDelegate { get { throw null; } } - public System.Threading.Tasks.Task InvokeAsync(T arg) { throw null; } + public System.Threading.Tasks.Task InvokeAsync(TValue arg) { throw null; } } public partial interface IComponent { @@ -278,6 +287,16 @@ namespace Microsoft.AspNetCore.Components [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.RenderFragment Body { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } + public partial class LayoutView : Microsoft.AspNetCore.Components.IComponent + { + public LayoutView() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public System.Type Layout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } + public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } + } public sealed partial class LocationChangeException : System.Exception { public LocationChangeException(string message, System.Exception innerException) { } @@ -323,20 +342,6 @@ namespace Microsoft.AspNetCore.Components protected OwningComponentBase() { } protected TService Service { get { throw null; } } } - public partial class PageDisplay : Microsoft.AspNetCore.Components.IComponent - { - public PageDisplay() { } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public System.Type Page { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public System.Collections.Generic.IDictionary PageParameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } - public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } - } [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)] public sealed partial class ParameterAttribute : System.Attribute { @@ -360,11 +365,11 @@ namespace Microsoft.AspNetCore.Components public static Microsoft.AspNetCore.Components.ParameterView Empty { get { throw null; } } public static Microsoft.AspNetCore.Components.ParameterView FromDictionary(System.Collections.Generic.IDictionary parameters) { throw null; } public Microsoft.AspNetCore.Components.ParameterView.Enumerator GetEnumerator() { throw null; } - public T GetValueOrDefault(string parameterName) { throw null; } - public T GetValueOrDefault(string parameterName, T defaultValue) { throw null; } + public TValue GetValueOrDefault(string parameterName) { throw null; } + public TValue GetValueOrDefault(string parameterName, TValue defaultValue) { throw null; } public void SetParameterProperties(object target) { } public System.Collections.Generic.IReadOnlyDictionary ToDictionary() { throw null; } - public bool TryGetValue(string parameterName, out T result) { throw null; } + public bool TryGetValue(string parameterName, out TValue result) { throw null; } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public partial struct Enumerator { @@ -374,8 +379,8 @@ namespace Microsoft.AspNetCore.Components public bool MoveNext() { throw null; } } } - public delegate void RenderFragment(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder); - public delegate Microsoft.AspNetCore.Components.RenderFragment RenderFragment(T value); + public delegate void RenderFragment(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder); + public delegate Microsoft.AspNetCore.Components.RenderFragment RenderFragment(TValue value); [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct RenderHandle { @@ -391,6 +396,23 @@ namespace Microsoft.AspNetCore.Components public RouteAttribute(string template) { } public string Template { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } + public sealed partial class RouteData + { + public RouteData(System.Type pageType, System.Collections.Generic.IReadOnlyDictionary routeValues) { } + public System.Type PageType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.Collections.Generic.IReadOnlyDictionary RouteValues { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + public partial class RouteView : Microsoft.AspNetCore.Components.IComponent + { + public RouteView() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public System.Type DefaultLayout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RouteData RouteData { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } + protected virtual void Render(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } + public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } + } } namespace Microsoft.AspNetCore.Components.CompilerServices { @@ -444,7 +466,7 @@ namespace Microsoft.AspNetCore.Components.Forms public FieldIdentifier(object model, string fieldName) { throw null; } public string FieldName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public object Model { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - public static Microsoft.AspNetCore.Components.Forms.FieldIdentifier Create(System.Linq.Expressions.Expression> accessor) { throw null; } + public static Microsoft.AspNetCore.Components.Forms.FieldIdentifier Create(System.Linq.Expressions.Expression> accessor) { throw null; } public bool Equals(Microsoft.AspNetCore.Components.Forms.FieldIdentifier otherIdentifier) { throw null; } public override bool Equals(object obj) { throw null; } public override int GetHashCode() { throw null; } @@ -513,18 +535,50 @@ namespace Microsoft.AspNetCore.Components.Rendering public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } public abstract Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get; } public event System.UnhandledExceptionEventHandler UnhandledSynchronizationException { add { } remove { } } - protected internal virtual void AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) { } protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; } public virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, System.EventArgs eventArgs) { throw null; } public void Dispose() { } protected virtual void Dispose(bool disposing) { } protected abstract void HandleException(System.Exception exception); protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; } + protected virtual void ProcessPendingRender() { } protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId) { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId, Microsoft.AspNetCore.Components.ParameterView initialParameters) { throw null; } protected abstract System.Threading.Tasks.Task UpdateDisplayAsync(in Microsoft.AspNetCore.Components.Rendering.RenderBatch renderBatch); } + public sealed partial class RenderTreeBuilder : System.IDisposable + { + public RenderTreeBuilder() { } + public void AddAttribute(int sequence, in Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame frame) { } + public void AddAttribute(int sequence, string name, Microsoft.AspNetCore.Components.EventCallback value) { } + public void AddAttribute(int sequence, string name, bool value) { } + public void AddAttribute(int sequence, string name, System.MulticastDelegate value) { } + public void AddAttribute(int sequence, string name, object value) { } + public void AddAttribute(int sequence, string name, string value) { } + public void AddAttribute(int sequence, string name, Microsoft.AspNetCore.Components.EventCallback value) { } + public void AddComponentReferenceCapture(int sequence, System.Action componentReferenceCaptureAction) { } + public void AddContent(int sequence, Microsoft.AspNetCore.Components.MarkupString markupContent) { } + public void AddContent(int sequence, Microsoft.AspNetCore.Components.RenderFragment fragment) { } + public void AddContent(int sequence, object textContent) { } + public void AddContent(int sequence, string textContent) { } + public void AddContent(int sequence, Microsoft.AspNetCore.Components.RenderFragment fragment, TValue value) { } + public void AddElementReferenceCapture(int sequence, System.Action elementReferenceCaptureAction) { } + public void AddMarkupContent(int sequence, string markupContent) { } + public void AddMultipleAttributes(int sequence, System.Collections.Generic.IEnumerable> attributes) { } + public void Clear() { } + public void CloseComponent() { } + public void CloseElement() { } + public void CloseRegion() { } + public Microsoft.AspNetCore.Components.RenderTree.ArrayRange GetFrames() { throw null; } + public void OpenComponent(int sequence, System.Type componentType) { } + public void OpenComponent(int sequence) where TComponent : Microsoft.AspNetCore.Components.IComponent { } + public void OpenElement(int sequence, string elementName) { } + public void OpenRegion(int sequence) { } + public void SetKey(object value) { } + public void SetUpdatesAttributeName(string updatesAttributeName) { } + void System.IDisposable.Dispose() { } + } } namespace Microsoft.AspNetCore.Components.RenderTree { @@ -548,38 +602,6 @@ namespace Microsoft.AspNetCore.Components.RenderTree public ArrayRange(T[] array, int count) { throw null; } public Microsoft.AspNetCore.Components.RenderTree.ArrayRange Clone() { throw null; } } - public sealed partial class RenderTreeBuilder : System.IDisposable - { - public RenderTreeBuilder() { } - public void AddAttribute(int sequence, in Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame frame) { } - public void AddAttribute(int sequence, string name, Microsoft.AspNetCore.Components.EventCallback value) { } - public void AddAttribute(int sequence, string name, bool value) { } - public void AddAttribute(int sequence, string name, System.MulticastDelegate value) { } - public void AddAttribute(int sequence, string name, object value) { } - public void AddAttribute(int sequence, string name, string value) { } - public void AddAttribute(int sequence, string name, Microsoft.AspNetCore.Components.EventCallback value) { } - public void AddComponentReferenceCapture(int sequence, System.Action componentReferenceCaptureAction) { } - public void AddContent(int sequence, Microsoft.AspNetCore.Components.MarkupString markupContent) { } - public void AddContent(int sequence, Microsoft.AspNetCore.Components.RenderFragment fragment) { } - public void AddContent(int sequence, object textContent) { } - public void AddContent(int sequence, string textContent) { } - public void AddContent(int sequence, Microsoft.AspNetCore.Components.RenderFragment fragment, T value) { } - public void AddElementReferenceCapture(int sequence, System.Action elementReferenceCaptureAction) { } - public void AddMarkupContent(int sequence, string markupContent) { } - public void AddMultipleAttributes(int sequence, System.Collections.Generic.IEnumerable> attributes) { } - public void Clear() { } - public void CloseComponent() { } - public void CloseElement() { } - public void CloseRegion() { } - public Microsoft.AspNetCore.Components.RenderTree.ArrayRange GetFrames() { throw null; } - public void OpenComponent(int sequence, System.Type componentType) { } - public void OpenComponent(int sequence) where TComponent : Microsoft.AspNetCore.Components.IComponent { } - public void OpenElement(int sequence, string elementName) { } - public void OpenRegion(int sequence) { } - public void SetKey(object value) { } - public void SetUpdatesAttributeName(string updatesAttributeName) { } - void System.IDisposable.Dispose() { } - } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct RenderTreeDiff { @@ -648,15 +670,12 @@ namespace Microsoft.AspNetCore.Components.Routing [Microsoft.AspNetCore.Components.ParameterAttribute] public System.Reflection.Assembly AppAssembly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public Microsoft.AspNetCore.Components.RenderFragment Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.RenderFragment NotFound { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } public void Dispose() { } System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; } - protected virtual void Render(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder, System.Type handler, System.Collections.Generic.IDictionary parameters) { } public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } } } diff --git a/src/Components/Components/src/Auth/AuthorizeRouteView.cs b/src/Components/Components/src/Auth/AuthorizeRouteView.cs new file mode 100644 index 0000000000..b0d01ab093 --- /dev/null +++ b/src/Components/Components/src/Auth/AuthorizeRouteView.cs @@ -0,0 +1,118 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components.Auth; +using Microsoft.AspNetCore.Components.Rendering; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Combines the behaviors of and , + /// so that it displays the page matching the specified route but only if the user + /// is authorized to see it. + /// + /// Additionally, this component supplies a cascading parameter of type , + /// which makes the user's current authentication state available to descendants. + /// + public sealed class AuthorizeRouteView : RouteView + { + // We expect applications to supply their own authorizing/not-authorized content, but + // it's better to have defaults than to make the parameters mandatory because in some + // cases they will never be used (e.g., "authorizing" in out-of-box server-side Blazor) + private static readonly RenderFragment _defaultNotAuthorizedContent + = state => builder => builder.AddContent(0, "Not authorized"); + private static readonly RenderFragment _defaultAuthorizingContent + = builder => builder.AddContent(0, "Authorizing..."); + + private readonly RenderFragment _renderAuthorizeRouteViewCoreDelegate; + private readonly RenderFragment _renderAuthorizedDelegate; + private readonly RenderFragment _renderNotAuthorizedDelegate; + private readonly RenderFragment _renderAuthorizingDelegate; + + public AuthorizeRouteView() + { + // Cache the rendering delegates so that we only construct new closure instances + // when they are actually used (e.g., we never prepare a RenderFragment bound to + // the NotAuthorized content except when you are displaying that particular state) + RenderFragment renderBaseRouteViewDelegate = builder => base.Render(builder); + _renderAuthorizedDelegate = authenticateState => renderBaseRouteViewDelegate; + _renderNotAuthorizedDelegate = authenticationState => builder => RenderNotAuthorizedInDefaultLayout(builder, authenticationState); + _renderAuthorizingDelegate = RenderAuthorizingInDefaultLayout; + _renderAuthorizeRouteViewCoreDelegate = RenderAuthorizeRouteViewCore; + } + + /// + /// The content that will be displayed if the user is not authorized. + /// + [Parameter] + public RenderFragment NotAuthorized { get; set; } + + /// + /// The content that will be displayed while asynchronous authorization is in progress. + /// + [Parameter] + public RenderFragment Authorizing { get; set; } + + [CascadingParameter] + private Task ExistingCascadedAuthenticationState { get; set; } + + /// + protected override void Render(RenderTreeBuilder builder) + { + if (ExistingCascadedAuthenticationState != null) + { + // If this component is already wrapped in a (or another + // compatible provider), then don't interfere with the cascaded authentication state. + _renderAuthorizeRouteViewCoreDelegate(builder); + } + else + { + // Otherwise, implicitly wrap the output in a + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(CascadingAuthenticationState.ChildContent), _renderAuthorizeRouteViewCoreDelegate); + builder.CloseComponent(); + } + } + + private void RenderAuthorizeRouteViewCore(RenderTreeBuilder builder) + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(AuthorizeRouteViewCore.RouteData), RouteData); + builder.AddAttribute(2, nameof(AuthorizeRouteViewCore.Authorized), _renderAuthorizedDelegate); + builder.AddAttribute(3, nameof(AuthorizeRouteViewCore.Authorizing), _renderAuthorizingDelegate); + builder.AddAttribute(4, nameof(AuthorizeRouteViewCore.NotAuthorized), _renderNotAuthorizedDelegate); + builder.CloseComponent(); + } + + private void RenderContentInDefaultLayout(RenderTreeBuilder builder, RenderFragment content) + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(LayoutView.Layout), DefaultLayout); + builder.AddAttribute(2, nameof(LayoutView.ChildContent), content); + builder.CloseComponent(); + } + + private void RenderNotAuthorizedInDefaultLayout(RenderTreeBuilder builder, AuthenticationState authenticationState) + { + var content = NotAuthorized ?? _defaultNotAuthorizedContent; + RenderContentInDefaultLayout(builder, content(authenticationState)); + } + + private void RenderAuthorizingInDefaultLayout(RenderTreeBuilder builder) + { + var content = Authorizing ?? _defaultAuthorizingContent; + RenderContentInDefaultLayout(builder, content); + } + + private class AuthorizeRouteViewCore : AuthorizeViewCore + { + [Parameter] + public RouteData RouteData { get; set; } + + protected override IAuthorizeData[] GetAuthorizeData() + => AttributeAuthorizeDataCache.GetAuthorizeDataForType(RouteData.PageType); + } + } +} diff --git a/src/Components/Components/src/Auth/AuthorizeViewCore.cs b/src/Components/Components/src/Auth/AuthorizeViewCore.cs index 8c60078184..cdbce6e8d2 100644 --- a/src/Components/Components/src/Auth/AuthorizeViewCore.cs +++ b/src/Components/Components/src/Auth/AuthorizeViewCore.cs @@ -5,7 +5,7 @@ using System; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components { @@ -52,6 +52,8 @@ namespace Microsoft.AspNetCore.Components /// protected override void BuildRenderTree(RenderTreeBuilder builder) { + // We're using the same sequence number for each of the content items here + // so that we can update existing instances if they are the same shape if (currentAuthenticationState == null) { builder.AddContent(0, Authorizing); @@ -59,11 +61,11 @@ namespace Microsoft.AspNetCore.Components else if (isAuthorized) { var authorized = Authorized ?? ChildContent; - builder.AddContent(1, authorized?.Invoke(currentAuthenticationState)); + builder.AddContent(0, authorized?.Invoke(currentAuthenticationState)); } else { - builder.AddContent(2, NotAuthorized?.Invoke(currentAuthenticationState)); + builder.AddContent(0, NotAuthorized?.Invoke(currentAuthenticationState)); } } @@ -102,6 +104,12 @@ namespace Microsoft.AspNetCore.Components private async Task IsAuthorizedAsync(ClaimsPrincipal user) { var authorizeData = GetAuthorizeData(); + if (authorizeData == null) + { + // No authorization applies, so no need to consult the authorization service + return true; + } + EnsureNoAuthenticationSchemeSpecified(authorizeData); var policy = await AuthorizationPolicy.CombineAsync( diff --git a/src/Components/Components/src/Auth/CascadingAuthenticationState.razor b/src/Components/Components/src/Auth/CascadingAuthenticationState.razor index 4f2e2bd630..695ebd1972 100644 --- a/src/Components/Components/src/Auth/CascadingAuthenticationState.razor +++ b/src/Components/Components/src/Auth/CascadingAuthenticationState.razor @@ -2,7 +2,7 @@ @implements IDisposable @inject AuthenticationStateProvider AuthenticationStateProvider - + @code { private Task _currentAuthenticationStateTask; diff --git a/src/Components/Components/src/CascadingValue.cs b/src/Components/Components/src/CascadingValue.cs index 1a03c2557e..564ca99def 100644 --- a/src/Components/Components/src/CascadingValue.cs +++ b/src/Components/Components/src/CascadingValue.cs @@ -5,14 +5,13 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; -using Microsoft.AspNetCore.Components.RenderTree; namespace Microsoft.AspNetCore.Components { /// /// A component that provides a cascading value to all descendant components. /// - public class CascadingValue : ICascadingValueComponent, IComponent + public class CascadingValue : ICascadingValueComponent, IComponent { private RenderHandle _renderHandle; private HashSet _subscribers; // Lazily instantiated @@ -26,7 +25,7 @@ namespace Microsoft.AspNetCore.Components /// /// The value to be provided. /// - [Parameter] public T Value { get; set; } + [Parameter] public TValue Value { get; set; } /// /// Optionally gives a name to the provided value. Descendant components @@ -74,7 +73,7 @@ namespace Microsoft.AspNetCore.Components { if (parameter.Name.Equals(nameof(Value), StringComparison.OrdinalIgnoreCase)) { - Value = (T)parameter.Value; + Value = (TValue)parameter.Value; hasSuppliedValue = true; } else if (parameter.Name.Equals(nameof(ChildContent), StringComparison.OrdinalIgnoreCase)) @@ -86,7 +85,7 @@ namespace Microsoft.AspNetCore.Components Name = (string)parameter.Value; if (string.IsNullOrEmpty(Name)) { - throw new ArgumentException($"The parameter '{nameof(Name)}' for component '{nameof(CascadingValue)}' does not allow null or empty values."); + throw new ArgumentException($"The parameter '{nameof(Name)}' for component '{nameof(CascadingValue)}' does not allow null or empty values."); } } else if (parameter.Name.Equals(nameof(IsFixed), StringComparison.OrdinalIgnoreCase)) @@ -95,7 +94,7 @@ namespace Microsoft.AspNetCore.Components } else { - throw new ArgumentException($"The component '{nameof(CascadingValue)}' does not accept a parameter with the name '{parameter.Name}'."); + throw new ArgumentException($"The component '{nameof(CascadingValue)}' does not accept a parameter with the name '{parameter.Name}'."); } } @@ -136,7 +135,7 @@ namespace Microsoft.AspNetCore.Components bool ICascadingValueComponent.CanSupplyValue(Type requestedType, string requestedName) { - if (!requestedType.IsAssignableFrom(typeof(T))) + if (!requestedType.IsAssignableFrom(typeof(TValue))) { return false; } diff --git a/src/Components/Components/src/ComponentBase.cs b/src/Components/Components/src/ComponentBase.cs index baa91fd458..018c016975 100644 --- a/src/Components/Components/src/ComponentBase.cs +++ b/src/Components/Components/src/ComponentBase.cs @@ -3,7 +3,7 @@ using System; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components { @@ -171,10 +171,25 @@ namespace Microsoft.AspNetCore.Components _renderHandle = renderHandle; } + /// - /// Method invoked to apply initial or updated parameters to the component. + /// Sets parameters supplied by the component's parent in the render tree. /// - /// The parameters to apply. + /// The parameters. + /// A that completes when the component has finished updating and rendering itself. + /// + /// + /// The method should be passed the entire set of parameter values each + /// time is called. It not required that the caller supply a parameter + /// value for all parameters that are logically understood by the component. + /// + /// + /// The default implementation of will set the value of each property + /// decorated with or that has + /// a corresponding value in the . Parameters that do not have a corresponding value + /// will be unchanged. + /// + /// public virtual Task SetParametersAsync(ParameterView parameters) { parameters.SetParameterProperties(this); diff --git a/src/Components/Components/src/ComponentFactory.cs b/src/Components/Components/src/ComponentFactory.cs index aebf96bc06..bf5de30d2a 100644 --- a/src/Components/Components/src/ComponentFactory.cs +++ b/src/Components/Components/src/ComponentFactory.cs @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Components ( propertyName: property.Name, propertyType: property.PropertyType, - setter: MemberAssignment.CreatePropertySetter(type, property) + setter: MemberAssignment.CreatePropertySetter(type, property, cascading: false) )).ToArray(); return Initialize; diff --git a/src/Components/Components/src/EventCallback.cs b/src/Components/Components/src/EventCallback.cs index 0d37124c64..3ba05ceaba 100644 --- a/src/Components/Components/src/EventCallback.cs +++ b/src/Components/Components/src/EventCallback.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components internal readonly IHandleEvent Receiver; /// - /// Creates the new . + /// Creates the new . /// /// The event receiver. /// The delegate to bind. @@ -66,73 +66,4 @@ namespace Microsoft.AspNetCore.Components return RequiresExplicitReceiver ? (object)this : Delegate; } } - - /// - /// A bound event handler delegate. - /// - public readonly struct EventCallback : IEventCallback - { - /// - /// Gets an empty . - /// - public static readonly EventCallback Empty = new EventCallback(null, (Action)(() => { })); - - internal readonly MulticastDelegate Delegate; - internal readonly IHandleEvent Receiver; - - /// - /// Creates the new . - /// - /// The event receiver. - /// The delegate to bind. - public EventCallback(IHandleEvent receiver, MulticastDelegate @delegate) - { - Receiver = receiver; - Delegate = @delegate; - } - - /// - /// Gets a value that indicates whether the delegate associated with this event dispatcher is non-null. - /// - public bool HasDelegate => Delegate != null; - - // This is a hint to the runtime that Receiver is a different object than what - // Delegate.Target points to. This allows us to avoid boxing the command object - // when building the render tree. See logic where this is used. - internal bool RequiresExplicitReceiver => Receiver != null && !object.ReferenceEquals(Receiver, Delegate?.Target); - - /// - /// Invokes the delegate associated with this binding and dispatches an event notification to the - /// appropriate component. - /// - /// The argument. - /// A which completes asynchronously once event processing has completed. - public Task InvokeAsync(T arg) - { - if (Receiver == null) - { - return EventCallbackWorkItem.InvokeAsync(Delegate, arg); - } - - return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg); - } - - internal EventCallback AsUntyped() - { - return new EventCallback(Receiver ?? Delegate?.Target as IHandleEvent, Delegate); - } - - object IEventCallback.UnpackForRenderTree() - { - return RequiresExplicitReceiver ? (object)AsUntyped() : Delegate; - } - } - - // Used to understand boxed generic EventCallbacks - internal interface IEventCallback - { - bool HasDelegate { get; } - - object UnpackForRenderTree(); - } } diff --git a/src/Components/Components/src/EventCallbackFactory.cs b/src/Components/Components/src/EventCallbackFactory.cs index a920d2277d..1b53376b85 100644 --- a/src/Components/Components/src/EventCallbackFactory.cs +++ b/src/Components/Components/src/EventCallbackFactory.cs @@ -105,14 +105,14 @@ namespace Microsoft.AspNetCore.Components /// /// [EditorBrowsable(EditorBrowsableState.Never)] - public EventCallback Create(object receiver, EventCallback callback) + public EventCallback Create(object receiver, EventCallback callback) { if (receiver == null) { throw new ArgumentNullException(nameof(receiver)); } - return new EventCallback(callback.Receiver, callback.Delegate); + return new EventCallback(callback.Receiver, callback.Delegate); } /// @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Components /// /// [EditorBrowsable(EditorBrowsableState.Never)] - public EventCallback Create(object receiver, EventCallback callback) + public EventCallback Create(object receiver, EventCallback callback) { if (receiver == null) { @@ -139,14 +139,14 @@ namespace Microsoft.AspNetCore.Components /// The event receiver. /// The event callback. /// The . - public EventCallback Create(object receiver, Action callback) + public EventCallback Create(object receiver, Action callback) { if (receiver == null) { throw new ArgumentNullException(nameof(receiver)); } - return CreateCore(receiver, callback); + return CreateCore(receiver, callback); } /// @@ -156,14 +156,14 @@ namespace Microsoft.AspNetCore.Components /// The event receiver. /// The event callback. /// The . - public EventCallback Create(object receiver, Action callback) + public EventCallback Create(object receiver, Action callback) { if (receiver == null) { throw new ArgumentNullException(nameof(receiver)); } - return CreateCore(receiver, callback); + return CreateCore(receiver, callback); } /// @@ -173,14 +173,14 @@ namespace Microsoft.AspNetCore.Components /// The event receiver. /// The event callback. /// The . - public EventCallback Create(object receiver, Func callback) + public EventCallback Create(object receiver, Func callback) { if (receiver == null) { throw new ArgumentNullException(nameof(receiver)); } - return CreateCore(receiver, callback); + return CreateCore(receiver, callback); } /// @@ -190,14 +190,14 @@ namespace Microsoft.AspNetCore.Components /// The event receiver. /// The event callback. /// The . - public EventCallback Create(object receiver, Func callback) + public EventCallback Create(object receiver, Func callback) { if (receiver == null) { throw new ArgumentNullException(nameof(receiver)); } - return CreateCore(receiver, callback); + return CreateCore(receiver, callback); } /// @@ -209,7 +209,7 @@ namespace Microsoft.AspNetCore.Components /// /// [EditorBrowsable(EditorBrowsableState.Never)] - public EventCallback CreateInferred(object receiver, Action callback, T value) + public EventCallback CreateInferred(object receiver, Action callback, TValue value) { return Create(receiver, callback); } @@ -223,7 +223,7 @@ namespace Microsoft.AspNetCore.Components /// /// [EditorBrowsable(EditorBrowsableState.Never)] - public EventCallback CreateInferred(object receiver, Func callback, T value) + public EventCallback CreateInferred(object receiver, Func callback, TValue value) { return Create(receiver, callback); } @@ -233,9 +233,9 @@ namespace Microsoft.AspNetCore.Components return new EventCallback(callback?.Target as IHandleEvent ?? receiver as IHandleEvent, callback); } - private EventCallback CreateCore(object receiver, MulticastDelegate callback) + private EventCallback CreateCore(object receiver, MulticastDelegate callback) { - return new EventCallback(callback?.Target as IHandleEvent ?? receiver as IHandleEvent, callback); + return new EventCallback(callback?.Target as IHandleEvent ?? receiver as IHandleEvent, callback); } } } diff --git a/src/Components/Components/src/EventCallbackOfT.cs b/src/Components/Components/src/EventCallbackOfT.cs new file mode 100644 index 0000000000..918d8d62e2 --- /dev/null +++ b/src/Components/Components/src/EventCallbackOfT.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// A bound event handler delegate. + /// + public readonly struct EventCallback : IEventCallback + { + /// + /// Gets an empty . + /// + public static readonly EventCallback Empty = new EventCallback(null, (Action)(() => { })); + + internal readonly MulticastDelegate Delegate; + internal readonly IHandleEvent Receiver; + + /// + /// Creates the new . + /// + /// The event receiver. + /// The delegate to bind. + public EventCallback(IHandleEvent receiver, MulticastDelegate @delegate) + { + Receiver = receiver; + Delegate = @delegate; + } + + /// + /// Gets a value that indicates whether the delegate associated with this event dispatcher is non-null. + /// + public bool HasDelegate => Delegate != null; + + // This is a hint to the runtime that Receiver is a different object than what + // Delegate.Target points to. This allows us to avoid boxing the command object + // when building the render tree. See logic where this is used. + internal bool RequiresExplicitReceiver => Receiver != null && !object.ReferenceEquals(Receiver, Delegate?.Target); + + /// + /// Invokes the delegate associated with this binding and dispatches an event notification to the + /// appropriate component. + /// + /// The argument. + /// A which completes asynchronously once event processing has completed. + public Task InvokeAsync(TValue arg) + { + if (Receiver == null) + { + return EventCallbackWorkItem.InvokeAsync(Delegate, arg); + } + + return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg); + } + + internal EventCallback AsUntyped() + { + return new EventCallback(Receiver ?? Delegate?.Target as IHandleEvent, Delegate); + } + + object IEventCallback.UnpackForRenderTree() + { + return RequiresExplicitReceiver ? (object)AsUntyped() : Delegate; + } + } +} diff --git a/src/Components/Components/src/Forms/FieldIdentifier.cs b/src/Components/Components/src/Forms/FieldIdentifier.cs index fa3fb72dad..1c2c923d38 100644 --- a/src/Components/Components/src/Forms/FieldIdentifier.cs +++ b/src/Components/Components/src/Forms/FieldIdentifier.cs @@ -16,7 +16,8 @@ namespace Microsoft.AspNetCore.Components.Forms /// Initializes a new instance of the structure. /// /// An expression that identifies an object member. - public static FieldIdentifier Create(Expression> accessor) + /// The field . + public static FieldIdentifier Create(Expression> accessor) { if (accessor == null) { diff --git a/src/Components/Components/src/IComponent.cs b/src/Components/Components/src/IComponent.cs index ec1059b1e6..936cd37944 100644 --- a/src/Components/Components/src/IComponent.cs +++ b/src/Components/Components/src/IComponent.cs @@ -21,6 +21,11 @@ namespace Microsoft.AspNetCore.Components /// /// The parameters. /// A that completes when the component has finished updating and rendering itself. + /// + /// The method should be passed the entire set of parameter values each + /// time is called. It not required that the caller supply a parameter + /// value for all parameters that are logically understood by the component. + /// Task SetParametersAsync(ParameterView parameters); } } diff --git a/src/Components/Components/src/IEventCallback.cs b/src/Components/Components/src/IEventCallback.cs new file mode 100644 index 0000000000..6c9fcac7a3 --- /dev/null +++ b/src/Components/Components/src/IEventCallback.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Components +{ + // Used to understand boxed generic EventCallbacks + internal interface IEventCallback + { + bool HasDelegate { get; } + + object UnpackForRenderTree(); + } +} diff --git a/src/Components/Components/src/LayoutView.cs b/src/Components/Components/src/LayoutView.cs new file mode 100644 index 0000000000..7b88d181f0 --- /dev/null +++ b/src/Components/Components/src/LayoutView.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Displays the specified content inside the specified layout and any further + /// nested layouts. + /// + public class LayoutView : IComponent + { + private static readonly RenderFragment EmptyRenderFragment = builder => { }; + + private RenderHandle _renderHandle; + + /// + /// Gets or sets the content to display. + /// + [Parameter] + public RenderFragment ChildContent { get; set; } + + /// + /// Gets or sets the type of the layout in which to display the content. + /// The type must implement and accept a parameter named . + /// + [Parameter] + public Type Layout { get; set; } + + /// + public void Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + /// + public Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + Render(); + return Task.CompletedTask; + } + + private void Render() + { + // In the middle goes the supplied content + var fragment = ChildContent ?? EmptyRenderFragment; + + // Then repeatedly wrap that in each layer of nested layout until we get + // to a layout that has no parent + var layoutType = Layout; + while (layoutType != null) + { + fragment = WrapInLayout(layoutType, fragment); + layoutType = GetParentLayoutType(layoutType); + } + + _renderHandle.Render(fragment); + } + + private static RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam) + { + return builder => + { + builder.OpenComponent(0, layoutType); + builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam); + builder.CloseComponent(); + }; + } + + private static Type GetParentLayoutType(Type type) + => type.GetCustomAttribute()?.LayoutType; + } +} diff --git a/src/Components/Components/src/PageDisplay.cs b/src/Components/Components/src/PageDisplay.cs deleted file mode 100644 index 14df824d13..0000000000 --- a/src/Components/Components/src/PageDisplay.cs +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Components.Auth; -using Microsoft.AspNetCore.Components.RenderTree; - -namespace Microsoft.AspNetCore.Components -{ - /// - /// Displays the specified page component, rendering it inside its layout - /// and any further nested layouts, plus applying any authorization rules. - /// - public class PageDisplay : IComponent - { - private RenderHandle _renderHandle; - - /// - /// Gets or sets the type of the page component to display. - /// The type must implement . - /// - [Parameter] - public Type Page { get; set; } - - /// - /// Gets or sets the parameters to pass to the page. - /// - [Parameter] - public IDictionary PageParameters { get; set; } - - /// - /// The content that will be displayed if the user is not authorized. - /// - [Parameter] - public RenderFragment NotAuthorized { get; set; } - - /// - /// The content that will be displayed while asynchronous authorization is in progress. - /// - [Parameter] - public RenderFragment Authorizing { get; set; } - - /// - public void Attach(RenderHandle renderHandle) - { - _renderHandle = renderHandle; - } - - /// - public Task SetParametersAsync(ParameterView parameters) - { - parameters.SetParameterProperties(this); - Render(); - return Task.CompletedTask; - } - - private void Render() - { - // In the middle goes the requested page - var fragment = (RenderFragment)RenderPageWithParameters; - - // Around that goes an AuthorizeViewCore - fragment = WrapInAuthorizeViewCore(fragment); - - // Then repeatedly wrap that in each layer of nested layout until we get - // to a layout that has no parent - Type layoutType = Page; - while ((layoutType = GetLayoutType(layoutType)) != null) - { - fragment = WrapInLayout(layoutType, fragment); - } - - _renderHandle.Render(fragment); - } - - private RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam) => builder => - { - builder.OpenComponent(0, layoutType); - builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam); - builder.CloseComponent(); - }; - - private void RenderPageWithParameters(RenderTreeBuilder builder) - { - builder.OpenComponent(0, Page); - - if (PageParameters != null) - { - foreach (var kvp in PageParameters) - { - builder.AddAttribute(1, kvp.Key, kvp.Value); - } - } - - builder.CloseComponent(); - } - - private RenderFragment WrapInAuthorizeViewCore(RenderFragment pageFragment) - { - var authorizeData = AttributeAuthorizeDataCache.GetAuthorizeDataForType(Page); - if (authorizeData == null) - { - // No authorization, so no need to wrap the fragment - return pageFragment; - } - - // Some authorization data exists, so we do need to wrap the fragment - RenderFragment authorized = context => pageFragment; - return builder => - { - builder.OpenComponent(0); - builder.AddAttribute(1, nameof(AuthorizeViewWithSuppliedData.AuthorizeDataParam), authorizeData); - builder.AddAttribute(2, nameof(AuthorizeViewWithSuppliedData.Authorized), authorized); - builder.AddAttribute(3, nameof(AuthorizeViewWithSuppliedData.NotAuthorized), NotAuthorized ?? DefaultNotAuthorized); - builder.AddAttribute(4, nameof(AuthorizeViewWithSuppliedData.Authorizing), Authorizing); - builder.CloseComponent(); - }; - } - - private static Type GetLayoutType(Type type) - => type.GetCustomAttribute()?.LayoutType; - - private class AuthorizeViewWithSuppliedData : AuthorizeViewCore - { - [Parameter] public IAuthorizeData[] AuthorizeDataParam { get; private set; } - - protected override IAuthorizeData[] GetAuthorizeData() => AuthorizeDataParam; - } - - // There has to be some default content. If we render blank by default, developers - // will find it hard to guess why their UI isn't appearing. - private static RenderFragment DefaultNotAuthorized(AuthenticationState authenticationState) - => builder => builder.AddContent(0, "Not authorized"); - } -} diff --git a/src/Components/Components/src/ParameterAttribute.cs b/src/Components/Components/src/ParameterAttribute.cs index 25a0bed565..8001f1cdb3 100644 --- a/src/Components/Components/src/ParameterAttribute.cs +++ b/src/Components/Components/src/ParameterAttribute.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components { diff --git a/src/Components/Components/src/ParameterView.cs b/src/Components/Components/src/ParameterView.cs index 3d682cc09c..fb7329aab2 100644 --- a/src/Components/Components/src/ParameterView.cs +++ b/src/Components/Components/src/ParameterView.cs @@ -53,17 +53,17 @@ namespace Microsoft.AspNetCore.Components /// /// Gets the value of the parameter with the specified name. /// - /// The type of the value. + /// The type of the value. /// The name of the parameter. /// Receives the result, if any. /// True if a matching parameter was found; false otherwise. - public bool TryGetValue(string parameterName, out T result) + public bool TryGetValue(string parameterName, out TValue result) { foreach (var entry in this) { if (string.Equals(entry.Name, parameterName)) { - result = (T)entry.Value; + result = (TValue)entry.Value; return true; } } @@ -76,22 +76,22 @@ namespace Microsoft.AspNetCore.Components /// Gets the value of the parameter with the specified name, or a default value /// if no such parameter exists in the collection. /// - /// The type of the value. + /// The type of the value. /// The name of the parameter. /// The parameter value if found; otherwise the default value for the specified type. - public T GetValueOrDefault(string parameterName) - => GetValueOrDefault(parameterName, default); + public TValue GetValueOrDefault(string parameterName) + => GetValueOrDefault(parameterName, default); /// /// Gets the value of the parameter with the specified name, or a specified default value /// if no such parameter exists in the collection. /// - /// The type of the value. + /// The type of the value. /// The name of the parameter. /// The default value to return if no such parameter exists in the collection. /// The parameter value if found; otherwise . - public T GetValueOrDefault(string parameterName, T defaultValue) - => TryGetValue(parameterName, out T result) ? result : defaultValue; + public TValue GetValueOrDefault(string parameterName, TValue defaultValue) + => TryGetValue(parameterName, out TValue result) ? result : defaultValue; /// /// Returns a dictionary populated with the contents of the . diff --git a/src/Components/Components/src/Reflection/ComponentProperties.cs b/src/Components/Components/src/Reflection/ComponentProperties.cs index 5caeab20b1..04f2f8d0c1 100644 --- a/src/Components/Components/src/Reflection/ComponentProperties.cs +++ b/src/Components/Components/src/Reflection/ComponentProperties.cs @@ -14,6 +14,9 @@ namespace Microsoft.AspNetCore.Components.Reflection { private const BindingFlags _bindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase; + // Right now it's not possible for a component to define a Parameter and a Cascading Parameter with + // the same name. We don't give you a way to express this in code (would create duplicate properties), + // and we don't have the ability to represent it in our data structures. private readonly static ConcurrentDictionary _cachedWritersByType = new ConcurrentDictionary(); @@ -44,6 +47,24 @@ namespace Microsoft.AspNetCore.Components.Reflection ThrowForUnknownIncomingParameterName(targetType, parameterName); throw null; // Unreachable } + else if (writer.Cascading && !parameter.Cascading) + { + // We don't allow you to set a cascading parameter with a non-cascading value. Put another way: + // cascading parameters are not part of the public API of a component, so it's not reasonable + // for someone to set it directly. + // + // If we find a strong reason for this to work in the future we can reverse our decision since + // this throws today. + ThrowForSettingCascadingParameterWithNonCascadingValue(targetType, parameterName); + throw null; // Unreachable + } + else if (!writer.Cascading && parameter.Cascading) + { + // We're giving a more specific error here because trying to set a non-cascading parameter + // with a cascading value is likely deliberate (but not supported), or is a bug in our code. + ThrowForSettingParameterWithCascadingValue(targetType, parameterName); + throw null; // Unreachable + } SetProperty(target, writer, parameterName, parameter.Value); } @@ -62,7 +83,24 @@ namespace Microsoft.AspNetCore.Components.Reflection } var isUnmatchedValue = !writers.WritersByName.TryGetValue(parameterName, out var writer); - if (isUnmatchedValue) + + if ((isUnmatchedValue && parameter.Cascading) || (writer != null && !writer.Cascading && parameter.Cascading)) + { + // Don't allow an "extra" cascading value to be collected - or don't allow a non-cascading + // parameter to be set with a cascading value. + // + // This is likely a bug in our infrastructure or an attempt to deliberately do something unsupported. + ThrowForSettingParameterWithCascadingValue(targetType, parameterName); + throw null; // Unreachable + + } + else if (isUnmatchedValue || + + // Allow unmatched parameters to collide with the names of cascading parameters. This is + // valid because cascading parameter names are not part of the public API. There's no + // way for the user of a component to know what the names of cascading parameters + // are. + (writer.Cascading && !parameter.Cascading)) { unmatched ??= new Dictionary(StringComparer.OrdinalIgnoreCase); unmatched[parameterName] = parameter.Value; @@ -138,6 +176,20 @@ namespace Microsoft.AspNetCore.Components.Reflection } } + private static void ThrowForSettingCascadingParameterWithNonCascadingValue(Type targetType, string parameterName) + { + throw new InvalidOperationException( + $"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " + + $"but it does not have [{nameof(ParameterAttribute)}] applied."); + } + + private static void ThrowForSettingParameterWithCascadingValue(Type targetType, string parameterName) + { + throw new InvalidOperationException( + $"The property '{parameterName}' on component type '{targetType.FullName}' cannot be set " + + $"using a cascading value."); + } + private static void ThrowForCaptureUnmatchedValuesConflict(Type targetType, string parameterName, Dictionary unmatched) { throw new InvalidOperationException( @@ -179,13 +231,14 @@ namespace Microsoft.AspNetCore.Components.Reflection foreach (var propertyInfo in GetCandidateBindableProperties(targetType)) { var parameterAttribute = propertyInfo.GetCustomAttribute(); - var isParameter = parameterAttribute != null || propertyInfo.IsDefined(typeof(CascadingParameterAttribute)); + var cascadingParameterAttribute = propertyInfo.GetCustomAttribute(); + var isParameter = parameterAttribute != null || cascadingParameterAttribute != null; if (!isParameter) { continue; } - var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo); + var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: cascadingParameterAttribute != null); var propertyName = propertyInfo.Name; if (WritersByName.ContainsKey(propertyName)) @@ -213,7 +266,7 @@ namespace Microsoft.AspNetCore.Components.Reflection ThrowForInvalidCaptureUnmatchedValuesParameterType(targetType, propertyInfo); } - CaptureUnmatchedValuesWriter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo); + CaptureUnmatchedValuesWriter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo, cascading: false); CaptureUnmatchedValuesPropertyName = propertyInfo.Name; } } diff --git a/src/Components/Components/src/Reflection/IPropertySetter.cs b/src/Components/Components/src/Reflection/IPropertySetter.cs index 575b2e669b..d6a60e2395 100644 --- a/src/Components/Components/src/Reflection/IPropertySetter.cs +++ b/src/Components/Components/src/Reflection/IPropertySetter.cs @@ -5,6 +5,8 @@ namespace Microsoft.AspNetCore.Components.Reflection { internal interface IPropertySetter { + bool Cascading { get; } + void SetValue(object target, object value); } } diff --git a/src/Components/Components/src/Reflection/MemberAssignment.cs b/src/Components/Components/src/Reflection/MemberAssignment.cs index 0ab288cedc..b59d7d2ed7 100644 --- a/src/Components/Components/src/Reflection/MemberAssignment.cs +++ b/src/Components/Components/src/Reflection/MemberAssignment.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Components.Reflection } } - public static IPropertySetter CreatePropertySetter(Type targetType, PropertyInfo property) + public static IPropertySetter CreatePropertySetter(Type targetType, PropertyInfo property, bool cascading) { if (property.SetMethod == null) { @@ -37,19 +37,23 @@ namespace Microsoft.AspNetCore.Components.Reflection return (IPropertySetter)Activator.CreateInstance( typeof(PropertySetter<,>).MakeGenericType(targetType, property.PropertyType), - property.SetMethod); + property.SetMethod, + cascading); } class PropertySetter : IPropertySetter { private readonly Action _setterDelegate; - public PropertySetter(MethodInfo setMethod) + public PropertySetter(MethodInfo setMethod, bool cascading) { _setterDelegate = (Action)Delegate.CreateDelegate( typeof(Action), setMethod); + Cascading = cascading; } + public bool Cascading { get; } + public void SetValue(object target, object value) { if (value == null) diff --git a/src/Components/Components/src/RenderFragment.cs b/src/Components/Components/src/RenderFragment.cs index 19959669a4..cf27ad2cc4 100644 --- a/src/Components/Components/src/RenderFragment.cs +++ b/src/Components/Components/src/RenderFragment.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components { @@ -13,10 +13,10 @@ namespace Microsoft.AspNetCore.Components public delegate void RenderFragment(RenderTreeBuilder builder); /// - /// Represents a segment of UI content for an object of type , implemented as + /// Represents a segment of UI content for an object of type , implemented as /// a function that returns a . /// - /// The type of object. + /// The type of object. /// The value used to build the content. - public delegate RenderFragment RenderFragment(T value); + public delegate RenderFragment RenderFragment(TValue value); } diff --git a/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs b/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs index 18317ece48..3377e18163 100644 --- a/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs +++ b/src/Components/Components/src/RenderTree/ArrayBuilderSegment.cs @@ -8,9 +8,12 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Represents a range of elements within an instance of . + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// /// The type of the elements in the array + // + // Represents a range of elements within an instance of . public readonly struct ArrayBuilderSegment : IEnumerable { // The following fields are memory mapped to the WASM client. Do not re-order or use auto-properties. diff --git a/src/Components/Components/src/RenderTree/ArrayRange.cs b/src/Components/Components/src/RenderTree/ArrayRange.cs index ec113dbdb8..b27a8d5310 100644 --- a/src/Components/Components/src/RenderTree/ArrayRange.cs +++ b/src/Components/Components/src/RenderTree/ArrayRange.cs @@ -4,9 +4,12 @@ namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Represents a range of elements in an array that are in use. + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// - /// The array item type. + /// + // + // Represents a range of elements in an array that are in use. public readonly struct ArrayRange { /// diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiff.cs b/src/Components/Components/src/RenderTree/RenderTreeDiff.cs index 9ad92ec31b..da5b3ed3f7 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeDiff.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeDiff.cs @@ -1,13 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; - namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Describes changes to a component's render tree between successive renders. + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// + // + // Describes changes to a component's render tree between successive renders. public readonly struct RenderTreeDiff { /// diff --git a/src/Components/Components/src/RenderTree/RenderTreeEdit.cs b/src/Components/Components/src/RenderTree/RenderTreeEdit.cs index 96f661924b..68437a7471 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeEdit.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeEdit.cs @@ -6,8 +6,11 @@ using System.Runtime.InteropServices; namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Represents a single edit operation on a component's render tree. + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// + // + // Represents a single edit operation on a component's render tree. [StructLayout(LayoutKind.Explicit)] public readonly struct RenderTreeEdit { diff --git a/src/Components/Components/src/RenderTree/RenderTreeEditType.cs b/src/Components/Components/src/RenderTree/RenderTreeEditType.cs index c2f3e4aba6..f508760135 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeEditType.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeEditType.cs @@ -4,8 +4,11 @@ namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Describes the type of a render tree edit operation. + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// + // + //Describes the type of a render tree edit operation. public enum RenderTreeEditType: int { /// diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrame.cs b/src/Components/Components/src/RenderTree/RenderTreeFrame.cs index f3e003080b..39dd7de74a 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeFrame.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeFrame.cs @@ -8,8 +8,11 @@ using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Represents an entry in a tree of user interface (UI) items. + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// + // + // Represents an entry in a tree of user interface (UI) items. [StructLayout(LayoutKind.Explicit, Pack = 4)] public readonly struct RenderTreeFrame { diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs index 61d2558305..339a7b6795 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs @@ -4,8 +4,11 @@ namespace Microsoft.AspNetCore.Components.RenderTree { /// - /// Describes the type of a . + /// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside + /// of the Blazor framework. These types will change in future release. /// + // + // Describes the type of a . public enum RenderTreeFrameType: short { /// diff --git a/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs similarity index 99% rename from src/Components/Components/src/RenderTree/RenderTreeBuilder.cs rename to src/Components/Components/src/Rendering/RenderTreeBuilder.cs index c7e2324d36..6876c97f0d 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeBuilder.cs +++ b/src/Components/Components/src/Rendering/RenderTreeBuilder.cs @@ -5,8 +5,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.RenderTree; -namespace Microsoft.AspNetCore.Components.RenderTree +namespace Microsoft.AspNetCore.Components.Rendering { // IMPORTANT // @@ -115,7 +116,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree /// An integer that represents the position of the instruction in the source code. /// Content to append. /// The value used by . - public void AddContent(int sequence, RenderFragment fragment, T value) + public void AddContent(int sequence, RenderFragment fragment, TValue value) { if (fragment != null) { @@ -280,7 +281,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree /// This method is provided for infrastructure purposes, and is used to support generated code /// that uses . /// - public void AddAttribute(int sequence, string name, EventCallback value) + public void AddAttribute(int sequence, string name, EventCallback value) { AssertCanAddAttribute(); if (_lastNonAttributeFrameType == RenderTreeFrameType.Component) diff --git a/src/Components/Components/src/Rendering/Renderer.Log.cs b/src/Components/Components/src/Rendering/Renderer.Log.cs index 3b70f58973..bd65809632 100644 --- a/src/Components/Components/src/Rendering/Renderer.Log.cs +++ b/src/Components/Components/src/Rendering/Renderer.Log.cs @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Components.Rendering } } - internal static void DisposingComponent(ILogger logger, ComponentState componentState) + public static void DisposingComponent(ILogger logger, ComponentState componentState) { if (logger.IsEnabled(LogLevel.Debug)) // This is almost always false, so skip the evaluations { @@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Components.Rendering } } - internal static void HandlingEvent(ILogger logger, ulong eventHandlerId, EventArgs eventArgs) + public static void HandlingEvent(ILogger logger, ulong eventHandlerId, EventArgs eventArgs) { _handlingEvent(logger, eventHandlerId, eventArgs?.GetType().Name ?? "null", null); } diff --git a/src/Components/Components/src/Rendering/Renderer.cs b/src/Components/Components/src/Rendering/Renderer.cs index 6e0ed1633e..ed3d075f0a 100644 --- a/src/Components/Components/src/Rendering/Renderer.cs +++ b/src/Components/Components/src/Rendering/Renderer.cs @@ -243,7 +243,7 @@ namespace Microsoft.AspNetCore.Components.Rendering // Since the task has yielded - process any queued rendering work before we return control // to the caller. - ProcessRenderQueue(); + ProcessPendingRender(); } // Task completed synchronously or is still running. We already processed all of the rendering @@ -334,7 +334,7 @@ namespace Microsoft.AspNetCore.Components.Rendering /// /// The ID of the component to render. /// A that will supply the updated UI contents. - protected internal virtual void AddToRenderQueue(int componentId, RenderFragment renderFragment) + internal void AddToRenderQueue(int componentId, RenderFragment renderFragment) { EnsureSynchronizationContext(); @@ -351,7 +351,7 @@ namespace Microsoft.AspNetCore.Components.Rendering if (!_isBatchInProgress) { - ProcessRenderQueue(); + ProcessPendingRender(); } } @@ -398,13 +398,33 @@ namespace Microsoft.AspNetCore.Components.Rendering ? componentState : null; + /// + /// Processses pending renders requests from components if there are any. + /// + protected virtual void ProcessPendingRender() + { + ProcessRenderQueue(); + } + private void ProcessRenderQueue() { + EnsureSynchronizationContext(); + + if (_isBatchInProgress) + { + throw new InvalidOperationException("Cannot start a batch when one is already in progress."); + } + _isBatchInProgress = true; var updateDisplayTask = Task.CompletedTask; try { + if (_batchBuilder.ComponentRenderQueue.Count == 0) + { + return; + } + // Process render queue until empty while (_batchBuilder.ComponentRenderQueue.Count > 0) { @@ -423,6 +443,7 @@ namespace Microsoft.AspNetCore.Components.Rendering { // Ensure we catch errors while running the render functions of the components. HandleException(e); + return; } finally { diff --git a/src/Components/Components/src/RouteView.cs b/src/Components/Components/src/RouteView.cs new file mode 100644 index 0000000000..6f77169e06 --- /dev/null +++ b/src/Components/Components/src/RouteView.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Rendering; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Displays the specified page component, rendering it inside its layout + /// and any further nested layouts. + /// + public class RouteView : IComponent + { + private readonly RenderFragment _renderDelegate; + private readonly RenderFragment _renderPageWithParametersDelegate; + private RenderHandle _renderHandle; + + /// + /// Gets or sets the route data. This determines the page that will be + /// displayed and the parameter values that will be supplied to the page. + /// + [Parameter] + public RouteData RouteData { get; set; } + + /// + /// Gets or sets the type of a layout to be used if the page does not + /// declare any layout. If specified, the type must implement + /// and accept a parameter named . + /// + [Parameter] + public Type DefaultLayout { get; set; } + + public RouteView() + { + // Cache the delegate instances + _renderDelegate = Render; + _renderPageWithParametersDelegate = RenderPageWithParameters; + } + + /// + public void Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + /// + public Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + + if (RouteData == null) + { + throw new InvalidOperationException($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteData)}."); + } + + _renderHandle.Render(_renderDelegate); + return Task.CompletedTask; + } + + /// + /// Renders the component. + /// + /// The . + protected virtual void Render(RenderTreeBuilder builder) + { + var pageLayoutType = RouteData.PageType.GetCustomAttribute()?.LayoutType + ?? DefaultLayout; + + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(LayoutView.Layout), pageLayoutType); + builder.AddAttribute(2, nameof(LayoutView.ChildContent), _renderPageWithParametersDelegate); + builder.CloseComponent(); + } + + private void RenderPageWithParameters(RenderTreeBuilder builder) + { + builder.OpenComponent(0, RouteData.PageType); + + foreach (var kvp in RouteData.RouteValues) + { + builder.AddAttribute(1, kvp.Key, kvp.Value); + } + + builder.CloseComponent(); + } + } +} diff --git a/src/Components/Components/src/Routing/RouteContext.cs b/src/Components/Components/src/Routing/RouteContext.cs index 7de5f3c615..7061e9be41 100644 --- a/src/Components/Components/src/Routing/RouteContext.cs +++ b/src/Components/Components/src/Routing/RouteContext.cs @@ -26,6 +26,6 @@ namespace Microsoft.AspNetCore.Components.Routing public Type Handler { get; set; } - public IDictionary Parameters { get; set; } + public IReadOnlyDictionary Parameters { get; set; } } } diff --git a/src/Components/Components/src/Routing/RouteData.cs b/src/Components/Components/src/Routing/RouteData.cs new file mode 100644 index 0000000000..e0da00f0c7 --- /dev/null +++ b/src/Components/Components/src/Routing/RouteData.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Describes information determined during routing that specifies + /// the page to be displayed. + /// + public sealed class RouteData + { + /// + /// Constructs an instance of . + /// + /// The type of the page matching the route, which must implement . + /// The route parameter values extracted from the matched route. + public RouteData(Type pageType, IReadOnlyDictionary routeValues) + { + if (pageType == null) + { + throw new ArgumentNullException(nameof(pageType)); + } + + if (!typeof(IComponent).IsAssignableFrom(pageType)) + { + throw new ArgumentException($"The value must implement {nameof(IComponent)}.", nameof(pageType)); + } + + PageType = pageType; + RouteValues = routeValues ?? throw new ArgumentNullException(nameof(routeValues)); + } + + /// + /// Gets the type of the page matching the route. + /// + public Type PageType { get; } + + /// + /// Gets route parameter values extracted from the matched route. + /// + public IReadOnlyDictionary RouteValues { get; } + } +} diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 96eca17322..42161ff828 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -3,20 +3,22 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Reflection; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Routing { /// - /// A component that displays whichever other component corresponds to the - /// current navigation location. + /// A component that supplies route data corresponding to the current navigation state. /// public class Router : IComponent, IHandleAfterRender, IDisposable { static readonly char[] _queryOrHashStartChar = new[] { '?', '#' }; + static readonly ReadOnlyDictionary _emptyParametersDictionary + = new ReadOnlyDictionary(new Dictionary()); RenderHandle _renderHandle; string _baseUri; @@ -33,25 +35,19 @@ namespace Microsoft.AspNetCore.Components.Routing [Inject] private ILoggerFactory LoggerFactory { get; set; } /// - /// Gets or sets the assembly that should be searched, along with its referenced - /// assemblies, for components matching the URI. + /// Gets or sets the assembly that should be searched for components matching the URI. /// [Parameter] public Assembly AppAssembly { get; set; } /// - /// Gets or sets the type of the component that should be used as a fallback when no match is found for the requested route. + /// Gets or sets the content to display when no match is found for the requested route. /// [Parameter] public RenderFragment NotFound { get; set; } /// - /// The content that will be displayed if the user is not authorized. + /// Gets or sets the content to display when a match is found for the requested route. /// - [Parameter] public RenderFragment NotAuthorized { get; set; } - - /// - /// The content that will be displayed while asynchronous authorization is in progress. - /// - [Parameter] public RenderFragment Authorizing { get; set; } + [Parameter] public RenderFragment Found { get; set; } private RouteTable Routes { get; set; } @@ -69,6 +65,22 @@ namespace Microsoft.AspNetCore.Components.Routing public Task SetParametersAsync(ParameterView parameters) { parameters.SetParameterProperties(this); + + // Found content is mandatory, because even though we could use something like as a + // reasonable default, if it's not declared explicitly in the template then people will have no way + // to discover how to customize this (e.g., to add authorization). + if (Found == null) + { + throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(Found)}."); + } + + // NotFound content is mandatory, because even though we could display a default message like "Not found", + // it has to be specified explicitly so that it can also be wrapped in a specific layout + if (NotFound == null) + { + throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}."); + } + Routes = RouteTableFactory.Create(AppAssembly); Refresh(isNavigationIntercepted: false); return Task.CompletedTask; @@ -80,7 +92,7 @@ namespace Microsoft.AspNetCore.Components.Routing NavigationManager.LocationChanged -= OnLocationChanged; } - private string StringUntilAny(string str, char[] chars) + private static string StringUntilAny(string str, char[] chars) { var firstIndex = str.IndexOfAny(chars); return firstIndex < 0 @@ -88,17 +100,6 @@ namespace Microsoft.AspNetCore.Components.Routing : str.Substring(0, firstIndex); } - /// - protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary parameters) - { - builder.OpenComponent(0, typeof(PageDisplay)); - builder.AddAttribute(1, nameof(PageDisplay.Page), handler); - builder.AddAttribute(2, nameof(PageDisplay.PageParameters), parameters); - builder.AddAttribute(3, nameof(PageDisplay.NotAuthorized), NotAuthorized); - builder.AddAttribute(4, nameof(PageDisplay.Authorizing), Authorizing); - builder.CloseComponent(); - } - private void Refresh(bool isNavigationIntercepted) { var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute); @@ -116,16 +117,19 @@ namespace Microsoft.AspNetCore.Components.Routing Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri); - _renderHandle.Render(builder => Render(builder, context.Handler, context.Parameters)); + var routeData = new RouteData( + context.Handler, + context.Parameters ?? _emptyParametersDictionary); + _renderHandle.Render(Found(routeData)); } else { - if (!isNavigationIntercepted && NotFound != null) + if (!isNavigationIntercepted) { Log.DisplayingNotFound(_logger, locationPath, _baseUri); // We did not find a Component that matches the route. - // Only show the NotFound if the application developer programatically got us here i.e we did not + // Only show the NotFound content if the application developer programatically got us here i.e we did not // intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content. _renderHandle.Render(NotFound); } diff --git a/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs b/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs new file mode 100644 index 0000000000..792132e0d0 --- /dev/null +++ b/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs @@ -0,0 +1,356 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Components +{ + public class AuthorizeRouteViewTest + { + private readonly static IReadOnlyDictionary EmptyParametersDictionary = new Dictionary(); + private readonly TestAuthenticationStateProvider _authenticationStateProvider; + private readonly TestRenderer _renderer; + private readonly RouteView _authorizeRouteViewComponent; + private readonly int _authorizeRouteViewComponentId; + private readonly TestAuthorizationService _testAuthorizationService; + + public AuthorizeRouteViewTest() + { + _authenticationStateProvider = new TestAuthenticationStateProvider(); + _authenticationStateProvider.CurrentAuthStateTask = Task.FromResult( + new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); + + _testAuthorizationService = new TestAuthorizationService(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(_authenticationStateProvider); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(_testAuthorizationService); + + _renderer = new TestRenderer(serviceCollection.BuildServiceProvider()); + _authorizeRouteViewComponent = new AuthorizeRouteView(); + _authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent); + } + + [Fact] + public void WhenAuthorized_RendersPageInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), new Dictionary + { + { nameof(TestPageRequiringAuthorization.Message), "Hello, world!" } + }); + _testAuthorizationService.NextResult = AuthorizationResult.Success(); + + // Act + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + })); + + // Assert: renders layout + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + AssertFrame.Component(batch.ReferenceFrames[edit.ReferenceFrameIndex]); + }, + edit => AssertPrependText(batch, edit, "Layout ends here")); + + // Assert: renders page + var pageDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(pageDiff.Edits, + edit => AssertPrependText(batch, edit, "Hello from the page with message: Hello, world!")); + } + + [Fact] + public void WhenNotAuthorized_RendersDefaultNotAuthorizedContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + _testAuthorizationService.NextResult = AuthorizationResult.Failed(); + + // Act + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + })); + + // Assert: renders layout containing "not authorized" message + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Not authorized"), + edit => AssertPrependText(batch, edit, "Layout ends here")); + } + + [Fact] + public void WhenNotAuthorized_RendersCustomNotAuthorizedContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + _testAuthorizationService.NextResult = AuthorizationResult.Failed(); + _authenticationStateProvider.CurrentAuthStateTask = Task.FromResult(new AuthenticationState( + new ClaimsPrincipal(new TestIdentity { Name = "Bert" }))); + + // Act + RenderFragment customNotAuthorized = + state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}"); + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + { nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized }, + })); + + // Assert: renders layout containing "not authorized" message + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Go away, Bert"), + edit => AssertPrependText(batch, edit, "Layout ends here")); + } + + [Fact] + public async Task WhenAuthorizing_RendersDefaultAuthorizingContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + var authStateTcs = new TaskCompletionSource(); + _authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task; + RenderFragment customNotAuthorized = + state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}"); + + // Act + var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + { nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized }, + })); + + // Assert: renders layout containing "authorizing" message + Assert.False(firstRenderTask.IsCompleted); + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Authorizing..."), + edit => AssertPrependText(batch, edit, "Layout ends here")); + + // Act 2: updates when authorization completes + authStateTcs.SetResult(new AuthenticationState( + new ClaimsPrincipal(new TestIdentity { Name = "Bert" }))); + await firstRenderTask; + + // Assert 2: Only the layout is updated + batch = _renderer.Batches.Skip(1).Single(); + var nonEmptyDiff = batch.DiffsInOrder.Where(d => d.Edits.Any()).Single(); + Assert.Equal(layoutDiff.ComponentId, nonEmptyDiff.ComponentId); + Assert.Collection(nonEmptyDiff.Edits, edit => + { + Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text(batch.ReferenceFrames[edit.ReferenceFrameIndex], "Go away, Bert"); + }); + } + + [Fact] + public void WhenAuthorizing_RendersCustomAuthorizingContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + var authStateTcs = new TaskCompletionSource(); + _authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task; + RenderFragment customAuthorizing = + builder => builder.AddContent(0, "Hold on, we're checking your papers."); + + // Act + var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + { nameof(AuthorizeRouteView.Authorizing), customAuthorizing }, + })); + + // Assert: renders layout containing "authorizing" message + Assert.False(firstRenderTask.IsCompleted); + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Hold on, we're checking your papers."), + edit => AssertPrependText(batch, edit, "Layout ends here")); + } + + [Fact] + public void WithoutCascadedAuthenticationState_WrapsOutputInCascadingAuthenticationState() + { + // Arrange/Act + var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary); + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData } + })); + + // Assert + var batch = _renderer.Batches.Single(); + var componentInstances = batch.ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Component) + .Select(f => f.Component); + + Assert.Collection(componentInstances, + // This is the hierarchy inside the AuthorizeRouteView, which contains its + // own CascadingAuthenticationState + component => Assert.IsType(component), + component => Assert.IsType>>(component), + component => Assert.IsAssignableFrom(component), + component => Assert.IsType(component), + component => Assert.IsType(component)); + } + + [Fact] + public void WithCascadedAuthenticationState_DoesNotWrapOutputInCascadingAuthenticationState() + { + // Arrange + var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary); + var rootComponent = new AuthorizeRouteViewWithExistingCascadedAuthenticationState( + _authenticationStateProvider.CurrentAuthStateTask, + routeData); + var rootComponentId = _renderer.AssignRootComponentId(rootComponent); + + // Act + _renderer.RenderRootComponent(rootComponentId); + + // Assert + var batch = _renderer.Batches.Single(); + var componentInstances = batch.ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Component) + .Select(f => f.Component); + + Assert.Collection(componentInstances, + // This is the externally-supplied cascading value + component => Assert.IsType>>(component), + component => Assert.IsType(component), + + // This is the hierarchy inside the AuthorizeRouteView. It doesn't contain a + // further CascadingAuthenticationState + component => Assert.IsAssignableFrom(component), + component => Assert.IsType(component), + component => Assert.IsType(component)); + } + + [Fact] + public void UpdatesOutputWhenRouteDataChanges() + { + // Arrange/Act 1: Start on some route + // Not asserting about the initial output, as that is covered by other tests + var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary); + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + })); + + // Act 2: Move to another route + var routeData2 = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + var render2Task = _renderer.Dispatcher.InvokeAsync(() => _authorizeRouteViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData2 }, + }))); + + // Assert: we retain the layout instance, and mutate its contents + Assert.True(render2Task.IsCompletedSuccessfully); + Assert.Equal(2, _renderer.Batches.Count); + var batch2 = _renderer.Batches[1]; + var diff = batch2.DiffsInOrder.Where(d => d.Edits.Any()).Single(); + Assert.Collection(diff.Edits, + edit => + { + // Inside the layout, we add the new content + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text(batch2.ReferenceFrames[edit.ReferenceFrameIndex], "Not authorized"); + }, + edit => + { + // ... and remove the old content + Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); + Assert.Equal(2, edit.SiblingIndex); + }); + } + + private static void AssertPrependText(CapturedBatch batch, RenderTreeEdit edit, string text) + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + ref var referenceFrame = ref batch.ReferenceFrames[edit.ReferenceFrameIndex]; + AssertFrame.Text(referenceFrame, text); + } + + class TestPageWithNoAuthorization : ComponentBase { } + + [Authorize] + class TestPageRequiringAuthorization : ComponentBase + { + [Parameter] public string Message { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"Hello from the page with message: {Message}"); + } + } + + class TestLayout : LayoutComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "Layout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "Layout ends here"); + } + } + + class AuthorizeRouteViewWithExistingCascadedAuthenticationState : AutoRenderComponent + { + private readonly Task _authenticationState; + private readonly RouteData _routeData; + + public AuthorizeRouteViewWithExistingCascadedAuthenticationState( + Task authenticationState, + RouteData routeData) + { + _authenticationState = authenticationState; + _routeData = routeData; + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenComponent>>(0); + builder.AddAttribute(1, nameof(CascadingValue.Value), _authenticationState); + builder.AddAttribute(2, nameof(CascadingValue.ChildContent), (RenderFragment)(builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(AuthorizeRouteView.RouteData), _routeData); + builder.CloseComponent(); + })); + builder.CloseComponent(); + } + } + } +} diff --git a/src/Components/Components/test/Auth/AuthorizeViewTest.cs b/src/Components/Components/test/Auth/AuthorizeViewTest.cs index 848e68e815..c331e9bbd5 100644 --- a/src/Components/Components/test/Auth/AuthorizeViewTest.cs +++ b/src/Components/Components/test/Auth/AuthorizeViewTest.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Security.Claims; @@ -11,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.Extensions.DependencyInjection; @@ -331,15 +331,9 @@ namespace Microsoft.AspNetCore.Components Assert.Equal(2, renderer.Batches.Count); var batch2 = renderer.Batches[1]; var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single(); - Assert.Collection(diff2.Edits, - edit => + Assert.Collection(diff2.Edits, edit => { - Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); - Assert.Equal(0, edit.SiblingIndex); - }, - edit => - { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); Assert.Equal(0, edit.SiblingIndex); AssertFrame.Text( batch2.ReferenceFrames[edit.ReferenceFrameIndex], @@ -513,15 +507,6 @@ namespace Microsoft.AspNetCore.Components => Task.FromResult(new AuthenticationState( new ClaimsPrincipal(new TestIdentity { Name = username }))); - class TestIdentity : IIdentity - { - public string AuthenticationType => "Test"; - - public bool IsAuthenticated => true; - - public string Name { get; set; } - } - public TestRenderer CreateTestRenderer(IAuthorizationService authorizationService) { var serviceCollection = new ServiceCollection(); @@ -530,52 +515,6 @@ namespace Microsoft.AspNetCore.Components return new TestRenderer(serviceCollection.BuildServiceProvider()); } - private class TestAuthorizationService : IAuthorizationService - { - public AuthorizationResult NextResult { get; set; } - = AuthorizationResult.Failed(); - - public List<(ClaimsPrincipal user, object resource, IEnumerable requirements)> AuthorizeCalls { get; } - = new List<(ClaimsPrincipal user, object resource, IEnumerable requirements)>(); - - public Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) - { - AuthorizeCalls.Add((user, resource, requirements)); - - // The TestAuthorizationService doesn't actually apply any authorization requirements - // It just returns the specified NextResult, since we're not trying to test the logic - // in DefaultAuthorizationService or similar here. So it's up to tests to set a desired - // NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls. - return Task.FromResult(NextResult); - } - - public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) - => throw new NotImplementedException(); - } - - private class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider - { - private readonly AuthorizationOptions options = new AuthorizationOptions(); - - public Task GetDefaultPolicyAsync() - => Task.FromResult(options.DefaultPolicy); - - public Task GetFallbackPolicyAsync() - => Task.FromResult(options.FallbackPolicy); - - public Task GetPolicyAsync(string policyName) => Task.FromResult( - new AuthorizationPolicy(new[] - { - new TestPolicyRequirement { PolicyName = policyName } - }, - new[] { $"TestScheme:{policyName}" })); - } - - public class TestPolicyRequirement : IAuthorizationRequirement - { - public string PolicyName { get; set; } - } - public class AuthorizeViewCoreWithScheme : AuthorizeViewCore { protected override IAuthorizeData[] GetAuthorizeData() diff --git a/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs b/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs index 21a269932c..ec347b01c2 100644 --- a/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs +++ b/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.Extensions.DependencyInjection; @@ -37,7 +38,7 @@ namespace Microsoft.AspNetCore.Components { // Arrange: Service var services = new ServiceCollection(); - var authStateProvider = new TestAuthStateProvider() + var authStateProvider = new TestAuthenticationStateProvider() { CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState("Bert")) }; @@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.Components // Arrange: Service var services = new ServiceCollection(); var authStateTaskCompletionSource = new TaskCompletionSource(); - var authStateProvider = new TestAuthStateProvider() + var authStateProvider = new TestAuthenticationStateProvider() { CurrentAuthStateTask = authStateTaskCompletionSource.Task }; @@ -122,7 +123,7 @@ namespace Microsoft.AspNetCore.Components { // Arrange: Service var services = new ServiceCollection(); - var authStateProvider = new TestAuthStateProvider() + var authStateProvider = new TestAuthenticationStateProvider() { CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState(null)) }; @@ -189,21 +190,6 @@ namespace Microsoft.AspNetCore.Components } } - class TestAuthStateProvider : AuthenticationStateProvider - { - public Task CurrentAuthStateTask { get; set; } - - public override Task GetAuthenticationStateAsync() - { - return CurrentAuthStateTask; - } - - internal void TriggerAuthenticationStateChanged(Task authState) - { - NotifyAuthenticationStateChanged(authState); - } - } - public static AuthenticationState CreateAuthenticationState(string username) => new AuthenticationState(new ClaimsPrincipal(username == null ? new ClaimsIdentity() diff --git a/src/Components/Components/test/Auth/TestAuthenticationStateProvider.cs b/src/Components/Components/test/Auth/TestAuthenticationStateProvider.cs new file mode 100644 index 0000000000..6a84916e93 --- /dev/null +++ b/src/Components/Components/test/Auth/TestAuthenticationStateProvider.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + public class TestAuthenticationStateProvider : AuthenticationStateProvider + { + public Task CurrentAuthStateTask { get; set; } + + public override Task GetAuthenticationStateAsync() + { + return CurrentAuthStateTask; + } + + internal void TriggerAuthenticationStateChanged(Task authState) + { + NotifyAuthenticationStateChanged(authState); + } + } +} diff --git a/src/Components/Components/test/Auth/TestAuthorizationPolicyProvider.cs b/src/Components/Components/test/Auth/TestAuthorizationPolicyProvider.cs new file mode 100644 index 0000000000..b3e903fdb3 --- /dev/null +++ b/src/Components/Components/test/Auth/TestAuthorizationPolicyProvider.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace Microsoft.AspNetCore.Components +{ + public class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider + { + private readonly AuthorizationOptions options = new AuthorizationOptions(); + + public Task GetDefaultPolicyAsync() + => Task.FromResult(options.DefaultPolicy); + + public Task GetFallbackPolicyAsync() + => Task.FromResult(options.FallbackPolicy); + + public Task GetPolicyAsync(string policyName) => Task.FromResult( + new AuthorizationPolicy(new[] + { + new TestPolicyRequirement { PolicyName = policyName } + }, + new[] { $"TestScheme:{policyName}" })); + } + + public class TestPolicyRequirement : IAuthorizationRequirement + { + public string PolicyName { get; set; } + } +} diff --git a/src/Components/Components/test/Auth/TestAuthorizationService.cs b/src/Components/Components/test/Auth/TestAuthorizationService.cs new file mode 100644 index 0000000000..d6cc1ff11a --- /dev/null +++ b/src/Components/Components/test/Auth/TestAuthorizationService.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace Microsoft.AspNetCore.Components +{ + public class TestAuthorizationService : IAuthorizationService + { + public AuthorizationResult NextResult { get; set; } + = AuthorizationResult.Failed(); + + public List<(ClaimsPrincipal user, object resource, IEnumerable requirements)> AuthorizeCalls { get; } + = new List<(ClaimsPrincipal user, object resource, IEnumerable requirements)>(); + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) + { + AuthorizeCalls.Add((user, resource, requirements)); + + // The TestAuthorizationService doesn't actually apply any authorization requirements + // It just returns the specified NextResult, since we're not trying to test the logic + // in DefaultAuthorizationService or similar here. So it's up to tests to set a desired + // NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls. + return Task.FromResult(NextResult); + } + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) + => throw new NotImplementedException(); + } +} diff --git a/src/Components/Components/test/Auth/TestIdentity.cs b/src/Components/Components/test/Auth/TestIdentity.cs new file mode 100644 index 0000000000..d650c53fe6 --- /dev/null +++ b/src/Components/Components/test/Auth/TestIdentity.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Principal; + +namespace Microsoft.AspNetCore.Components +{ + public class TestIdentity : IIdentity + { + public string AuthenticationType => "Test"; + + public bool IsAuthenticated => true; + + public string Name { get; set; } + } +} diff --git a/src/Components/Components/test/CascadingParameterTest.cs b/src/Components/Components/test/CascadingParameterTest.cs index 1e05b98f68..aba1199d9b 100644 --- a/src/Components/Components/test/CascadingParameterTest.cs +++ b/src/Components/Components/test/CascadingParameterTest.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; using Xunit; diff --git a/src/Components/Components/test/ComponentBaseTest.cs b/src/Components/Components/test/ComponentBaseTest.cs index 6f9a32b07e..1f9ab22826 100644 --- a/src/Components/Components/test/ComponentBaseTest.cs +++ b/src/Components/Components/test/ComponentBaseTest.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; using Xunit; diff --git a/src/Components/Components/test/LayoutViewTest.cs b/src/Components/Components/test/LayoutViewTest.cs new file mode 100644 index 0000000000..592cb7e62d --- /dev/null +++ b/src/Components/Components/test/LayoutViewTest.cs @@ -0,0 +1,326 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Test +{ + public class LayoutViewTest + { + private readonly TestRenderer _renderer; + private readonly LayoutView _layoutViewComponent; + private readonly int _layoutViewComponentId; + + public LayoutViewTest() + { + _renderer = new TestRenderer(); + _layoutViewComponent = new LayoutView(); + _layoutViewComponentId = _renderer.AssignRootComponentId(_layoutViewComponent); + } + + [Fact] + public void GivenNoParameters_RendersNothing() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.Empty)); + Assert.True(setParametersTask.IsCompletedSuccessfully); + var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + + // Assert + Assert.Single(_renderer.Batches); + Assert.Empty(frames); + } + + [Fact] + public void GivenContentButNoLayout_RendersContent() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(123, "Hello"); + builder.AddContent(456, "Goodbye"); + })} + }))); + Assert.True(setParametersTask.IsCompletedSuccessfully); + var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + + // Assert + Assert.Single(_renderer.Batches); + Assert.Collection(frames, + frame => AssertFrame.Text(frame, "Hello", 123), + frame => AssertFrame.Text(frame, "Goodbye", 456)); + } + + [Fact] + public void GivenLayoutButNoContent_RendersLayoutWithEmptyBody() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(RootLayout) } + }))); + + // Assert + Assert.True(setParametersTask.IsCompletedSuccessfully); + var batch = _renderer.Batches.Single(); + + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + var rootLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable(); + Assert.Collection(rootLayoutFrames, + frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 1), // i.e., empty region + frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2)); + } + + [Fact] + public void RendersContentInsideLayout() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(RootLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(123, "Hello"); + builder.AddContent(456, "Goodbye"); + })} + }))); + + // Assert + Assert.True(setParametersTask.IsCompletedSuccessfully); + var batch = _renderer.Batches.Single(); + + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + var rootLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable(); + Assert.Collection(rootLayoutFrames, + frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3), + frame => AssertFrame.Text(frame, "Hello", sequence: 123), + frame => AssertFrame.Text(frame, "Goodbye", sequence: 456), + frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2)); + } + + [Fact] + public void RendersContentInsideNestedLayout() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(123, "Hello"); + builder.AddContent(456, "Goodbye"); + })} + }))); + + // Assert + Assert.True(setParametersTask.IsCompletedSuccessfully); + var batch = _renderer.Batches.Single(); + + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + var rootLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable(); + Assert.Collection(rootLayoutFrames, + frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1), + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1), + frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2)); + + var nestedLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var nestedLayoutFrames = _renderer.GetCurrentRenderTreeFrames(nestedLayoutComponentId).AsEnumerable(); + Assert.Collection(nestedLayoutFrames, + frame => AssertFrame.Text(frame, "NestedLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1), + frame => AssertFrame.Text(frame, "Hello", sequence: 123), + frame => AssertFrame.Text(frame, "Goodbye", sequence: 456), + frame => AssertFrame.Text(frame, "NestedLayout ends here", sequence: 2)); + } + + [Fact] + public void CanChangeContentWithSameLayout() + { + // Arrange + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(0, "Initial content"); + })} + }))); + + // Act + Assert.True(setParametersTask.IsCompletedSuccessfully); + _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(0, "Changed content"); + })} + }))); + + // Assert + Assert.Equal(2, _renderer.Batches.Count); + var batch = _renderer.Batches[1]; + Assert.Equal(0, batch.DisposedComponentIDs.Count); + Assert.Collection(batch.DiffsInOrder, + diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes + diff => Assert.Empty(diff.Edits), // RootLayout rerendered, but with no changes + diff => + { + // NestedLayout rerendered, patching content in place + Assert.Collection(diff.Edits, edit => + { + Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "Changed content", + sequence: 0); + }); + }); + } + + [Fact] + public void CanChangeLayout() + { + // Arrange + var setParametersTask1 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(0, "Some content"); + })} + }))); + Assert.True(setParametersTask1.IsCompletedSuccessfully); + + // Act + var setParametersTask2 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(OtherNestedLayout) }, + }))); + + // Assert + Assert.True(setParametersTask2.IsCompletedSuccessfully); + Assert.Equal(2, _renderer.Batches.Count); + var batch = _renderer.Batches[1]; + Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposes NestedLayout + Assert.Collection(batch.DiffsInOrder, + diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes + diff => + { + // RootLayout rerendered, changing child + Assert.Collection(diff.Edits, + edit => + { + Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + }, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Component( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + sequence: 0); + }); + }, + diff => + { + // Inserts new OtherNestedLayout + Assert.Collection(diff.Edits, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(0, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "OtherNestedLayout starts here"); + }, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "Some content"); + }, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(2, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "OtherNestedLayout ends here"); + }); + }); + } + + private class RootLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + if (Body == null) + { + // Prove that we don't expect layouts to tolerate null values for Body + throw new InvalidOperationException("Got a null body when not expecting it"); + } + + builder.AddContent(0, "RootLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "RootLayout ends here"); + } + } + + [Layout(typeof(RootLayout))] + private class NestedLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "NestedLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "NestedLayout ends here"); + } + } + + [Layout(typeof(RootLayout))] + private class OtherNestedLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "OtherNestedLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "OtherNestedLayout ends here"); + } + } + } +} diff --git a/src/Components/Components/test/PageDisplayTest.cs b/src/Components/Components/test/PageDisplayTest.cs deleted file mode 100644 index 9b28d95d23..0000000000 --- a/src/Components/Components/test/PageDisplayTest.cs +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.AspNetCore.Components.Test.Helpers; -using System.Collections.Generic; -using System.Linq; -using Xunit; - -namespace Microsoft.AspNetCore.Components.Test -{ - public class PageDisplayTest - { - private TestRenderer _renderer = new TestRenderer(); - private PageDisplay _pageDisplayComponent = new PageDisplay(); - private int _pageDisplayComponentId; - - public PageDisplayTest() - { - _renderer = new TestRenderer(); - _pageDisplayComponent = new PageDisplay(); - _pageDisplayComponentId = _renderer.AssignRootComponentId(_pageDisplayComponent); - } - - [Fact] - public void DisplaysComponentInsideLayout() - { - // Arrange/Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithLayout) } - }))); - - // Assert - var batch = _renderer.Batches.Single(); - Assert.Collection(batch.DiffsInOrder, - diff => - { - // First is the LayoutDisplay component, which contains a RootLayout - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Component( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex]); - }, - diff => - { - // ... then a RootLayout which contains a ComponentWithLayout - // First is the LayoutDisplay component, which contains a RootLayout - Assert.Collection(diff.Edits, - edit => - { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Text( - batch.ReferenceFrames[edit.ReferenceFrameIndex], - "RootLayout starts here"); - }, - edit => - { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }, - edit => - { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Text( - batch.ReferenceFrames[edit.ReferenceFrameIndex], - "RootLayout ends here"); - }); - }, - diff => - { - // ... then the ComponentWithLayout - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Text( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex], - $"{nameof(ComponentWithLayout)} is here."); - }); - } - - [Fact] - public void DisplaysComponentInsideNestedLayout() - { - // Arrange/Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) } - }))); - - // Assert - var batch = _renderer.Batches.Single(); - Assert.Collection(batch.DiffsInOrder, - // First, a LayoutDisplay containing a RootLayout - diff => AssertFrame.Component( - batch.ReferenceFrames[diff.Edits[0].ReferenceFrameIndex]), - // Then a RootLayout containing a NestedLayout - diff => AssertFrame.Component( - batch.ReferenceFrames[diff.Edits[1].ReferenceFrameIndex]), - // Then a NestedLayout containing a ComponentWithNestedLayout - diff => AssertFrame.Component( - batch.ReferenceFrames[diff.Edits[1].ReferenceFrameIndex]), - // Then the ComponentWithNestedLayout - diff => AssertFrame.Text( - batch.ReferenceFrames[diff.Edits[0].ReferenceFrameIndex], - $"{nameof(ComponentWithNestedLayout)} is here.")); - } - - [Fact] - public void CanChangeDisplayedPageWithSameLayout() - { - // Arrange - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithLayout) } - }))); - - // Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(DifferentComponentWithLayout) } - }))); - - // Assert - Assert.Equal(2, _renderer.Batches.Count); - var batch = _renderer.Batches[1]; - Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposed only the inner page component - Assert.Collection(batch.DiffsInOrder, - diff => Assert.Empty(diff.Edits), // LayoutDisplay rerendered, but with no changes - diff => - { - // RootLayout rerendered - Assert.Collection(diff.Edits, - edit => - { - // Removed old page - Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - }, - edit => - { - // Inserted new one - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }); - }, - diff => - { - // New page rendered - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Text( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex], - $"{nameof(DifferentComponentWithLayout)} is here."); - }); - } - - [Fact] - public void CanChangeDisplayedPageWithDifferentLayout() - { - // Arrange - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithLayout) } - }))); - - // Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) } - }))); - - // Assert - Assert.Equal(2, _renderer.Batches.Count); - var batch = _renderer.Batches[1]; - Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposed only the inner page component - Assert.Collection(batch.DiffsInOrder, - diff => Assert.Empty(diff.Edits), // LayoutDisplay rerendered, but with no changes - diff => - { - // RootLayout rerendered - Assert.Collection(diff.Edits, - edit => - { - // Removed old page - Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - }, - edit => - { - // Inserted new nested layout - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }); - }, - diff => - { - // New nested layout rendered - var edit = diff.Edits[1]; - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }, - diff => - { - // New inner page rendered - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Text( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex], - $"{nameof(ComponentWithNestedLayout)} is here."); - }); - } - - private class RootLayout : AutoRenderComponent - { - [Parameter] - public RenderFragment Body { get; set; } - - protected override void BuildRenderTree(RenderTreeBuilder builder) - { - builder.AddContent(0, "RootLayout starts here"); - builder.AddContent(1, Body); - builder.AddContent(2, "RootLayout ends here"); - } - } - - [Layout(typeof(RootLayout))] - private class NestedLayout : AutoRenderComponent - { - [Parameter] - public RenderFragment Body { get; set; } - - protected override void BuildRenderTree(RenderTreeBuilder builder) - { - builder.AddContent(0, "NestedLayout starts here"); - builder.AddContent(1, Body); - builder.AddContent(2, "NestedLayout ends here"); - } - } - - [Layout(typeof(RootLayout))] - private class ComponentWithLayout : AutoRenderComponent - { - protected override void BuildRenderTree(RenderTreeBuilder builder) - => builder.AddContent(0, $"{nameof(ComponentWithLayout)} is here."); - } - - [Layout(typeof(RootLayout))] - private class DifferentComponentWithLayout : AutoRenderComponent - { - protected override void BuildRenderTree(RenderTreeBuilder builder) - => builder.AddContent(0, $"{nameof(DifferentComponentWithLayout)} is here."); - } - - [Layout(typeof(NestedLayout))] - private class ComponentWithNestedLayout : AutoRenderComponent - { - protected override void BuildRenderTree(RenderTreeBuilder builder) - => builder.AddContent(0, $"{nameof(ComponentWithNestedLayout)} is here."); - } - } -} diff --git a/src/Components/Components/test/ParameterViewTest.Assignment.cs b/src/Components/Components/test/ParameterViewTest.Assignment.cs index 7c2056683a..bf2f38af5a 100644 --- a/src/Components/Components/test/ParameterViewTest.Assignment.cs +++ b/src/Components/Components/test/ParameterViewTest.Assignment.cs @@ -5,9 +5,8 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.AspNetCore.Components.Test.Helpers; using Xunit; namespace Microsoft.AspNetCore.Components @@ -96,23 +95,58 @@ namespace Microsoft.AspNetCore.Components } [Fact] - public void IncomingParameterMatchesNoDeclaredParameter_Throws() + public void IncomingCascadingValueMatchesCascadingParameter_SetsValue() { // Arrange - var target = new HasPropertyWithoutParameterAttribute(); - var parameters = new ParameterViewBuilder - { - { "AnyOtherKey", 123 }, - }.Build(); + var builder = new ParameterViewBuilder(); + builder.Add(nameof(HasCascadingParameter.Cascading), "hi", cascading: true); + var parameters = builder.Build(); + + var target = new HasCascadingParameter(); // Act - var ex = Assert.Throws( - () => parameters.SetParameterProperties(target)); + parameters.SetParameterProperties(target); + + // Assert + Assert.Equal("hi", target.Cascading); + } + + [Fact] + public void NoIncomingCascadingValueMatchesDeclaredCascadingParameter_LeavesValueUnchanged() + { + // Arrange + var builder = new ParameterViewBuilder(); + var parameters = builder.Build(); + + var target = new HasCascadingParameter() + { + Cascading = "bye", + }; + + // Act + parameters.SetParameterProperties(target); + + // Assert + Assert.Equal("bye", target.Cascading); + } + + [Fact] + public void IncomingCascadingValueMatchesNoDeclaredParameter_Throws() + { + // Arrange + var builder = new ParameterViewBuilder(); + builder.Add("SomethingElse", "hi", cascading: true); + var parameters = builder.Build(); + + var target = new HasCascadingParameter(); + + // Act + var ex = Assert.Throws(() => parameters.SetParameterProperties(target)); // Assert Assert.Equal( - $"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' does not have a property " + - $"matching the name 'AnyOtherKey'.", + $"Object of type '{typeof(HasCascadingParameter).FullName}' does not have a property " + + $"matching the name 'SomethingElse'.", ex.Message); } @@ -138,6 +172,45 @@ namespace Microsoft.AspNetCore.Components ex.Message); } + [Fact] + public void IncomingNonCascadingValueMatchesCascadingParameter_Throws() + { + // Arrange + var target = new HasCascadingParameter(); + var parameters = new ParameterViewBuilder + { + { nameof(HasCascadingParameter.Cascading), 123 }, + }.Build(); + + // Act + var ex = Assert.Throws(() => parameters.SetParameterProperties(target)); + + // Assert + Assert.Equal( + $"Object of type '{typeof(HasCascadingParameter).FullName}' has a property matching the name '{nameof(HasCascadingParameter.Cascading)}', " + + $"but it does not have [{nameof(ParameterAttribute)}] applied.", + ex.Message); + } + + [Fact] + public void IncomingCascadingValueMatchesNonCascadingParameter_Throws() + { + // Arrange + var target = new HasInstanceProperties(); + var builder = new ParameterViewBuilder(); + builder.Add(nameof(HasInstanceProperties.IntProp), 16, cascading: true); + var parameters = builder.Build(); + + // Act + var ex = Assert.Throws(() => parameters.SetParameterProperties(target)); + + // Assert + Assert.Equal( + $"The property '{nameof(HasInstanceProperties.IntProp)}' on component type '{typeof(HasInstanceProperties).FullName}' " + + $"cannot be set using a cascading value.", + ex.Message); + } + [Fact] public void SettingCaptureUnmatchedValuesParameterExplicitlyWorks() { @@ -274,6 +347,51 @@ namespace Microsoft.AspNetCore.Components ex.Message); } + [Fact] + public void IncomingNonCascadingValueMatchesCascadingParameter_WithCaptureUnmatchedValues_DoesNotThrow() + { + // Arrange + var target = new HasCaptureUnmatchedValuesPropertyAndCascadingParameter() + { + Cascading = "bye", + }; + var parameters = new ParameterViewBuilder + { + { nameof(HasCaptureUnmatchedValuesPropertyAndCascadingParameter.Cascading), "hi" }, + }.Build(); + + // Act + parameters.SetParameterProperties(target); + + Assert.Collection( + target.CaptureUnmatchedValues, + kvp => + { + Assert.Equal(nameof(HasCaptureUnmatchedValuesPropertyAndCascadingParameter.Cascading), kvp.Key); + Assert.Equal("hi", kvp.Value); + }); + Assert.Equal("bye", target.Cascading); + } + + [Fact] + public void IncomingCascadingValueMatchesNonCascadingParameter_WithCaptureUnmatchedValues_Throws() + { + // Arrange + var target = new HasCaptureUnmatchedValuesProperty(); + var builder = new ParameterViewBuilder(); + builder.Add(nameof(HasInstanceProperties.IntProp), 16, cascading: true); + var parameters = builder.Build(); + + // Act + var ex = Assert.Throws(() => parameters.SetParameterProperties(target)); + + // Assert + Assert.Equal( + $"The property '{nameof(HasCaptureUnmatchedValuesProperty.IntProp)}' on component type '{typeof(HasCaptureUnmatchedValuesProperty).FullName}' " + + $"cannot be set using a cascading value.", + ex.Message); + } + [Fact] public void IncomingParameterValueMismatchesDeclaredParameterType_Throws() { @@ -396,6 +514,11 @@ namespace Microsoft.AspNetCore.Components } } + class HasCascadingParameter + { + [CascadingParameter] public string Cascading { get; set; } + } + class HasPropertyWithoutParameterAttribute { internal int IntProp { get; set; } @@ -435,6 +558,12 @@ namespace Microsoft.AspNetCore.Components [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary CaptureUnmatchedValues { get; set; } } + class HasCaptureUnmatchedValuesPropertyAndCascadingParameter + { + [CascadingParameter] public string Cascading { get; set; } + [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary CaptureUnmatchedValues { get; set; } + } + class HasDupliateCaptureUnmatchedValuesProperty { [Parameter(CaptureUnmatchedValues = true)] public Dictionary CaptureUnmatchedValuesProp1 { get; set; } @@ -448,11 +577,11 @@ namespace Microsoft.AspNetCore.Components class ParameterViewBuilder : IEnumerable { - private readonly List<(string Name, object Value)> _keyValuePairs - = new List<(string, object)>(); + private readonly List<(string Name, object Value, bool Cascading)> _keyValuePairs + = new List<(string, object, bool)>(); - public void Add(string name, object value) - => _keyValuePairs.Add((name, value)); + public void Add(string name, object value, bool cascading = false) + => _keyValuePairs.Add((name, value, cascading)); public IEnumerator GetEnumerator() => throw new NotImplementedException(); @@ -460,13 +589,56 @@ namespace Microsoft.AspNetCore.Components public ParameterView Build() { var builder = new RenderTreeBuilder(); + builder.OpenComponent(0); foreach (var kvp in _keyValuePairs) { - builder.AddAttribute(1, kvp.Name, kvp.Value); + if (!kvp.Cascading) + { + builder.AddAttribute(1, kvp.Name, kvp.Value); + } } builder.CloseComponent(); - return new ParameterView(builder.GetFrames().Array, ownerIndex: 0); + + var view = new ParameterView(builder.GetFrames().Array, ownerIndex: 0); + + var cascadingParameters = new List(); + foreach (var kvp in _keyValuePairs) + { + if (kvp.Cascading) + { + cascadingParameters.Add(new CascadingParameterState(kvp.Name, new TestCascadingValueProvider(kvp.Value))); + } + } + + return view.WithCascadingParameters(cascadingParameters); + } + } + + private class TestCascadingValueProvider : ICascadingValueComponent + { + public TestCascadingValueProvider(object value) + { + CurrentValue = value; + } + + public object CurrentValue { get; } + + public bool CurrentValueIsFixed => throw new NotImplementedException(); + + public bool CanSupplyValue(Type valueType, string valueName) + { + throw new NotImplementedException(); + } + + public void Subscribe(ComponentState subscriber) + { + throw new NotImplementedException(); + } + + public void Unsubscribe(ComponentState subscriber) + { + throw new NotImplementedException(); } } } diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index f895c47f10..8d3ce23570 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -3395,6 +3395,27 @@ namespace Microsoft.AspNetCore.Components.Test } } + [Fact] + public void CannotStartOverlappingBatches() + { + // Arrange + var renderer = new InvalidRecursiveRenderer(); + var component = new CallbackOnRenderComponent(() => + { + // The renderer disallows one batch to be started inside another, because that + // would violate all kinds of state tracking invariants. It's not something that + // would ever happen except if you subclass the renderer and do something unsupported + // that commences batches from inside each other. + renderer.ProcessPendingRender(); + }); + var componentId = renderer.AssignRootComponentId(component); + + // Act/Assert + var ex = Assert.Throws( + () => renderer.RenderRootComponent(componentId)); + Assert.Contains("Cannot start a batch when one is already in progress.", ex.Message); + } + private class NoOpRenderer : Renderer { public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance) @@ -4109,5 +4130,24 @@ namespace Microsoft.AspNetCore.Components.Test private class DerivedEventArgs : EventArgs { } + + class CallbackOnRenderComponent : AutoRenderComponent + { + private readonly Action _callback; + + public CallbackOnRenderComponent(Action callback) + { + _callback = callback; + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + => _callback(); + } + + class InvalidRecursiveRenderer : TestRenderer + { + public new void ProcessPendingRender() + => base.ProcessPendingRender(); + } } } diff --git a/src/Components/Components/test/RenderTreeBuilderTest.cs b/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs similarity index 99% rename from src/Components/Components/test/RenderTreeBuilderTest.cs rename to src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs index 3e5cc0b969..7dfce8a79c 100644 --- a/src/Components/Components/test/RenderTreeBuilderTest.cs +++ b/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs @@ -5,14 +5,13 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Components.Test +namespace Microsoft.AspNetCore.Components.Rendering { public class RenderTreeBuilderTest { diff --git a/src/Components/Components/test/RouteViewTest.cs b/src/Components/Components/test/RouteViewTest.cs new file mode 100644 index 0000000000..05b1c8b20f --- /dev/null +++ b/src/Components/Components/test/RouteViewTest.cs @@ -0,0 +1,209 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Test +{ + public class RouteViewTest + { + private readonly TestRenderer _renderer; + private readonly RouteView _routeViewComponent; + private readonly int _routeViewComponentId; + + public RouteViewTest() + { + _renderer = new TestRenderer(); + _routeViewComponent = new RouteView(); + _routeViewComponentId = _renderer.AssignRootComponentId(_routeViewComponent); + } + + [Fact] + public void ThrowsIfNoRouteDataSupplied() + { + var ex = Assert.Throws(() => + { + // Throws synchronously, so no need to await + _ = _routeViewComponent.SetParametersAsync(ParameterView.Empty); + }); + + + Assert.Equal($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteView.RouteData)}.", ex.Message); + } + + [Fact] + public void RendersPageInsideLayoutView() + { + // Arrange + var routeParams = new Dictionary + { + { nameof(ComponentWithLayout.Message), "Test message" } + }; + var routeData = new RouteData(typeof(ComponentWithLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + }))); + + // Assert: RouteView renders LayoutView + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(TestLayout), sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + + // Assert: LayoutView renders TestLayout + var layoutViewComponentId = batch.GetComponentFrames().Single().ComponentId; + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + // Assert: TestLayout renders page + var testLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var testLayoutFrames = _renderer.GetCurrentRenderTreeFrames(testLayoutComponentId).AsEnumerable(); + Assert.Collection(testLayoutFrames, + frame => AssertFrame.Text(frame, "Layout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3), + frame => AssertFrame.Component(frame, sequence: 0, subtreeLength: 2), + frame => AssertFrame.Attribute(frame, nameof(ComponentWithLayout.Message), "Test message", sequence: 1), + frame => AssertFrame.Text(frame, "Layout ends here", sequence: 2)); + + // Assert: page itself is rendered, having received parameters from the original route data + var pageComponentId = batch.GetComponentFrames().Single().ComponentId; + var pageFrames = _renderer.GetCurrentRenderTreeFrames(pageComponentId).AsEnumerable(); + Assert.Collection(pageFrames, + frame => AssertFrame.Text(frame, "Hello from the page with message 'Test message'", sequence: 0)); + + // Assert: nothing else was rendered + Assert.Equal(4, batch.DiffsInOrder.Count); + } + + [Fact] + public void UsesDefaultLayoutIfNoneSetOnPage() + { + // Arrange + var routeParams = new Dictionary(); + var routeData = new RouteData(typeof(ComponentWithoutLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + { nameof(RouteView.DefaultLayout), typeof(OtherLayout) }, + }))); + + // Assert: uses default layout + // Not asserting about what else gets rendered as that's covered by other tests + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(OtherLayout), sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + } + + [Fact] + public void UsesNoLayoutIfNoneSetOnPageAndNoDefaultSet() + { + // Arrange + var routeParams = new Dictionary(); + var routeData = new RouteData(typeof(ComponentWithoutLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + }))); + + // Assert: uses no layout + // Not asserting about what else gets rendered as that's covered by other tests + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)null, sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + } + + [Fact] + public void PageLayoutSupersedesDefaultLayout() + { + // Arrange + var routeParams = new Dictionary(); + var routeData = new RouteData(typeof(ComponentWithLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + { nameof(RouteView.DefaultLayout), typeof(OtherLayout) }, + }))); + + // Assert: uses layout specified by page + // Not asserting about what else gets rendered as that's covered by other tests + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(TestLayout), sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + } + + private class ComponentWithoutLayout : AutoRenderComponent + { + [Parameter] public string Message { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"Hello from the page with message '{Message}'"); + } + } + + [Layout(typeof(TestLayout))] + private class ComponentWithLayout : AutoRenderComponent + { + [Parameter] public string Message { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"Hello from the page with message '{Message}'"); + } + } + + private class TestLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "Layout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "Layout ends here"); + } + } + + private class OtherLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "OtherLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "OtherLayout ends here"); + } + } + } +} diff --git a/src/Components/Directory.Build.targets b/src/Components/Directory.Build.targets index bd6e405829..b992960cc3 100644 --- a/src/Components/Directory.Build.targets +++ b/src/Components/Directory.Build.targets @@ -5,6 +5,16 @@ + + + + diff --git a/src/Components/Server/ref/Microsoft.AspNetCore.Components.Server.netcoreapp3.0.cs b/src/Components/Server/ref/Microsoft.AspNetCore.Components.Server.netcoreapp3.0.cs index 7a89318f43..5440426852 100644 --- a/src/Components/Server/ref/Microsoft.AspNetCore.Components.Server.netcoreapp3.0.cs +++ b/src/Components/Server/ref/Microsoft.AspNetCore.Components.Server.netcoreapp3.0.cs @@ -33,6 +33,21 @@ namespace Microsoft.AspNetCore.Components.Server public int DisconnectedCircuitMaxRetained { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public System.TimeSpan DisconnectedCircuitRetentionPeriod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public System.TimeSpan JSInteropDefaultCallTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public int MaxBufferedUnacknowledgedRenderBatches { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } + public abstract partial class RevalidatingServerAuthenticationStateProvider : Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider, System.IDisposable + { + public RevalidatingServerAuthenticationStateProvider(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } + protected abstract System.TimeSpan RevalidationInterval { get; } + protected virtual void Dispose(bool disposing) { } + void System.IDisposable.Dispose() { } + protected abstract System.Threading.Tasks.Task ValidateAuthenticationStateAsync(Microsoft.AspNetCore.Components.AuthenticationState authenticationState, System.Threading.CancellationToken cancellationToken); + } + public partial class ServerAuthenticationStateProvider : Microsoft.AspNetCore.Components.AuthenticationStateProvider, Microsoft.AspNetCore.Components.IHostEnvironmentAuthenticationStateProvider + { + public ServerAuthenticationStateProvider() { } + public override System.Threading.Tasks.Task GetAuthenticationStateAsync() { throw null; } + public void SetAuthenticationState(System.Threading.Tasks.Task authenticationStateTask) { } } } namespace Microsoft.AspNetCore.Components.Server.Circuits diff --git a/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs b/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs index 021272c7d6..a154feb4e6 100644 --- a/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs +++ b/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs @@ -10,11 +10,13 @@ namespace Microsoft.AspNetCore.Builder /// public sealed class ComponentEndpointConventionBuilder : IHubEndpointConventionBuilder { - private readonly IEndpointConventionBuilder _endpointConventionBuilder; + private readonly IEndpointConventionBuilder _hubEndpoint; + private readonly IEndpointConventionBuilder _disconnectEndpoint; - internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder endpointConventionBuilder) + internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder hubEndpoint, IEndpointConventionBuilder disconnectEndpoint) { - _endpointConventionBuilder = endpointConventionBuilder; + _hubEndpoint = hubEndpoint; + _disconnectEndpoint = disconnectEndpoint; } /// @@ -23,7 +25,8 @@ namespace Microsoft.AspNetCore.Builder /// The convention to add to the builder. public void Add(Action convention) { - _endpointConventionBuilder.Add(convention); + _hubEndpoint.Add(convention); + _disconnectEndpoint.Add(convention); } } } diff --git a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs index 8af4ad37e8..d747a69b60 100644 --- a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs @@ -292,7 +292,17 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(configureOptions)); } - return new ComponentEndpointConventionBuilder(endpoints.MapHub(path, configureOptions)).AddComponent(componentType, selector); + var hubEndpoint = endpoints.MapHub(path, configureOptions); + + var disconnectEndpoint = endpoints.Map( + (path.EndsWith("/") ? path : path + "/") + "disconnect/", + endpoints.CreateApplicationBuilder().UseMiddleware().Build()) + .WithDisplayName("Blazor disconnect"); + + return new ComponentEndpointConventionBuilder( + hubEndpoint, + disconnectEndpoint) + .AddComponent(componentType, selector); } } } diff --git a/src/Components/Server/src/CircuitDisconnectMiddleware.cs b/src/Components/Server/src/CircuitDisconnectMiddleware.cs new file mode 100644 index 0000000000..ecacf511d7 --- /dev/null +++ b/src/Components/Server/src/CircuitDisconnectMiddleware.cs @@ -0,0 +1,103 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Server.Circuits; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Components.Server +{ + // We use a middlware so that we can use DI. + internal class CircuitDisconnectMiddleware + { + private const string CircuitIdKey = "circuitId"; + + public CircuitDisconnectMiddleware( + ILogger logger, + CircuitRegistry registry, + CircuitIdFactory circuitIdFactory, + RequestDelegate next) + { + Logger = logger; + Registry = registry; + CircuitIdFactory = circuitIdFactory; + Next = next; + } + + public ILogger Logger { get; } + public CircuitRegistry Registry { get; } + public CircuitIdFactory CircuitIdFactory { get; } + public RequestDelegate Next { get; } + + public async Task Invoke(HttpContext context) + { + if (!HttpMethods.IsPost(context.Request.Method)) + { + context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed; + return; + } + + var (hasCircuitId, circuitId) = await TryGetCircuitIdAsync(context); + if (!hasCircuitId) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + await TerminateCircuitGracefully(circuitId); + + context.Response.StatusCode = StatusCodes.Status200OK; + } + + private async Task<(bool, string)> TryGetCircuitIdAsync(HttpContext context) + { + try + { + if (!context.Request.HasFormContentType) + { + return (false, null); + } + + var form = await context.Request.ReadFormAsync(); + if (!form.TryGetValue(CircuitIdKey, out var circuitId) || !CircuitIdFactory.ValidateCircuitId(circuitId)) + { + return (false, null); + } + + return (true, circuitId); + } + catch + { + return (false, null); + } + } + + private async Task TerminateCircuitGracefully(string circuitId) + { + try + { + await Registry.Terminate(circuitId); + Log.CircuitTerminatedGracefully(Logger, circuitId); + } + catch (Exception e) + { + Log.UnhandledExceptionInCircuit(Logger, circuitId, e); + } + } + + private class Log + { + private static readonly Action _circuitTerminatedGracefully = + LoggerMessage.Define(LogLevel.Debug, new EventId(1, "CircuitTerminatedGracefully"), "Circuit '{CircuitId}' terminated gracefully"); + + private static readonly Action _unhandledExceptionInCircuit = + LoggerMessage.Define(LogLevel.Warning, new EventId(2, "UnhandledExceptionInCircuit"), "Unhandled exception in circuit {CircuitId} while terminating gracefully."); + + public static void CircuitTerminatedGracefully(ILogger logger, string circuitId) => _circuitTerminatedGracefully(logger, circuitId, null); + + public static void UnhandledExceptionInCircuit(ILogger logger, string circuitId, Exception exception) => _unhandledExceptionInCircuit(logger, circuitId, exception); + } + } +} diff --git a/src/Components/Server/src/CircuitOptions.cs b/src/Components/Server/src/CircuitOptions.cs index 30c1b9c407..68ca25c85a 100644 --- a/src/Components/Server/src/CircuitOptions.cs +++ b/src/Components/Server/src/CircuitOptions.cs @@ -64,5 +64,17 @@ namespace Microsoft.AspNetCore.Components.Server /// Defaults to 1 minute. /// public TimeSpan JSInteropDefaultCallTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Gets or sets the maximum number of render batches that a circuit will buffer until an acknowledgement for the batch is + /// received. + /// + /// + /// When the limit of buffered render batches is reached components will stop rendering and will wait until either the + /// circuit is disconnected and disposed or at least one batch gets acknowledged. + /// + /// + /// Defaults to 10. + public int MaxBufferedUnacknowledgedRenderBatches { get; set; } = 10; } } diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index 563cacbff6..92a2365971 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Security.Claims; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Web; @@ -41,7 +40,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime); - RendererRegistry.SetCurrentRendererRegistry(circuitHost.RendererRegistry); } public event UnhandledExceptionEventHandler UnhandledException; @@ -50,7 +48,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits string circuitId, IServiceScope scope, CircuitClientProxy client, - RendererRegistry rendererRegistry, RemoteRenderer renderer, IReadOnlyList descriptors, RemoteJSRuntime jsRuntime, @@ -60,7 +57,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits CircuitId = circuitId; _scope = scope ?? throw new ArgumentNullException(nameof(scope)); Client = client; - RendererRegistry = rendererRegistry ?? throw new ArgumentNullException(nameof(rendererRegistry)); Descriptors = descriptors ?? throw new ArgumentNullException(nameof(descriptors)); Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); JSRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime)); @@ -85,8 +81,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public RemoteRenderer Renderer { get; } - public RendererRegistry RendererRegistry { get; } - public IReadOnlyList Descriptors { get; } public IServiceProvider Services { get; } @@ -137,46 +131,38 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } } - public async Task DispatchEvent(string eventDescriptorJson, string eventArgs) + public async Task DispatchEvent(string eventDescriptorJson, string eventArgsJson) { - RendererRegistryEventDispatcher.BrowserEventDescriptor eventDescriptor = null; + WebEventData webEventData; try { AssertInitialized(); - eventDescriptor = ParseEventDescriptor(eventDescriptorJson); - if (eventDescriptor == null) - { - return; - } + webEventData = WebEventData.Parse(eventDescriptorJson, eventArgsJson); + } + catch (Exception ex) + { + Log.DispatchEventFailedToParseEventData(_logger, ex); + return; + } + try + { await Renderer.Dispatcher.InvokeAsync(() => { SetCurrentCircuitHost(this); - return RendererRegistryEventDispatcher.DispatchEvent(eventDescriptor, eventArgs); + return Renderer.DispatchEventAsync( + webEventData.EventHandlerId, + webEventData.EventFieldInfo, + webEventData.EventArgs); }); } catch (Exception ex) { - Log.DispatchEventFailedToDispatchEvent(_logger, eventDescriptor != null ? eventDescriptor.EventHandlerId.ToString() : null, ex); + Log.DispatchEventFailedToDispatchEvent(_logger, webEventData.EventHandlerId.ToString(), ex); UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); } } - private RendererRegistryEventDispatcher.BrowserEventDescriptor ParseEventDescriptor(string eventDescriptorJson) - { - try - { - return JsonSerializer.Deserialize( - eventDescriptorJson, - JsonSerializerOptionsProvider.Options); - } - catch (Exception ex) - { - Log.DispatchEventFailedToParseEventDescriptor(_logger, ex); - return null; - } - } - public async Task InitializeAsync(CancellationToken cancellationToken) { await Renderer.Dispatcher.InvokeAsync(async () => @@ -214,11 +200,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits try { AssertInitialized(); - if (assemblyName == "Microsoft.AspNetCore.Components.Web" && methodIdentifier == "DispatchEvent") - { - Log.DispatchEventTroughJSInterop(_logger); - return; - } await Renderer.Dispatcher.InvokeAsync(() => { @@ -230,7 +211,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits catch (Exception ex) { // We don't expect any of this code to actually throw, because DotNetDispatcher.BeginInvoke doesn't throw - // however, we still want this to get logged if we do. + // however, we still want this to get logged if we do. UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false)); } } @@ -410,9 +391,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits private static readonly Action _endInvokeDispatchException; private static readonly Action _endInvokeJSFailed; private static readonly Action _endInvokeJSSucceeded; - private static readonly Action _dispatchEventFailedToParseEventDescriptor; + private static readonly Action _dispatchEventFailedToParseEventData; private static readonly Action _dispatchEventFailedToDispatchEvent; - private static readonly Action _dispatchEventThroughJSInterop; private static readonly Action _locationChange; private static readonly Action _locationChangeSucceeded; private static readonly Action _locationChangeFailed; @@ -426,7 +406,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public static readonly EventId OnConnectionDown = new EventId(104, "OnConnectionDown"); public static readonly EventId OnCircuitClosed = new EventId(105, "OnCircuitClosed"); public static readonly EventId InvalidBrowserEventFormat = new EventId(106, "InvalidBrowserEventFormat"); - public static readonly EventId DispatchEventFailedToParseEventDescriptor = new EventId(107, "DispatchEventFailedToParseEventDescriptor"); + public static readonly EventId DispatchEventFailedToParseEventData = new EventId(107, "DispatchEventFailedToParseEventData"); public static readonly EventId DispatchEventFailedToDispatchEvent = new EventId(108, "DispatchEventFailedToDispatchEvent"); public static readonly EventId BeginInvokeDotNet = new EventId(109, "BeginInvokeDotNet"); public static readonly EventId EndInvokeDispatchException = new EventId(110, "EndInvokeDispatchException"); @@ -495,21 +475,16 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits EventIds.EndInvokeJSSucceeded, "The JS interop call with callback id '{AsyncCall}' succeeded."); - _dispatchEventFailedToParseEventDescriptor = LoggerMessage.Define( + _dispatchEventFailedToParseEventData = LoggerMessage.Define( LogLevel.Debug, - EventIds.DispatchEventFailedToParseEventDescriptor, - "Failed to parse the event descriptor data when trying to dispatch an event."); + EventIds.DispatchEventFailedToParseEventData, + "Failed to parse the event data when trying to dispatch an event."); _dispatchEventFailedToDispatchEvent = LoggerMessage.Define( LogLevel.Debug, EventIds.DispatchEventFailedToDispatchEvent, "There was an error dispatching the event '{EventHandlerId}' to the application."); - _dispatchEventThroughJSInterop = LoggerMessage.Define( - LogLevel.Debug, - EventIds.DispatchEventThroughJSInterop, - "There was an intent to dispatch a browser event through JS interop."); - _locationChange = LoggerMessage.Define( LogLevel.Debug, EventIds.LocationChange, @@ -555,7 +530,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public static void EndInvokeJSSucceeded(ILogger logger, long asyncCall) => _endInvokeJSSucceeded(logger, asyncCall, null); - public static void DispatchEventFailedToParseEventDescriptor(ILogger logger, Exception ex) => _dispatchEventFailedToParseEventDescriptor(logger, ex); + public static void DispatchEventFailedToParseEventData(ILogger logger, Exception ex) => _dispatchEventFailedToParseEventData(logger, ex); public static void DispatchEventFailedToDispatchEvent(ILogger logger, string eventHandlerId, Exception ex) => _dispatchEventFailedToDispatchEvent(logger, eventHandlerId ?? "", ex); @@ -571,8 +546,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } } - public static void DispatchEventTroughJSInterop(ILogger logger) => _dispatchEventThroughJSInterop(logger, null); - public static void LocationChange(ILogger logger, string circuitId, string uri) => _locationChange(logger, circuitId, uri, null); public static void LocationChangeSucceeded(ILogger logger, string circuitId, string uri) => _locationChangeSucceeded(logger, circuitId, uri, null); diff --git a/src/Components/Server/src/Circuits/CircuitRegistry.cs b/src/Components/Server/src/Circuits/CircuitRegistry.cs index 9fe25c5b68..a5c970c4af 100644 --- a/src/Components/Server/src/Circuits/CircuitRegistry.cs +++ b/src/Components/Server/src/Circuits/CircuitRegistry.cs @@ -81,15 +81,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } } - public void PermanentDisconnect(CircuitHost circuitHost) - { - if (ConnectedCircuits.TryRemove(circuitHost.CircuitId, out _)) - { - Log.CircuitDisconnectedPermanently(_logger, circuitHost.CircuitId); - circuitHost.Client.SetDisconnected(); - } - } - public virtual Task DisconnectAsync(CircuitHost circuitHost, string connectionId) { Log.CircuitDisconnectStarted(_logger, circuitHost.CircuitId, connectionId); @@ -297,6 +288,29 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits } } + public ValueTask Terminate(string circuitId) + { + CircuitHost circuitHost; + DisconnectedCircuitEntry entry = default; + lock (CircuitRegistryLock) + { + if (ConnectedCircuits.TryGetValue(circuitId, out circuitHost) || DisconnectedCircuits.TryGetValue(circuitId, out entry)) + { + circuitHost ??= entry.CircuitHost; + DisconnectedCircuits.Remove(circuitHost.CircuitId); + ConnectedCircuits.TryRemove(circuitHost.CircuitId, out _); + Log.CircuitDisconnectedPermanently(_logger, circuitHost.CircuitId); + circuitHost.Client.SetDisconnected(); + } + else + { + return default; + } + } + + return circuitHost?.DisposeAsync() ?? default; + } + private readonly struct DisconnectedCircuitEntry { public DisconnectedCircuitEntry(CircuitHost circuitHost, CancellationTokenSource tokenSource) diff --git a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs index d353d5dc2c..43af5cc8f4 100644 --- a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs +++ b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; +using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.Rendering; using Microsoft.AspNetCore.Components.Routing; @@ -13,8 +14,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.JSInterop; -using System.Threading.Tasks; namespace Microsoft.AspNetCore.Components.Server.Circuits { @@ -24,16 +25,19 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; private readonly CircuitIdFactory _circuitIdFactory; + private readonly CircuitOptions _options; public DefaultCircuitFactory( IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory, - CircuitIdFactory circuitIdFactory) + CircuitIdFactory circuitIdFactory, + IOptions options) { _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); _loggerFactory = loggerFactory; _logger = _loggerFactory.CreateLogger(); _circuitIdFactory = circuitIdFactory ?? throw new ArgumentNullException(nameof(circuitIdFactory)); + _options = options.Value; } public override CircuitHost CreateCircuitHost( @@ -54,13 +58,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits jsRuntime.Initialize(client); componentContext.Initialize(client); - var authenticationStateProvider = scope.ServiceProvider.GetService() as IHostEnvironmentAuthenticationStateProvider; - if (authenticationStateProvider != null) - { - var authenticationState = new AuthenticationState(httpContext.User); // TODO: Get this from the hub connection context instead - authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState)); - } - var navigationManager = (RemoteNavigationManager)scope.ServiceProvider.GetRequiredService(); var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService(); if (client.Connected) @@ -75,11 +72,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits navigationManager.Initialize(baseUri, uri); } - var rendererRegistry = new RendererRegistry(); var renderer = new RemoteRenderer( scope.ServiceProvider, _loggerFactory, - rendererRegistry, + _options, jsRuntime, client, encoder, @@ -93,7 +89,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits _circuitIdFactory.CreateCircuitId(), scope, client, - rendererRegistry, renderer, components, jsRuntime, diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 8b9ebb341a..f2dae7dd0d 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -3,12 +3,12 @@ using System; using System.Collections.Concurrent; -using System.Diagnostics; using System.Linq; using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Internal; @@ -23,8 +23,9 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering private readonly IJSRuntime _jsRuntime; private readonly CircuitClientProxy _client; - private readonly RendererRegistry _rendererRegistry; + private readonly CircuitOptions _options; private readonly ILogger _logger; + internal readonly ConcurrentQueue _unacknowledgedRenderBatches = new ConcurrentQueue(); private long _nextRenderId = 1; private bool _disposing = false; @@ -39,27 +40,21 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering public RemoteRenderer( IServiceProvider serviceProvider, ILoggerFactory loggerFactory, - RendererRegistry rendererRegistry, + CircuitOptions options, IJSRuntime jsRuntime, CircuitClientProxy client, HtmlEncoder encoder, ILogger logger) : base(serviceProvider, loggerFactory, encoder.Encode) { - _rendererRegistry = rendererRegistry; _jsRuntime = jsRuntime; _client = client; - - Id = _rendererRegistry.Add(this); + _options = options; _logger = logger; } - internal ConcurrentQueue UnacknowledgedRenderBatches = new ConcurrentQueue(); - public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault(); - public int Id { get; } - /// /// Associates the with the , /// causing it to be displayed in the specified DOM element. @@ -73,7 +68,6 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering var attachComponentTask = _jsRuntime.InvokeAsync( "Blazor._internal.attachRootComponentToElement", - Id, domElementSelector, componentId); CaptureAsyncExceptions(attachComponentTask); @@ -81,6 +75,34 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering return RenderRootComponentAsync(componentId); } + protected override void ProcessPendingRender() + { + if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches) + { + // If we got here it means we are at max capacity, so we don't want to actually process the queue, + // as we have a client that is not acknowledging render batches fast enough (something we consider needs + // to be fast). + // The result is something as follows: + // Lets imagine an extreme case where the server produces a new batch every milisecond. + // Lets say the client is able to ACK a batch every 100 miliseconds. + // When the app starts the client might see the sequence 0->(MaxUnacknowledgedRenderBatches-1) and then + // after 100 miliseconds it sees it jump to 1xx, then to 2xx where xx is something between {0..99} the + // reason for this is that the server slows down rendering new batches to as fast as the client can consume + // them. + // Similarly, if a client were to send events at a faster pace than the server can consume them, the server + // would still proces the events, but would not produce new renders until it gets an ack that frees up space + // for a new render. + // We should never see UnacknowledgedRenderBatches.Count > _options.MaxBufferedUnacknowledgedRenderBatches + + // But if we do, it's safer to simply disable the rendering in that case too instead of allowing batches to + Log.FullUnacknowledgedRenderBatchesQueue(_logger); + + return; + } + + base.ProcessPendingRender(); + } + /// protected override void HandleException(Exception exception) { @@ -103,8 +125,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering protected override void Dispose(bool disposing) { _disposing = true; - _rendererRegistry.TryRemove(Id); - while (UnacknowledgedRenderBatches.TryDequeue(out var entry)) + while (_unacknowledgedRenderBatches.TryDequeue(out var entry)) { entry.CompletionSource.TrySetCanceled(); entry.Data.Dispose(); @@ -146,7 +167,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // Buffer the rendered batches no matter what. We'll send it down immediately when the client // is connected or right after the client reconnects. - UnacknowledgedRenderBatches.Enqueue(pendingRender); + _unacknowledgedRenderBatches.Enqueue(pendingRender); } catch { @@ -167,7 +188,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // All the batches are sent in order based on the fact that SignalR // provides ordering for the underlying messages and that the batches // are always in order. - return Task.WhenAll(UnacknowledgedRenderBatches.Select(b => WriteBatchBytesAsync(b))); + return Task.WhenAll(_unacknowledgedRenderBatches.Select(b => WriteBatchBytesAsync(b))); } private async Task WriteBatchBytesAsync(UnacknowledgedRenderBatch pending) @@ -190,7 +211,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering Log.BeginUpdateDisplayAsync(_logger, _client.ConnectionId, pending.BatchId, pending.Data.Count); var segment = new ArraySegment(pending.Data.Buffer, 0, pending.Data.Count); - await _client.SendAsync("JS.RenderBatch", Id, pending.BatchId, segment); + await _client.SendAsync("JS.RenderBatch", pending.BatchId, segment); } catch (Exception e) { @@ -203,12 +224,12 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // disposed. } - public void OnRenderCompleted(long incomingBatchId, string errorMessageOrNull) + public Task OnRenderCompleted(long incomingBatchId, string errorMessageOrNull) { if (_disposing) { // Disposing so don't do work. - return; + return Task.CompletedTask; } // When clients send acks we know for sure they received and applied the batch. @@ -234,18 +255,21 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // synchronizes calls to hub methods. That is, it won't issue more than one call to this method from the same hub // at the same time on different threads. - if (!UnacknowledgedRenderBatches.TryPeek(out var nextUnacknowledgedBatch) || incomingBatchId < nextUnacknowledgedBatch.BatchId) + if (!_unacknowledgedRenderBatches.TryPeek(out var nextUnacknowledgedBatch) || incomingBatchId < nextUnacknowledgedBatch.BatchId) { Log.ReceivedDuplicateBatchAck(_logger, incomingBatchId); + return Task.CompletedTask; } else { var lastBatchId = nextUnacknowledgedBatch.BatchId; // Order is important here so that we don't prematurely dequeue the last nextUnacknowledgedBatch - while (UnacknowledgedRenderBatches.TryPeek(out nextUnacknowledgedBatch) && nextUnacknowledgedBatch.BatchId <= incomingBatchId) + while (_unacknowledgedRenderBatches.TryPeek(out nextUnacknowledgedBatch) && nextUnacknowledgedBatch.BatchId <= incomingBatchId) { lastBatchId = nextUnacknowledgedBatch.BatchId; - UnacknowledgedRenderBatches.TryDequeue(out _); + // At this point the queue is definitely not full, we have at least emptied one slot, so we allow a further + // full queue log entry the next time it fills up. + _unacknowledgedRenderBatches.TryDequeue(out _); ProcessPendingBatch(errorMessageOrNull, nextUnacknowledgedBatch); } @@ -253,7 +277,16 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering { HandleException( new InvalidOperationException($"Received an acknowledgement for batch with id '{incomingBatchId}' when the last batch produced was '{lastBatchId}'.")); + return Task.CompletedTask; } + + // Normally we will not have pending renders, but it might happen that we reached the limit of + // available buffered renders and new renders got queued. + // Invoke ProcessBufferedRenderRequests so that we might produce any additional batch that is + // missing. + + // We return the task in here, but the caller doesn't await it. + return Dispatcher.InvokeAsync(() => ProcessPendingRender()); } } @@ -321,6 +354,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering private static readonly Action _completingBatchWithError; private static readonly Action _completingBatchWithoutError; private static readonly Action _receivedDuplicateBatchAcknowledgement; + private static readonly Action _fullUnacknowledgedRenderBatchesQueue; private static class EventIds { @@ -331,6 +365,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering public static readonly EventId CompletingBatchWithError = new EventId(104, "CompletingBatchWithError"); public static readonly EventId CompletingBatchWithoutError = new EventId(105, "CompletingBatchWithoutError"); public static readonly EventId ReceivedDuplicateBatchAcknowledgement = new EventId(106, "ReceivedDuplicateBatchAcknowledgement"); + public static readonly EventId FullUnacknowledgedRenderBatchesQueue = new EventId(107, "FullUnacknowledgedRenderBatchesQueue"); } static Log() @@ -369,6 +404,11 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering LogLevel.Debug, EventIds.ReceivedDuplicateBatchAcknowledgement, "Received a duplicate ACK for batch id '{IncomingBatchId}'."); + + _fullUnacknowledgedRenderBatchesQueue = LoggerMessage.Define( + LogLevel.Debug, + EventIds.FullUnacknowledgedRenderBatchesQueue, + "The queue of unacknowledged render batches is full."); } public static void SendBatchDataFailed(ILogger logger, Exception exception) @@ -421,10 +461,27 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering null); } - internal static void ReceivedDuplicateBatchAck(ILogger logger, long incomingBatchId) + public static void ReceivedDuplicateBatchAck(ILogger logger, long incomingBatchId) { _receivedDuplicateBatchAcknowledgement(logger, incomingBatchId, null); } + + public static void FullUnacknowledgedRenderBatchesQueue(ILogger logger) + { + _fullUnacknowledgedRenderBatchesQueue(logger, null); + } } } + + internal readonly struct PendingRender + { + public PendingRender(int componentId, RenderFragment renderFragment) + { + ComponentId = componentId; + RenderFragment = renderFragment; + } + + public int ComponentId { get; } + public RenderFragment RenderFragment { get; } + } } diff --git a/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs b/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs new file mode 100644 index 0000000000..2895d6df94 --- /dev/null +++ b/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs @@ -0,0 +1,119 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Components.Server +{ + /// + /// A base class for services that receive an + /// authentication state from the host environment, and revalidate it at regular intervals. + /// + public abstract class RevalidatingServerAuthenticationStateProvider + : ServerAuthenticationStateProvider, IDisposable + { + private readonly ILogger _logger; + private CancellationTokenSource _loopCancellationTokenSource = new CancellationTokenSource(); + + /// + /// Constructs an instance of . + /// + /// A logger factory. + public RevalidatingServerAuthenticationStateProvider(ILoggerFactory loggerFactory) + { + if (loggerFactory is null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + _logger = loggerFactory.CreateLogger(); + + // Whenever we receive notification of a new authentication state, cancel any + // existing revalidation loop and start a new one + AuthenticationStateChanged += authenticationStateTask => + { + _loopCancellationTokenSource?.Cancel(); + _loopCancellationTokenSource = new CancellationTokenSource(); + _ = RevalidationLoop(authenticationStateTask, _loopCancellationTokenSource.Token); + }; + } + + /// + /// Gets the interval between revalidation attempts. + /// + protected abstract TimeSpan RevalidationInterval { get; } + + /// + /// Determines whether the authentication state is still valid. + /// + /// The current . + /// A to observe while performing the operation. + /// A that resolves as true if the is still valid, or false if it is not. + protected abstract Task ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken); + + private async Task RevalidationLoop(Task authenticationStateTask, CancellationToken cancellationToken) + { + try + { + var authenticationState = await authenticationStateTask; + if (authenticationState.User.Identity.IsAuthenticated) + { + while (!cancellationToken.IsCancellationRequested) + { + bool isValid; + + try + { + await Task.Delay(RevalidationInterval, cancellationToken); + isValid = await ValidateAuthenticationStateAsync(authenticationState, cancellationToken); + } + catch (TaskCanceledException tce) + { + // If it was our cancellation token, then this revalidation loop gracefully completes + // Otherwise, treat it like any other failure + if (tce.CancellationToken == cancellationToken) + { + break; + } + + throw; + } + + if (!isValid) + { + ForceSignOut(); + break; + } + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while revalidating authentication state"); + ForceSignOut(); + } + } + + private void ForceSignOut() + { + var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity()); + var anonymousState = new AuthenticationState(anonymousUser); + SetAuthenticationState(Task.FromResult(anonymousState)); + } + + void IDisposable.Dispose() + { + _loopCancellationTokenSource?.Cancel(); + Dispose(disposing: true); + } + + /// + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs b/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs index bd1fecfa68..2708b902e9 100644 --- a/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs +++ b/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs @@ -4,19 +4,21 @@ using System; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Components.Server.Circuits +namespace Microsoft.AspNetCore.Components.Server { /// /// An intended for use in server-side Blazor. /// - internal class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider + public class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider { private Task _authenticationStateTask; + /// public override Task GetAuthenticationStateAsync() => _authenticationStateTask ?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(SetAuthenticationState)}."); + /// public void SetAuthenticationState(Task authenticationStateTask) { _authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask)); diff --git a/src/Components/Server/src/ComponentHub.cs b/src/Components/Server/src/ComponentHub.cs index 4da9a3391e..f83fb5e210 100644 --- a/src/Components/Server/src/ComponentHub.cs +++ b/src/Components/Server/src/ComponentHub.cs @@ -68,34 +68,7 @@ namespace Microsoft.AspNetCore.Components.Server return Task.CompletedTask; } - if (exception != null) - { - return _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId); - } - else - { - // The client will gracefully disconnect when using websockets by correctly closing the TCP connection. - // This happens when the user closes a tab, navigates away from the page or reloads the page. - // In these situations we know the user is done with the circuit, so we can get rid of it at that point. - // This is important to be able to more efficiently manage resources, specially memory. - return TerminateCircuitGracefully(circuitHost); - } - } - - private async Task TerminateCircuitGracefully(CircuitHost circuitHost) - { - try - { - Log.CircuitTerminatedGracefully(_logger, circuitHost.CircuitId); - _circuitRegistry.PermanentDisconnect(circuitHost); - await circuitHost.DisposeAsync(); - } - catch (Exception e) - { - Log.UnhandledExceptionInCircuit(_logger, circuitHost.CircuitId, e); - } - - await _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId); + return _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId); } /// @@ -242,7 +215,7 @@ namespace Microsoft.AspNetCore.Components.Server } Log.ReceivedConfirmationForBatch(_logger, renderId); - CircuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull); + _ = CircuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull); } public void OnLocationChanged(string uri, bool intercepted) diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj index 3f87bd43af..639cdc9d5b 100644 --- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj +++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj @@ -21,6 +21,16 @@ + + + + @@ -31,6 +41,10 @@ + + + + @@ -53,12 +67,7 @@ - - ..\..\Web.JS\dist\Release\blazor.server.js - ..\..\Web.JS\dist\Debug\blazor.server.js + ..\..\Web.JS\dist\$(Configuration)\blazor.server.js diff --git a/src/Components/Server/test/CircuitDisconnectMiddlewareTest.cs b/src/Components/Server/test/CircuitDisconnectMiddlewareTest.cs new file mode 100644 index 0000000000..627c51b576 --- /dev/null +++ b/src/Components/Server/test/CircuitDisconnectMiddlewareTest.cs @@ -0,0 +1,244 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Server.Circuits; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Server +{ + public class CircuitDisconnectMiddlewareTest + { + [Theory] + [InlineData("GET")] + [InlineData("PUT")] + [InlineData("DELETE")] + [InlineData("HEAD")] + public async Task DisconnectMiddleware_OnlyAccepts_PostRequests(string httpMethod) + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + var context = new DefaultHttpContext(); + context.Request.Method = httpMethod; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status405MethodNotAllowed, context.Response.StatusCode); + } + + [Theory] + [InlineData(null)] + [InlineData("application/json")] + public async Task Returns400BadRequest_ForInvalidContentTypes(string contentType) + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = contentType; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + } + + [Fact] + public async Task Returns400BadRequest_IfNoCircuitIdOnForm() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + } + + [Fact] + public async Task Returns400BadRequest_InvalidCircuitId() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + using var memory = new MemoryStream(); + await new FormUrlEncodedContent(new Dictionary { ["circuitId"] = "1234" }).CopyToAsync(memory); + memory.Seek(0, SeekOrigin.Begin); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = memory; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); + } + + [Fact] + public async Task Returns200OK_NonExistingCircuit() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var id = circuitIdFactory.CreateCircuitId(); + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + using var memory = new MemoryStream(); + await new FormUrlEncodedContent(new Dictionary { ["circuitId"] = id }).CopyToAsync(memory); + memory.Seek(0, SeekOrigin.Begin); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = memory; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + } + + [Fact] + public async Task GracefullyTerminates_ConnectedCircuit() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var id = circuitIdFactory.CreateCircuitId(); + var testCircuitHost = TestCircuitHost.Create(id); + + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + registry.Register(testCircuitHost); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + using var memory = new MemoryStream(); + await new FormUrlEncodedContent(new Dictionary { ["circuitId"] = id }).CopyToAsync(memory); + memory.Seek(0, SeekOrigin.Begin); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = memory; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + } + + [Fact] + public async Task GracefullyTerminates_DisconnectedCircuit() + { + // Arrange + var circuitIdFactory = TestCircuitIdFactory.CreateTestFactory(); + var id = circuitIdFactory.CreateCircuitId(); + var circuitHost = TestCircuitHost.Create(id); + + var registry = new CircuitRegistry( + Options.Create(new CircuitOptions()), + NullLogger.Instance, + circuitIdFactory); + + registry.Register(circuitHost); + await registry.DisconnectAsync(circuitHost, "1234"); + + var middleware = new CircuitDisconnectMiddleware( + NullLogger.Instance, + registry, + circuitIdFactory, + (ctx) => Task.CompletedTask); + + using var memory = new MemoryStream(); + await new FormUrlEncodedContent(new Dictionary { ["circuitId"] = id }).CopyToAsync(memory); + memory.Seek(0, SeekOrigin.Begin); + + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = memory; + + // Act + await middleware.Invoke(context); + + // Assert + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + } + } +} diff --git a/src/Components/Server/test/Circuits/CircuitHostTest.cs b/src/Components/Server/test/Circuits/CircuitHostTest.cs index 58803a56b2..3c2ca28992 100644 --- a/src/Components/Server/test/Circuits/CircuitHostTest.cs +++ b/src/Components/Server/test/Circuits/CircuitHostTest.cs @@ -232,15 +232,14 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { return new TestRemoteRenderer( Mock.Of(), - new RendererRegistry(), Mock.Of(), Mock.Of()); } private class TestRemoteRenderer : RemoteRenderer { - public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client) - : base(serviceProvider, NullLoggerFactory.Instance, rendererRegistry, jsRuntime, new CircuitClientProxy(client, "connection"), HtmlEncoder.Default, NullLogger.Instance) + public TestRemoteRenderer(IServiceProvider serviceProvider, IJSRuntime jsRuntime, IClientProxy client) + : base(serviceProvider, NullLoggerFactory.Instance, new CircuitOptions(), jsRuntime, new CircuitClientProxy(client, "connection"), HtmlEncoder.Default, NullLogger.Instance) { } diff --git a/src/Components/Server/test/Circuits/RemoteRendererTest.cs b/src/Components/Server/test/Circuits/RemoteRendererTest.cs index ffb913544a..d50e5fa5e6 100644 --- a/src/Components/Server/test/Circuits/RemoteRendererTest.cs +++ b/src/Components/Server/test/Circuits/RemoteRendererTest.cs @@ -8,9 +8,11 @@ using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.JSInterop; using Moq; @@ -48,7 +50,82 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering component.TriggerRender(); // Assert - Assert.Equal(2, renderer.UnacknowledgedRenderBatches.Count); + Assert.Equal(2, renderer._unacknowledgedRenderBatches.Count); + } + + [Fact] + public void NotAcknowledgingRenders_ProducesBatches_UpToTheLimit() + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); + var component = new TestComponent(builder => + { + builder.OpenElement(0, "my element"); + builder.AddContent(1, "some text"); + builder.CloseElement(); + }); + + // Act + var componentId = renderer.AssignRootComponentId(component); + for (int i = 0; i < 20; i++) + { + component.TriggerRender(); + + } + + // Assert + Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count); + } + + [Fact] + public async Task NoNewBatchesAreCreated_WhenThereAreNoPendingRenderRequestsFromComponents() + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); + var component = new TestComponent(builder => + { + builder.OpenElement(0, "my element"); + builder.AddContent(1, "some text"); + builder.CloseElement(); + }); + + // Act + var componentId = renderer.AssignRootComponentId(component); + for (var i = 0; i < 10; i++) + { + component.TriggerRender(); + } + + await renderer.OnRenderCompleted(2, null); + + // Assert + Assert.Equal(9, renderer._unacknowledgedRenderBatches.Count); + } + + + [Fact] + public async Task ProducesNewBatch_WhenABatchGetsAcknowledged() + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider); + var i = 0; + var component = new TestComponent(builder => + { + builder.AddContent(0, $"Value {i}"); + }); + + // Act + var componentId = renderer.AssignRootComponentId(component); + for (i = 0; i < 20; i++) + { + component.TriggerRender(); + } + Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count); + + await renderer.OnRenderCompleted(2, null); + + // Assert + Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count); } [Fact] @@ -65,7 +142,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering var initialClient = new Mock(); initialClient.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1])) + .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[0])) .Returns(firstBatchTCS.Task); var circuitClient = new CircuitClientProxy(initialClient.Object, "connection0"); var renderer = GetRemoteRenderer(serviceProvider, circuitClient); @@ -78,12 +155,12 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering var client = new Mock(); client.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1])) - .Returns((n, v, t) => (long)v[1] == 3 ? secondBatchTCS.Task : thirdBatchTCS.Task); + .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[0])) + .Returns((n, v, t) => (long)v[0] == 3 ? secondBatchTCS.Task : thirdBatchTCS.Task); var componentId = renderer.AssignRootComponentId(component); component.TriggerRender(); - renderer.OnRenderCompleted(2, null); + _ = renderer.OnRenderCompleted(2, null); @event.Reset(); firstBatchTCS.SetResult(null); @@ -101,7 +178,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering foreach (var id in renderIds.ToArray()) { - renderer.OnRenderCompleted(id, null); + _ = renderer.OnRenderCompleted(id, null); } secondBatchTCS.SetResult(null); @@ -152,7 +229,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // This produces an additional batch (id = 3) trigger.TriggerRender(); - var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count; + var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act offlineClient.Transfer(onlineClient.Object, "new-connection"); @@ -164,14 +241,14 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // Receive the ack for the intial batch - renderer.OnRenderCompleted(2, null); + _ = renderer.OnRenderCompleted(2, null); // Receive the ack for the second batch - renderer.OnRenderCompleted(3, null); + _ = renderer.OnRenderCompleted(3, null); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); // Repeat the ack for the third batch - renderer.OnRenderCompleted(3, null); + _ = renderer.OnRenderCompleted(3, null); // Assert Assert.Empty(exceptions); @@ -215,7 +292,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // This produces an additional batch (id = 3) trigger.TriggerRender(); - var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count; + var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act offlineClient.Transfer(onlineClient.Object, "new-connection"); @@ -227,14 +304,14 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // Receive the ack for the intial batch - renderer.OnRenderCompleted(2, null); + _ = renderer.OnRenderCompleted(2, null); // Receive the ack for the second batch - renderer.OnRenderCompleted(2, null); + _ = renderer.OnRenderCompleted(2, null); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); // Repeat the ack for the third batch - renderer.OnRenderCompleted(3, null); + _ = renderer.OnRenderCompleted(3, null); // Assert Assert.Empty(exceptions); @@ -278,7 +355,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // This produces an additional batch (id = 3) trigger.TriggerRender(); - var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count; + var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act var exceptions = new List(); @@ -288,13 +365,13 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // Pretend that we missed the ack for the initial batch - renderer.OnRenderCompleted(3, null); + _ = renderer.OnRenderCompleted(3, null); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); // Assert Assert.Empty(exceptions); - Assert.Empty(renderer.UnacknowledgedRenderBatches); + Assert.Empty(renderer._unacknowledgedRenderBatches); } [Fact] @@ -335,7 +412,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering }; // This produces an additional batch (id = 3) trigger.TriggerRender(); - var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count; + var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count; // Act var exceptions = new List(); @@ -344,7 +421,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering exceptions.Add(e); }; - renderer.OnRenderCompleted(4, null); + _ = renderer.OnRenderCompleted(4, null); firstBatchTCS.SetResult(null); secondBatchTCS.SetResult(null); @@ -372,7 +449,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering // Assert Assert.Equal(0, first.ComponentId); Assert.Equal(1, second.ComponentId); - Assert.Equal(2, renderer.UnacknowledgedRenderBatches.Count); + Assert.Equal(2, renderer._unacknowledgedRenderBatches.Count); } private RemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClientProxy) @@ -388,7 +465,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering return new RemoteRenderer( serviceProvider, NullLoggerFactory.Instance, - new RendererRegistry(), + new CircuitOptions(), jsRuntime.Object, circuitClientProxy, HtmlEncoder.Default, diff --git a/src/Components/Server/test/Circuits/RevalidatingServerAuthenticationStateProvider.cs b/src/Components/Server/test/Circuits/RevalidatingServerAuthenticationStateProvider.cs new file mode 100644 index 0000000000..9f791c41eb --- /dev/null +++ b/src/Components/Server/test/Circuits/RevalidatingServerAuthenticationStateProvider.cs @@ -0,0 +1,256 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Microsoft.AspNetCore.Components +{ + public class RevalidatingServerAuthenticationStateProviderTest + { + [Fact] + public void AcceptsAndReturnsAuthStateFromHost() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider(TimeSpan.MaxValue); + + // Act/Assert: Host can supply a value + var hostAuthStateTask = (new TaskCompletionSource()).Task; + provider.SetAuthenticationState(hostAuthStateTask); + Assert.Same(hostAuthStateTask, provider.GetAuthenticationStateAsync()); + + // Act/Assert: Host can supply a changed value + var hostAuthStateTask2 = (new TaskCompletionSource()).Task; + provider.SetAuthenticationState(hostAuthStateTask2); + Assert.Same(hostAuthStateTask2, provider.GetAuthenticationStateAsync()); + } + + [Fact] + public async Task IfValidateAuthenticationStateAsyncReturnsTrue_ContinuesRevalidating() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.NextValidationResult = Task.FromResult(true); + var didNotifyAuthenticationStateChanged = false; + provider.AuthenticationStateChanged += _ => { didNotifyAuthenticationStateChanged = true; }; + + // Act + for (var i = 0; i < 10; i++) + { + await provider.NextValidateAuthenticationStateAsyncCall; + } + + // Assert + Assert.Equal(10, provider.RevalidationCallLog.Count); + Assert.False(didNotifyAuthenticationStateChanged); + Assert.Equal("test user", (await provider.GetAuthenticationStateAsync()).User.Identity.Name); + } + + [Fact] + public async Task IfValidateAuthenticationStateAsyncReturnsFalse_ForcesSignOut() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.NextValidationResult = Task.FromResult(false); + + var newAuthStateNotificationTcs = new TaskCompletionSource>(); + provider.AuthenticationStateChanged += newStateTask => newAuthStateNotificationTcs.SetResult(newStateTask); + + // Act + var newAuthStateTask = await newAuthStateNotificationTcs.Task; + var newAuthState = await newAuthStateTask; + + // Assert + Assert.False(newAuthState.User.Identity.IsAuthenticated); + + // Assert: no longer revalidates + await Task.Delay(200); + Assert.Single(provider.RevalidationCallLog); + } + + [Fact] + public async Task IfValidateAuthenticationStateAsyncThrows_ForcesSignOut() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.NextValidationResult = Task.FromException(new InvalidTimeZoneException()); + + var newAuthStateNotificationTcs = new TaskCompletionSource>(); + provider.AuthenticationStateChanged += newStateTask => newAuthStateNotificationTcs.SetResult(newStateTask); + + // Act + var newAuthStateTask = await newAuthStateNotificationTcs.Task; + var newAuthState = await newAuthStateTask; + + // Assert + Assert.False(newAuthState.User.Identity.IsAuthenticated); + + // Assert: no longer revalidates + await Task.Delay(200); + Assert.Single(provider.RevalidationCallLog); + } + + [Fact] + public async Task IfHostSuppliesNewAuthenticationState_RestartsRevalidationLoop() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.NextValidationResult = Task.FromResult(true); + await provider.NextValidateAuthenticationStateAsyncCall; + Assert.Collection(provider.RevalidationCallLog, + call => Assert.Equal("test user", call.AuthenticationState.User.Identity.Name)); + + // Act/Assert 1: Can become signed out + // Doesn't revalidate unauthenticated states + provider.SetAuthenticationState(CreateAuthenticationStateTask(null)); + await Task.Delay(200); + Assert.Empty(provider.RevalidationCallLog.Skip(1)); + + // Act/Assert 2: Can become a different user; resumes revalidation + provider.SetAuthenticationState(CreateAuthenticationStateTask("different user")); + await provider.NextValidateAuthenticationStateAsyncCall; + Assert.Collection(provider.RevalidationCallLog.Skip(1), + call => Assert.Equal("different user", call.AuthenticationState.User.Identity.Name)); + } + + [Fact] + public async Task StopsRevalidatingAfterDisposal() + { + // Arrange + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.NextValidationResult = Task.FromResult(true); + + // Act + ((IDisposable)provider).Dispose(); + await Task.Delay(200); + + // Assert + Assert.Empty(provider.RevalidationCallLog); + } + + [Fact] + public async Task SuppliesCancellationTokenThatSignalsWhenRevalidationLoopIsBeingDiscarded() + { + // Arrange + var validationTcs = new TaskCompletionSource(); + var authenticationStateChangedCount = 0; + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.NextValidationResult = validationTcs.Task; + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.AuthenticationStateChanged += _ => { authenticationStateChangedCount++; }; + + // Act/Assert 1: token isn't cancelled initially + await provider.NextValidateAuthenticationStateAsyncCall; + var firstRevalidationCall = provider.RevalidationCallLog.Single(); + Assert.False(firstRevalidationCall.CancellationToken.IsCancellationRequested); + Assert.Equal(0, authenticationStateChangedCount); + + // Have the task throw a TCE to show this doesn't get treated as a failure + firstRevalidationCall.CancellationToken.Register(() => validationTcs.TrySetCanceled(firstRevalidationCall.CancellationToken)); + + // Act/Assert 2: token is cancelled when the loop is superseded + provider.NextValidationResult = Task.FromResult(true); + provider.SetAuthenticationState(CreateAuthenticationStateTask("different user")); + Assert.True(firstRevalidationCall.CancellationToken.IsCancellationRequested); + + // Since we asked for that operation to be cancelled, we don't treat it as a failure and + // don't force a logout + Assert.Equal(1, authenticationStateChangedCount); + Assert.Equal("different user", (await provider.GetAuthenticationStateAsync()).User.Identity.Name); + + // Subsequent revalidation can complete successfully + await provider.NextValidateAuthenticationStateAsyncCall; + Assert.Collection(provider.RevalidationCallLog.Skip(1), + call => Assert.Equal("different user", call.AuthenticationState.User.Identity.Name)); + } + + [Fact] + public async Task IfValidateAuthenticationStateAsyncReturnsUnrelatedCancelledTask_TreatAsFailure() + { + // Arrange + var validationTcs = new TaskCompletionSource(); + var authenticationStateChangedCount = 0; + using var provider = new TestRevalidatingServerAuthenticationStateProvider( + TimeSpan.FromMilliseconds(50)); + provider.NextValidationResult = validationTcs.Task; + provider.SetAuthenticationState(CreateAuthenticationStateTask("test user")); + provider.AuthenticationStateChanged += _ => { authenticationStateChangedCount++; }; + + // Be waiting for the first ValidateAuthenticationStateAsync to complete + await provider.NextValidateAuthenticationStateAsyncCall; + var firstRevalidationCall = provider.RevalidationCallLog.Single(); + Assert.Equal(0, authenticationStateChangedCount); + + // Act: ValidateAuthenticationStateAsync returns cancelled task, but the cancellation + // is unrelated to the CT we supplied + validationTcs.TrySetCanceled(new CancellationTokenSource().Token); + + // Assert: Since we didn't ask for that operation to be cancelled, this is treated as + // a failure to validate, so we force a logout + Assert.Equal(1, authenticationStateChangedCount); + var newAuthState = await provider.GetAuthenticationStateAsync(); + Assert.False(newAuthState.User.Identity.IsAuthenticated); + Assert.Null(newAuthState.User.Identity.Name); + } + + static Task CreateAuthenticationStateTask(string username) + { + var identity = !string.IsNullOrEmpty(username) + ? new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username) }, "testauth") + : new ClaimsIdentity(); + var authenticationState = new AuthenticationState(new ClaimsPrincipal(identity)); + return Task.FromResult(authenticationState); + } + + class TestRevalidatingServerAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider + { + private readonly TimeSpan _revalidationInterval; + private TaskCompletionSource _nextValidateAuthenticationStateAsyncCallSource + = new TaskCompletionSource(); + + public TestRevalidatingServerAuthenticationStateProvider(TimeSpan revalidationInterval) + : base(NullLoggerFactory.Instance) + { + _revalidationInterval = revalidationInterval; + } + + public Task NextValidationResult { get; set; } + + public Task NextValidateAuthenticationStateAsyncCall + => _nextValidateAuthenticationStateAsyncCallSource.Task; + + public List<(AuthenticationState AuthenticationState, CancellationToken CancellationToken)> RevalidationCallLog { get; } + = new List<(AuthenticationState, CancellationToken)>(); + + protected override TimeSpan RevalidationInterval => _revalidationInterval; + + protected override Task ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken) + { + RevalidationCallLog.Add((authenticationState, cancellationToken)); + var result = NextValidationResult; + var prevCts = _nextValidateAuthenticationStateAsyncCallSource; + _nextValidateAuthenticationStateAsyncCallSource = new TaskCompletionSource(); + prevCts.SetResult(true); + return result; + } + } + } +} diff --git a/src/Components/Server/test/Circuits/TestCircuitHost.cs b/src/Components/Server/test/Circuits/TestCircuitHost.cs index f8f5996d95..3e9896e9df 100644 --- a/src/Components/Server/test/Circuits/TestCircuitHost.cs +++ b/src/Components/Server/test/Circuits/TestCircuitHost.cs @@ -18,8 +18,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { internal class TestCircuitHost : CircuitHost { - private TestCircuitHost(string circuitId, IServiceScope scope, CircuitClientProxy client, RendererRegistry rendererRegistry, RemoteRenderer renderer, IReadOnlyList descriptors, RemoteJSRuntime jsRuntime, CircuitHandler[] circuitHandlers, ILogger logger) - : base(circuitId, scope, client, rendererRegistry, renderer, descriptors, jsRuntime, circuitHandlers, logger) + private TestCircuitHost(string circuitId, IServiceScope scope, CircuitClientProxy client, RemoteRenderer renderer, IReadOnlyList descriptors, RemoteJSRuntime jsRuntime, CircuitHandler[] circuitHandlers, ILogger logger) + : base(circuitId, scope, client, renderer, descriptors, jsRuntime, circuitHandlers, logger) { } @@ -37,7 +37,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { serviceScope = serviceScope ?? Mock.Of(); clientProxy = clientProxy ?? new CircuitClientProxy(Mock.Of(), Guid.NewGuid().ToString()); - var renderRegistry = new RendererRegistry(); var jsRuntime = new RemoteJSRuntime(Options.Create(new CircuitOptions()), Mock.Of>()); if (remoteRenderer == null) @@ -45,7 +44,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits remoteRenderer = new RemoteRenderer( serviceScope.ServiceProvider ?? Mock.Of(), NullLoggerFactory.Instance, - new RendererRegistry(), + new CircuitOptions(), jsRuntime, clientProxy, HtmlEncoder.Default, @@ -57,7 +56,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits circuitId ?? Guid.NewGuid().ToString(), serviceScope, clientProxy, - renderRegistry, remoteRenderer, new List(), jsRuntime, diff --git a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs index cba13e8581..852ad62f0d 100644 --- a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs +++ b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; @@ -63,6 +64,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests services.AddRouting(); services.AddSignalR(); services.AddServerSideBlazor(); + services.AddSingleton(new ConfigurationBuilder().Build()); var serviceProvder = services.BuildServiceProvider(); diff --git a/src/Components/Web/src/BrowserNavigationManagerInterop.cs b/src/Components/Shared/src/BrowserNavigationManagerInterop.cs similarity index 100% rename from src/Components/Web/src/BrowserNavigationManagerInterop.cs rename to src/Components/Shared/src/BrowserNavigationManagerInterop.cs diff --git a/src/Components/Web/src/RendererRegistryEventDispatcher.cs b/src/Components/Shared/src/WebEventData.cs similarity index 52% rename from src/Components/Web/src/RendererRegistryEventDispatcher.cs rename to src/Components/Shared/src/WebEventData.cs index 7bffa1ec6e..dbcf8c5aa9 100644 --- a/src/Components/Web/src/RendererRegistryEventDispatcher.cs +++ b/src/Components/Shared/src/WebEventData.cs @@ -3,57 +3,47 @@ using System; using System.Text.Json; -using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; -using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Components.Web { - /// - /// Provides mechanisms for dispatching events to components in a . - /// - public static class RendererRegistryEventDispatcher + internal class WebEventData { - /// - /// For framework use only. - /// - [JSInvokable(nameof(DispatchEvent))] - public static Task DispatchEvent(BrowserEventDescriptor eventDescriptor, string eventArgsJson) + // This class represents the second half of parsing incoming event data, + // once the type of the eventArgs becomes known. + + public static WebEventData Parse(string eventDescriptorJson, string eventArgsJson) { - InterpretEventDescriptor(eventDescriptor); - var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson); - var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId); - return renderer.DispatchEventAsync(eventDescriptor.EventHandlerId, eventDescriptor.EventFieldInfo, eventArgs); + return Parse( + Deserialize(eventDescriptorJson), + eventArgsJson); } - private static void InterpretEventDescriptor(BrowserEventDescriptor eventDescriptor) + public static WebEventData Parse(WebEventDescriptor eventDescriptor, string eventArgsJson) { - // The incoming field value can be either a bool or a string, but since the .NET property - // type is 'object', it will deserialize initially as a JsonElement - var fieldInfo = eventDescriptor.EventFieldInfo; - if (fieldInfo != null) - { - if (fieldInfo.FieldValue is JsonElement attributeValueJsonElement) - { - switch (attributeValueJsonElement.ValueKind) - { - case JsonValueKind.True: - case JsonValueKind.False: - fieldInfo.FieldValue = attributeValueJsonElement.GetBoolean(); - break; - default: - fieldInfo.FieldValue = attributeValueJsonElement.GetString(); - break; - } - } - else - { - // Unanticipated value type. Ensure we don't do anything with it. - eventDescriptor.EventFieldInfo = null; - } - } + return new WebEventData( + eventDescriptor.BrowserRendererId, + eventDescriptor.EventHandlerId, + InterpretEventFieldInfo(eventDescriptor.EventFieldInfo), + ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson)); } + private WebEventData(int browserRendererId, ulong eventHandlerId, EventFieldInfo eventFieldInfo, EventArgs eventArgs) + { + BrowserRendererId = browserRendererId; + EventHandlerId = eventHandlerId; + EventFieldInfo = eventFieldInfo; + EventArgs = eventArgs; + } + + public int BrowserRendererId { get; } + + public ulong EventHandlerId { get; } + + public EventFieldInfo EventFieldInfo { get; } + + public EventArgs EventArgs { get; } + private static EventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson) { switch (eventArgsType) @@ -83,13 +73,40 @@ namespace Microsoft.AspNetCore.Components.Web case "wheel": return Deserialize(eventArgsJson); default: - throw new ArgumentException($"Unsupported value '{eventArgsType}'.", nameof(eventArgsType)); + throw new ArgumentException($"Unsupported value '{eventArgsType}'.", nameof(eventArgsType)); } } - private static T Deserialize(string eventArgsJson) + private static T Deserialize(string json) { - return JsonSerializer.Deserialize(eventArgsJson, JsonSerializerOptionsProvider.Options); + return JsonSerializer.Deserialize(json, JsonSerializerOptionsProvider.Options); + } + + private static EventFieldInfo InterpretEventFieldInfo(EventFieldInfo fieldInfo) + { + // The incoming field value can be either a bool or a string, but since the .NET property + // type is 'object', it will deserialize initially as a JsonElement + if (fieldInfo?.FieldValue is JsonElement attributeValueJsonElement) + { + switch (attributeValueJsonElement.ValueKind) + { + case JsonValueKind.True: + case JsonValueKind.False: + return new EventFieldInfo + { + ComponentId = fieldInfo.ComponentId, + FieldValue = attributeValueJsonElement.GetBoolean() + }; + default: + return new EventFieldInfo + { + ComponentId = fieldInfo.ComponentId, + FieldValue = attributeValueJsonElement.GetString() + }; + } + } + + return null; } private static ChangeEventArgs DeserializeChangeEventArgs(string eventArgsJson) @@ -113,31 +130,5 @@ namespace Microsoft.AspNetCore.Components.Web } return changeArgs; } - - /// - /// For framework use only. - /// - public class BrowserEventDescriptor - { - /// - /// For framework use only. - /// - public int BrowserRendererId { get; set; } - - /// - /// For framework use only. - /// - public ulong EventHandlerId { get; set; } - - /// - /// For framework use only. - /// - public string EventArgsType { get; set; } - - /// - /// For framework use only. - /// - public EventFieldInfo EventFieldInfo { get; set; } - } } } diff --git a/src/Components/Shared/test/AutoRenderComponent.cs b/src/Components/Shared/test/AutoRenderComponent.cs index c446034950..10cf5b9bf7 100644 --- a/src/Components/Shared/test/AutoRenderComponent.cs +++ b/src/Components/Shared/test/AutoRenderComponent.cs @@ -4,7 +4,7 @@ using System; using System.Runtime.ExceptionServices; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; using Xunit; namespace Microsoft.AspNetCore.Components.Test.Helpers diff --git a/src/Components/Shared/test/AutoRenderFragmentComponent.cs b/src/Components/Shared/test/AutoRenderFragmentComponent.cs index 272c33a61b..1526d36e57 100644 --- a/src/Components/Shared/test/AutoRenderFragmentComponent.cs +++ b/src/Components/Shared/test/AutoRenderFragmentComponent.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Test.Helpers { diff --git a/src/Components/Shared/test/TestRenderer.cs b/src/Components/Shared/test/TestRenderer.cs index d5ce07bf4b..135a5cbda2 100644 --- a/src/Components/Shared/test/TestRenderer.cs +++ b/src/Components/Shared/test/TestRenderer.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -48,6 +49,9 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers public new int AssignRootComponentId(IComponent component) => base.AssignRootComponentId(component); + public new ArrayRange GetCurrentRenderTreeFrames(int componentId) + => base.GetCurrentRenderTreeFrames(componentId); + public void RenderRootComponent(int componentId, ParameterView? parameters = default) { var task = Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters ?? ParameterView.Empty)); diff --git a/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj b/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj index f47ada3d4f..8e0a17ece0 100644 --- a/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj +++ b/src/Components/Web.JS/Microsoft.AspNetCore.Components.Web.JS.npmproj @@ -7,8 +7,18 @@ - - + + diff --git a/src/Components/Web.JS/dist/Release/blazor.server.js b/src/Components/Web.JS/dist/Release/blazor.server.js index a79de53cec..72813d4eb7 100644 --- a/src/Components/Web.JS/dist/Release/blazor.server.js +++ b/src/Components/Web.JS/dist/Release/blazor.server.js @@ -1,15 +1,15 @@ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=49)}([function(e,t,n){"use strict";var r;n.d(t,"a",function(){return r}),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"}(r||(r={}))},function(e,t,n){"use strict";n.d(t,"a",function(){return a}),n.d(t,"c",function(){return c}),n.d(t,"f",function(){return u}),n.d(t,"g",function(){return l}),n.d(t,"h",function(){return f}),n.d(t,"e",function(){return h}),n.d(t,"d",function(){return p}),n.d(t,"b",function(){return d});var r=n(0),o=n(6),i=function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function s(e){try{c(r.next(e))}catch(e){i(e)}}function a(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(s,a)}c((r=r.apply(e,t||[])).next())})},s=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&&this.subject.observers.splice(e,1),0===this.subject.observers.length&&this.subject.cancelCallback&&this.subject.cancelCallback().catch(function(e){})},e}(),d=function(){function e(e){this.minimumLogLevel=e,this.outputConsole=console}return e.prototype.log=function(e,t){if(e>=this.minimumLogLevel)switch(e){case r.a.Critical:case r.a.Error:this.outputConsole.error("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Warning:this.outputConsole.warn("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Information:this.outputConsole.info("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;default:this.outputConsole.log("["+(new Date).toISOString()+"] "+r.a[e]+": "+t)}},e}()},function(e,t,n){"use strict";n.r(t);var r,o,i=n(3),s=n(4),a=n(43),c=n(0),u=(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),l=function(e){function t(t){var n=e.call(this)||this;return n.logger=t,n}return u(t,e),t.prototype.send=function(e){var t=this;return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?new Promise(function(n,r){var o=new XMLHttpRequest;o.open(e.method,e.url,!0),o.withCredentials=!0,o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.setRequestHeader("Content-Type","text/plain;charset=UTF-8");var a=e.headers;a&&Object.keys(a).forEach(function(e){o.setRequestHeader(e,a[e])}),e.responseType&&(o.responseType=e.responseType),e.abortSignal&&(e.abortSignal.onabort=function(){o.abort(),r(new i.a)}),e.timeout&&(o.timeout=e.timeout),o.onload=function(){e.abortSignal&&(e.abortSignal.onabort=null),o.status>=200&&o.status<300?n(new s.b(o.status,o.statusText,o.response||o.responseText)):r(new i.b(o.statusText,o.status))},o.onerror=function(){t.logger.log(c.a.Warning,"Error from HTTP request. "+o.status+": "+o.statusText+"."),r(new i.b(o.statusText,o.status))},o.ontimeout=function(){t.logger.log(c.a.Warning,"Timeout from HTTP request."),r(new i.c)},o.send(e.content||"")}):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t}(s.a),f=function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),h=function(e){function t(t){var n=e.call(this)||this;return"undefined"!=typeof XMLHttpRequest?n.httpClient=new l(t):n.httpClient=new a.a(t),n}return f(t,e),t.prototype.send=function(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?this.httpClient.send(e):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t.prototype.getCookieString=function(e){return this.httpClient.getCookieString(e)},t}(s.a),p=n(44);!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"}(o||(o={}));var d,g=n(1),y=function(){function e(){this.observers=[]}return e.prototype.next=function(e){for(var t=0,n=this.observers;t0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]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]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]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]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]0?[2,Promise.reject(new Error("Unable to connect to the server with any of the available transports. "+i.join(" ")))]:[2,Promise.reject(new Error("None of the transports supported by the client are supported by the server."))]}})})},e.prototype.constructTransport=function(e){switch(e){case E.WebSockets:if(!this.options.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new A(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.WebSocket);case E.ServerSentEvents:if(!this.options.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new O(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.EventSource);case E.LongPolling:return new x(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1);default:throw new Error("Unknown transport: "+e+".")}},e.prototype.startTransport=function(e,t){var n=this;return this.transport.onreceive=this.onreceive,this.transport.onclose=function(e){return n.stopConnection(e)},this.transport.connect(e,t)},e.prototype.resolveTransportOrError=function(e,t,n){var r=E[e.transport];if(null==r)return this.logger.log(c.a.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(c.a.Debug,"Skipping transport '"+E[r]+"' because it was disabled by the client."),new Error("'"+E[r]+"' is disabled by the client.");if(!(e.transferFormats.map(function(e){return S[e]}).indexOf(n)>=0))return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it does not support the requested transfer format '"+S[n]+"'."),new Error("'"+E[r]+"' does not support "+S[n]+".");if(r===E.WebSockets&&!this.options.WebSocket||r===E.ServerSentEvents&&!this.options.EventSource)return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it is not supported in your environment.'"),new Error("'"+E[r]+"' is not supported in your environment.");this.logger.log(c.a.Debug,"Selecting transport '"+E[r]+"'.");try{return this.constructTransport(r)}catch(e){return e}},e.prototype.isITransport=function(e){return e&&"object"==typeof e&&"connect"in e},e.prototype.stopConnection=function(e){if(this.logger.log(c.a.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){if("Disconnecting"===this.connectionState&&this.stopPromiseResolver(),e?this.logger.log(c.a.Error,"Connection disconnected with error '"+e+"'."):this.logger.log(c.a.Information,"Connection disconnected."),this.connectionId=void 0,this.connectionState="Disconnected",this.onclose&&this.connectionStarted){this.connectionStarted=!1;try{this.onclose(e)}catch(t){this.logger.log(c.a.Error,"HttpConnection.onclose("+e+") threw error '"+t+"'.")}}}else this.logger.log(c.a.Warning,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection hasn't yet left the in the connecting state.");else this.logger.log(c.a.Debug,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection is already in the disconnected state.")},e.prototype.resolveUrl=function(e){if(0===e.lastIndexOf("https://",0)||0===e.lastIndexOf("http://",0))return e;if(!g.c.isBrowser||!window.document)throw new Error("Cannot resolve '"+e+"'.");var t=window.document.createElement("a");return t.href=e,this.logger.log(c.a.Information,"Normalizing '"+e+"' to '"+t.href+"'."),t.href},e.prototype.resolveNegotiateUrl=function(e){var t=e.indexOf("?"),n=e.substring(0,-1===t?e.length:t);return"/"!==n[n.length-1]&&(n+="/"),n+="negotiate",n+=-1===t?"":e.substring(t)},e}();var q=function(){function e(e){this.transport=e,this.buffer=[],this.executing=!0,this.sendBufferedData=new W,this.transportResult=new W,this.sendLoopPromise=this.sendLoop()}return e.prototype.send=function(e){return this.bufferData(e),this.transportResult||(this.transportResult=new W),this.transportResult.promise},e.prototype.stop=function(){return this.executing=!1,this.sendBufferedData.resolve(),this.sendLoopPromise},e.prototype.bufferData=function(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()},e.prototype.sendLoop=function(){return B(this,void 0,void 0,function(){var t,n,r;return j(this,function(o){switch(o.label){case 0:return[4,this.sendBufferedData.promise];case 1:if(o.sent(),!this.executing)return this.transportResult&&this.transportResult.reject("Connection stopped."),[3,6];this.sendBufferedData=new W,t=this.transportResult,this.transportResult=void 0,n="string"==typeof this.buffer[0]?this.buffer.join(""):e.concatBuffers(this.buffer),this.buffer.length=0,o.label=2;case 2:return o.trys.push([2,4,,5]),[4,this.transport.send(n)];case 3:return o.sent(),t.resolve(),[3,5];case 4:return r=o.sent(),t.reject(r),[3,5];case 5:return[3,0];case 6:return[2]}})})},e.concatBuffers=function(e){for(var t=e.map(function(e){return e.byteLength}).reduce(function(e,t){return e+t}),n=new Uint8Array(t),r=0,o=0,i=e;o0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]-1&&this.subject.observers.splice(e,1),0===this.subject.observers.length&&this.subject.cancelCallback&&this.subject.cancelCallback().catch(function(e){})},e}(),d=function(){function e(e){this.minimumLogLevel=e,this.outputConsole=console}return e.prototype.log=function(e,t){if(e>=this.minimumLogLevel)switch(e){case r.a.Critical:case r.a.Error:this.outputConsole.error("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Warning:this.outputConsole.warn("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;case r.a.Information:this.outputConsole.info("["+(new Date).toISOString()+"] "+r.a[e]+": "+t);break;default:this.outputConsole.log("["+(new Date).toISOString()+"] "+r.a[e]+": "+t)}},e}()},function(e,t,n){"use strict";n.r(t);var r,o,i=n(3),a=n(4),s=n(43),c=n(0),u=(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),l=function(e){function t(t){var n=e.call(this)||this;return n.logger=t,n}return u(t,e),t.prototype.send=function(e){var t=this;return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?new Promise(function(n,r){var o=new XMLHttpRequest;o.open(e.method,e.url,!0),o.withCredentials=!0,o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.setRequestHeader("Content-Type","text/plain;charset=UTF-8");var s=e.headers;s&&Object.keys(s).forEach(function(e){o.setRequestHeader(e,s[e])}),e.responseType&&(o.responseType=e.responseType),e.abortSignal&&(e.abortSignal.onabort=function(){o.abort(),r(new i.a)}),e.timeout&&(o.timeout=e.timeout),o.onload=function(){e.abortSignal&&(e.abortSignal.onabort=null),o.status>=200&&o.status<300?n(new a.b(o.status,o.statusText,o.response||o.responseText)):r(new i.b(o.statusText,o.status))},o.onerror=function(){t.logger.log(c.a.Warning,"Error from HTTP request. "+o.status+": "+o.statusText+"."),r(new i.b(o.statusText,o.status))},o.ontimeout=function(){t.logger.log(c.a.Warning,"Timeout from HTTP request."),r(new i.c)},o.send(e.content||"")}):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t}(a.a),f=function(){var e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])};return function(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}(),h=function(e){function t(t){var n=e.call(this)||this;return"undefined"!=typeof XMLHttpRequest?n.httpClient=new l(t):n.httpClient=new s.a(t),n}return f(t,e),t.prototype.send=function(e){return e.abortSignal&&e.abortSignal.aborted?Promise.reject(new i.a):e.method?e.url?this.httpClient.send(e):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))},t.prototype.getCookieString=function(e){return this.httpClient.getCookieString(e)},t}(a.a),p=n(44);!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"}(o||(o={}));var d,g=n(1),y=function(){function e(){this.observers=[]}return e.prototype.next=function(e){for(var t=0,n=this.observers;t0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0?[2,Promise.reject(new Error("Unable to connect to the server with any of the available transports. "+i.join(" ")))]:[2,Promise.reject(new Error("None of the transports supported by the client are supported by the server."))]}})})},e.prototype.constructTransport=function(e){switch(e){case E.WebSockets:if(!this.options.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new A(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.WebSocket);case E.ServerSentEvents:if(!this.options.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new O(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1,this.options.EventSource);case E.LongPolling:return new x(this.httpClient,this.accessTokenFactory,this.logger,this.options.logMessageContent||!1);default:throw new Error("Unknown transport: "+e+".")}},e.prototype.startTransport=function(e,t){var n=this;return this.transport.onreceive=this.onreceive,this.transport.onclose=function(e){return n.stopConnection(e)},this.transport.connect(e,t)},e.prototype.resolveTransportOrError=function(e,t,n){var r=E[e.transport];if(null==r)return this.logger.log(c.a.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(c.a.Debug,"Skipping transport '"+E[r]+"' because it was disabled by the client."),new Error("'"+E[r]+"' is disabled by the client.");if(!(e.transferFormats.map(function(e){return S[e]}).indexOf(n)>=0))return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it does not support the requested transfer format '"+S[n]+"'."),new Error("'"+E[r]+"' does not support "+S[n]+".");if(r===E.WebSockets&&!this.options.WebSocket||r===E.ServerSentEvents&&!this.options.EventSource)return this.logger.log(c.a.Debug,"Skipping transport '"+E[r]+"' because it is not supported in your environment.'"),new Error("'"+E[r]+"' is not supported in your environment.");this.logger.log(c.a.Debug,"Selecting transport '"+E[r]+"'.");try{return this.constructTransport(r)}catch(e){return e}},e.prototype.isITransport=function(e){return e&&"object"==typeof e&&"connect"in e},e.prototype.stopConnection=function(e){if(this.logger.log(c.a.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){if("Disconnecting"===this.connectionState&&this.stopPromiseResolver(),e?this.logger.log(c.a.Error,"Connection disconnected with error '"+e+"'."):this.logger.log(c.a.Information,"Connection disconnected."),this.connectionId=void 0,this.connectionState="Disconnected",this.onclose&&this.connectionStarted){this.connectionStarted=!1;try{this.onclose(e)}catch(t){this.logger.log(c.a.Error,"HttpConnection.onclose("+e+") threw error '"+t+"'.")}}}else this.logger.log(c.a.Warning,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection hasn't yet left the in the connecting state.");else this.logger.log(c.a.Debug,"Call to HttpConnection.stopConnection("+e+") was ignored because the connection is already in the disconnected state.")},e.prototype.resolveUrl=function(e){if(0===e.lastIndexOf("https://",0)||0===e.lastIndexOf("http://",0))return e;if(!g.c.isBrowser||!window.document)throw new Error("Cannot resolve '"+e+"'.");var t=window.document.createElement("a");return t.href=e,this.logger.log(c.a.Information,"Normalizing '"+e+"' to '"+t.href+"'."),t.href},e.prototype.resolveNegotiateUrl=function(e){var t=e.indexOf("?"),n=e.substring(0,-1===t?e.length:t);return"/"!==n[n.length-1]&&(n+="/"),n+="negotiate",n+=-1===t?"":e.substring(t)},e}();var q=function(){function e(e){this.transport=e,this.buffer=[],this.executing=!0,this.sendBufferedData=new W,this.transportResult=new W,this.sendLoopPromise=this.sendLoop()}return e.prototype.send=function(e){return this.bufferData(e),this.transportResult||(this.transportResult=new W),this.transportResult.promise},e.prototype.stop=function(){return this.executing=!1,this.sendBufferedData.resolve(),this.sendLoopPromise},e.prototype.bufferData=function(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()},e.prototype.sendLoop=function(){return B(this,void 0,void 0,function(){var t,n,r;return j(this,function(o){switch(o.label){case 0:return[4,this.sendBufferedData.promise];case 1:if(o.sent(),!this.executing)return this.transportResult&&this.transportResult.reject("Connection stopped."),[3,6];this.sendBufferedData=new W,t=this.transportResult,this.transportResult=void 0,n="string"==typeof this.buffer[0]?this.buffer.join(""):e.concatBuffers(this.buffer),this.buffer.length=0,o.label=2;case 2:return o.trys.push([2,4,,5]),[4,this.transport.send(n)];case 3:return o.sent(),t.resolve(),[3,5];case 4:return r=o.sent(),t.reject(r),[3,5];case 5:return[3,0];case 6:return[2]}})})},e.concatBuffers=function(e){for(var t=e.map(function(e){return e.byteLength}).reduce(function(e,t){return e+t}),n=new Uint8Array(t),r=0,o=0,i=e;o * @license MIT */ -var r=n(50),o=n(51),i=n(52);function s(){return c.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function a(e,t){if(s()=s())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+s().toString(16)+" bytes");return 0|e}function d(e,t){if(c.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return F(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return H(e).length;default:if(r)return F(e).length;t=(""+t).toLowerCase(),r=!0}}function g(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function y(e,t,n,r,o){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=o?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(o)return-1;n=e.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof t&&(t=c.from(t,r)),c.isBuffer(t))return 0===t.length?-1:v(e,t,n,r,o);if("number"==typeof t)return t&=255,c.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):v(e,[t],n,r,o);throw new TypeError("val must be string, number or Buffer")}function v(e,t,n,r,o){var i,s=1,a=e.length,c=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;s=2,a/=2,c/=2,n/=2}function u(e,t){return 1===s?e[t]:e.readUInt16BE(t*s)}if(o){var l=-1;for(i=n;ia&&(n=a-c),i=n;i>=0;i--){for(var f=!0,h=0;ho&&(r=o):r=o;var i=t.length;if(i%2!=0)throw new TypeError("Invalid hex string");r>i/2&&(r=i/2);for(var s=0;s>8,o=n%256,i.push(o),i.push(r);return i}(t,e.length-n),e,n,r)}function _(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function I(e,t,n){n=Math.min(e.length,n);for(var r=[],o=t;o239?4:u>223?3:u>191?2:1;if(o+f<=n)switch(f){case 1:u<128&&(l=u);break;case 2:128==(192&(i=e[o+1]))&&(c=(31&u)<<6|63&i)>127&&(l=c);break;case 3:i=e[o+1],s=e[o+2],128==(192&i)&&128==(192&s)&&(c=(15&u)<<12|(63&i)<<6|63&s)>2047&&(c<55296||c>57343)&&(l=c);break;case 4:i=e[o+1],s=e[o+2],a=e[o+3],128==(192&i)&&128==(192&s)&&128==(192&a)&&(c=(15&u)<<18|(63&i)<<12|(63&s)<<6|63&a)>65535&&c<1114112&&(l=c)}null===l?(l=65533,f=1):l>65535&&(l-=65536,r.push(l>>>10&1023|55296),l=56320|1023&l),r.push(l),o+=f}return function(e){var t=e.length;if(t<=T)return String.fromCharCode.apply(String,e);var n="",r=0;for(;rthis.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return x(this,t,n);case"utf8":case"utf-8":return I(this,t,n);case"ascii":return k(this,t,n);case"latin1":case"binary":return P(this,t,n);case"base64":return _(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return R(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}.apply(this,arguments)},c.prototype.equals=function(e){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e||0===c.compare(this,e)},c.prototype.inspect=function(){var e="",n=t.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},c.prototype.compare=function(e,t,n,r,o){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===o&&(o=this.length),t<0||n>e.length||r<0||o>this.length)throw new RangeError("out of range index");if(r>=o&&t>=n)return 0;if(r>=o)return-1;if(t>=n)return 1;if(this===e)return 0;for(var i=(o>>>=0)-(r>>>=0),s=(n>>>=0)-(t>>>=0),a=Math.min(i,s),u=this.slice(r,o),l=e.slice(t,n),f=0;fo)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var i=!1;;)switch(r){case"hex":return b(this,e,t,n);case"utf8":case"utf-8":return m(this,e,t,n);case"ascii":return w(this,e,t,n);case"latin1":case"binary":return E(this,e,t,n);case"base64":return S(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return C(this,e,t,n);default:if(i)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),i=!0}},c.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var T=4096;function k(e,t,n){var r="";n=Math.min(e.length,n);for(var o=t;or)&&(n=r);for(var o="",i=t;in)throw new RangeError("Trying to access beyond buffer length")}function O(e,t,n,r,o,i){if(!c.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function M(e,t,n,r){t<0&&(t=65535+t+1);for(var o=0,i=Math.min(e.length-n,2);o>>8*(r?o:1-o)}function L(e,t,n,r){t<0&&(t=4294967295+t+1);for(var o=0,i=Math.min(e.length-n,4);o>>8*(r?o:3-o)&255}function A(e,t,n,r,o,i){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function B(e,t,n,r,i){return i||A(e,0,n,4),o.write(e,t,n,r,23,4),n+4}function j(e,t,n,r,i){return i||A(e,0,n,8),o.write(e,t,n,r,52,8),n+8}c.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t0&&(o*=256);)r+=this[e+--t]*o;return r},c.prototype.readUInt8=function(e,t){return t||D(e,1,this.length),this[e]},c.prototype.readUInt16LE=function(e,t){return t||D(e,2,this.length),this[e]|this[e+1]<<8},c.prototype.readUInt16BE=function(e,t){return t||D(e,2,this.length),this[e]<<8|this[e+1]},c.prototype.readUInt32LE=function(e,t){return t||D(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},c.prototype.readUInt32BE=function(e,t){return t||D(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},c.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||D(e,t,this.length);for(var r=this[e],o=1,i=0;++i=(o*=128)&&(r-=Math.pow(2,8*t)),r},c.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||D(e,t,this.length);for(var r=t,o=1,i=this[e+--r];r>0&&(o*=256);)i+=this[e+--r]*o;return i>=(o*=128)&&(i-=Math.pow(2,8*t)),i},c.prototype.readInt8=function(e,t){return t||D(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},c.prototype.readInt16LE=function(e,t){t||D(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt16BE=function(e,t){t||D(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt32LE=function(e,t){return t||D(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},c.prototype.readInt32BE=function(e,t){return t||D(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},c.prototype.readFloatLE=function(e,t){return t||D(e,4,this.length),o.read(this,e,!0,23,4)},c.prototype.readFloatBE=function(e,t){return t||D(e,4,this.length),o.read(this,e,!1,23,4)},c.prototype.readDoubleLE=function(e,t){return t||D(e,8,this.length),o.read(this,e,!0,52,8)},c.prototype.readDoubleBE=function(e,t){return t||D(e,8,this.length),o.read(this,e,!1,52,8)},c.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||O(this,e,t,n,Math.pow(2,8*n)-1,0);var o=1,i=0;for(this[t]=255&e;++i=0&&(i*=256);)this[t+o]=e/i&255;return t+n},c.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,1,255,0),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},c.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):M(this,e,t,!0),t+2},c.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):M(this,e,t,!1),t+2},c.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):L(this,e,t,!0),t+4},c.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):L(this,e,t,!1),t+4},c.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);O(this,e,t,n,o-1,-o)}var i=0,s=1,a=0;for(this[t]=255&e;++i>0)-a&255;return t+n},c.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);O(this,e,t,n,o-1,-o)}var i=n-1,s=1,a=0;for(this[t+i]=255&e;--i>=0&&(s*=256);)e<0&&0===a&&0!==this[t+i+1]&&(a=1),this[t+i]=(e/s>>0)-a&255;return t+n},c.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,1,127,-128),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},c.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):M(this,e,t,!0),t+2},c.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):M(this,e,t,!1),t+2},c.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,2147483647,-2147483648),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):L(this,e,t,!0),t+4},c.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):L(this,e,t,!1),t+4},c.prototype.writeFloatLE=function(e,t,n){return B(this,e,t,!0,n)},c.prototype.writeFloatBE=function(e,t,n){return B(this,e,t,!1,n)},c.prototype.writeDoubleLE=function(e,t,n){return j(this,e,t,!0,n)},c.prototype.writeDoubleBE=function(e,t,n){return j(this,e,t,!1,n)},c.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--o)e[o+t]=this[o+n];else if(i<1e3||!c.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(i=t;i55295&&n<57344){if(!o){if(n>56319){(t-=3)>-1&&i.push(239,191,189);continue}if(s+1===r){(t-=3)>-1&&i.push(239,191,189);continue}o=n;continue}if(n<56320){(t-=3)>-1&&i.push(239,191,189),o=n;continue}n=65536+(o-55296<<10|n-56320)}else o&&(t-=3)>-1&&i.push(239,191,189);if(o=null,n<128){if((t-=1)<0)break;i.push(n)}else if(n<2048){if((t-=2)<0)break;i.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;i.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;i.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return i}function H(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(U,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function q(e,t,n,r){for(var o=0;o=t.length||o>=e.length);++o)t[o+n]=e[o];return o}}).call(this,n(10))},function(e,t,n){"use strict";n.d(t,"a",function(){return r});var r=function(){function e(){}return e.prototype.log=function(e,t){},e.instance=new e,e}()},function(e,t,n){"use strict";n.d(t,"a",function(){return r});var r=function(){function e(){}return e.write=function(t){return""+t+e.RecordSeparator},e.parse=function(t){if(t[t.length-1]!==e.RecordSeparator)throw new Error("Message is incomplete.");var n=t.split(e.RecordSeparator);return n.pop(),n},e.RecordSeparatorCode=30,e.RecordSeparator=String.fromCharCode(e.RecordSeparatorCode),e}()},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(25),n(9);var r=n(26),o=n(16),i={},s=!1;function a(e,t,n){var o=i[e];o||(o=i[e]=new r.BrowserRenderer(e)),o.attachRootComponentToLogicalElement(n,t)}t.attachRootComponentToLogicalElement=a,t.attachRootComponentToElement=function(e,t,n){var r=document.querySelector(t);if(!r)throw new Error("Could not find any element matching selector '"+t+"'.");a(e,o.toLogicalElement(r,!0),n)},t.renderBatch=function(e,t){var n=i[e];if(!n)throw new Error("There is no browser renderer with ID "+e+".");for(var r=t.arrayRangeReader,o=t.updatedComponents(),a=r.values(o),c=r.count(o),u=t.referenceFrames(),l=r.values(u),f=t.diffReader,h=0;h=0,"must have a non-negative type"),o(s,"must have a decode function"),this.registerEncoder(function(e){return e instanceof t},function(t){var o=i(),s=r.allocUnsafe(1);return s.writeInt8(e,0),o.append(s),o.append(n(t)),o}),this.registerDecoder(e,s),this},registerEncoder:function(e,n){return o(e,"must have an encode function"),o(n,"must have an encode function"),t.push({check:e,encode:n}),this},registerDecoder:function(e,t){return o(e>=0,"must have a non-negative type"),o(t,"must have a decode function"),n.push({type:e,decode:t}),this},encoder:s.encoder,decoder:s.decoder,buffer:!0,type:"msgpack5",IncompleteBufferError:a.IncompleteBufferError}}},function(e,t,n){var r=n(5),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function s(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=s),i(o,s),s.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},s.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},s.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},s.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,i=null;function s(e){t.push(e)}function a(e,t){for(var n=[],r=2;r0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function a(e,t,n){var i=e;if(e instanceof Comment&&(u(i)&&u(i).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(c(i))throw new Error("Not implemented: moving existing logical children");var s=u(t);if(n0;)e(r,0);var i=r;i.parentNode.removeChild(i)},t.getLogicalParent=c,t.getLogicalSiblingEnd=function(e){return e[i]||null},t.getLogicalChild=function(e,t){return u(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===l(e).namespaceURI},t.getLogicalChildrenArray=u,t.permuteLogicalChildren=function(e,t){var n=u(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=c(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):h(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,i=r;i;){var s=i.nextSibling;if(n.insertBefore(i,t),i===o)break;i=s}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=l},function(e,t,n){"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),t.dispatchEvent=function(e,t){if(!r)throw new Error("eventDispatcher not initialized. Call 'setEventDispatcher' to configure it.");return r(e,t)},t.setEventDispatcher=function(e){r=e}},function(e,t){var n,r,o=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function s(){throw new Error("clearTimeout has not been defined")}function a(e){if(n===setTimeout)return setTimeout(e,0);if((n===i||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:i}catch(e){n=i}try{r="function"==typeof clearTimeout?clearTimeout:s}catch(e){r=s}}();var c,u=[],l=!1,f=-1;function h(){l&&c&&(l=!1,c.length?u=c.concat(u):f=-1,u.length&&p())}function p(){if(!l){var e=a(h);l=!0;for(var t=u.length;t;){for(c=u,u=[];++f1)for(var n=1;n0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]this.length)&&(r=this.length),n>=this.length)return e||i.alloc(0);if(r<=0)return e||i.alloc(0);var o,s,a=!!e,c=this._offset(n),u=r-n,l=u,f=a&&t||0,h=c[1];if(0===n&&r==this.length){if(!a)return 1===this._bufs.length?this._bufs[0]:i.concat(this._bufs,this.length);for(s=0;s(o=this._bufs[s].length-h))){this._bufs[s].copy(e,f,h,h+l);break}this._bufs[s].copy(e,f,h),f+=o,l-=o,h&&(h=0)}return e},s.prototype.shallowSlice=function(e,t){e=e||0,t=t||this.length,e<0&&(e+=this.length),t<0&&(t+=this.length);var n=this._offset(e),r=this._offset(t),o=this._bufs.slice(n[0],r[0]+1);return 0==r[1]?o.pop():o[o.length-1]=o[o.length-1].slice(0,r[1]),0!=n[1]&&(o[0]=o[0].slice(n[1])),new s(o)},s.prototype.toString=function(e,t,n){return this.slice(t,n).toString(e)},s.prototype.consume=function(e){for(;this._bufs.length;){if(!(e>=this._bufs[0].length)){this._bufs[0]=this._bufs[0].slice(e),this.length-=e;break}e-=this._bufs[0].length,this.length-=this._bufs[0].length,this._bufs.shift()}return this},s.prototype.duplicate=function(){for(var e=0,t=new s;e0&&o[o.length-1])&&(6===i[0]||2===i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]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]=i)return e;switch(e){case"%s":return String(r[n++]);case"%d":return Number(r[n++]);case"%j":try{return JSON.stringify(r[n++])}catch(e){return"[Circular]"}default:return e}}),c=r[n];n=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),d(n)?r.showHidden=n:n&&t._extend(r,n),b(r.showHidden)&&(r.showHidden=!1),b(r.depth)&&(r.depth=2),b(r.colors)&&(r.colors=!1),b(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=c),l(r,e,r.depth)}function c(e,t){var n=a.styles[t];return n?"["+a.colors[n][0]+"m"+e+"["+a.colors[n][1]+"m":e}function u(e,t){return e}function l(e,n,r){if(e.customInspect&&n&&C(n.inspect)&&n.inspect!==t.inspect&&(!n.constructor||n.constructor.prototype!==n)){var o=n.inspect(r,e);return v(o)||(o=l(e,o,r)),o}var i=function(e,t){if(b(t))return e.stylize("undefined","undefined");if(v(t)){var n="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(n,"string")}if(y(t))return e.stylize(""+t,"number");if(d(t))return e.stylize(""+t,"boolean");if(g(t))return e.stylize("null","null")}(e,n);if(i)return i;var s=Object.keys(n),a=function(e){var t={};return e.forEach(function(e,n){t[e]=!0}),t}(s);if(e.showHidden&&(s=Object.getOwnPropertyNames(n)),S(n)&&(s.indexOf("message")>=0||s.indexOf("description")>=0))return f(n);if(0===s.length){if(C(n)){var c=n.name?": "+n.name:"";return e.stylize("[Function"+c+"]","special")}if(m(n))return e.stylize(RegExp.prototype.toString.call(n),"regexp");if(E(n))return e.stylize(Date.prototype.toString.call(n),"date");if(S(n))return f(n)}var u,w="",_=!1,I=["{","}"];(p(n)&&(_=!0,I=["[","]"]),C(n))&&(w=" [Function"+(n.name?": "+n.name:"")+"]");return m(n)&&(w=" "+RegExp.prototype.toString.call(n)),E(n)&&(w=" "+Date.prototype.toUTCString.call(n)),S(n)&&(w=" "+f(n)),0!==s.length||_&&0!=n.length?r<0?m(n)?e.stylize(RegExp.prototype.toString.call(n),"regexp"):e.stylize("[Object]","special"):(e.seen.push(n),u=_?function(e,t,n,r,o){for(var i=[],s=0,a=t.length;s=0&&0,e+t.replace(/\u001b\[\d\d?m/g,"").length+1},0)>60)return n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1];return n[0]+t+" "+e.join(", ")+" "+n[1]}(u,w,I)):I[0]+w+I[1]}function f(e){return"["+Error.prototype.toString.call(e)+"]"}function h(e,t,n,r,o,i){var s,a,c;if((c=Object.getOwnPropertyDescriptor(t,o)||{value:t[o]}).get?a=c.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):c.set&&(a=e.stylize("[Setter]","special")),k(r,o)||(s="["+o+"]"),a||(e.seen.indexOf(c.value)<0?(a=g(n)?l(e,c.value,null):l(e,c.value,n-1)).indexOf("\n")>-1&&(a=i?a.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+a.split("\n").map(function(e){return" "+e}).join("\n")):a=e.stylize("[Circular]","special")),b(s)){if(i&&o.match(/^\d+$/))return a;(s=JSON.stringify(""+o)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(s=s.substr(1,s.length-2),s=e.stylize(s,"name")):(s=s.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),s=e.stylize(s,"string"))}return s+": "+a}function p(e){return Array.isArray(e)}function d(e){return"boolean"==typeof e}function g(e){return null===e}function y(e){return"number"==typeof e}function v(e){return"string"==typeof e}function b(e){return void 0===e}function m(e){return w(e)&&"[object RegExp]"===_(e)}function w(e){return"object"==typeof e&&null!==e}function E(e){return w(e)&&"[object Date]"===_(e)}function S(e){return w(e)&&("[object Error]"===_(e)||e instanceof Error)}function C(e){return"function"==typeof e}function _(e){return Object.prototype.toString.call(e)}function I(e){return e<10?"0"+e.toString(10):e.toString(10)}t.debuglog=function(n){if(b(i)&&(i=e.env.NODE_DEBUG||""),n=n.toUpperCase(),!s[n])if(new RegExp("\\b"+n+"\\b","i").test(i)){var r=e.pid;s[n]=function(){var e=t.format.apply(t,arguments);console.error("%s %d: %s",n,r,e)}}else s[n]=function(){};return s[n]},t.inspect=a,a.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},a.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.isArray=p,t.isBoolean=d,t.isNull=g,t.isNullOrUndefined=function(e){return null==e},t.isNumber=y,t.isString=v,t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=b,t.isRegExp=m,t.isObject=w,t.isDate=E,t.isError=S,t.isFunction=C,t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=n(54);var T=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function k(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.log=function(){var e,n;console.log("%s - %s",(e=new Date,n=[I(e.getHours()),I(e.getMinutes()),I(e.getSeconds())].join(":"),[e.getDate(),T[e.getMonth()],n].join(" ")),t.format.apply(t,arguments))},t.inherits=n(55),t._extend=function(e,t){if(!t||!w(t))return e;for(var n=Object.keys(t),r=n.length;r--;)e[n[r]]=t[n[r]];return e};var P="undefined"!=typeof Symbol?Symbol("util.promisify.custom"):void 0;function x(e,t){if(!e){var n=new Error("Promise was rejected with a falsy value");n.reason=e,e=n}return t(e)}t.promisify=function(e){if("function"!=typeof e)throw new TypeError('The "original" argument must be of type Function');if(P&&e[P]){var t;if("function"!=typeof(t=e[P]))throw new TypeError('The "util.promisify.custom" argument must be of type Function');return Object.defineProperty(t,P,{value:t,enumerable:!1,writable:!1,configurable:!0}),t}function t(){for(var t,n,r=new Promise(function(e,r){t=e,n=r}),o=[],i=0;i0?("string"==typeof t||s.objectMode||Object.getPrototypeOf(t)===u.prototype||(t=function(e){return u.from(e)}(t)),r?s.endEmitted?e.emit("error",new Error("stream.unshift() after end event")):E(e,s,t,!0):s.ended?e.emit("error",new Error("stream.push() after EOF")):(s.reading=!1,s.decoder&&!n?(t=s.decoder.write(t),s.objectMode||0!==t.length?E(e,s,t,!1):T(e,s)):E(e,s,t,!1))):r||(s.reading=!1));return function(e){return!e.ended&&(e.needReadable||e.lengtht.highWaterMark&&(t.highWaterMark=function(e){return e>=S?e=S:(e--,e|=e>>>1,e|=e>>>2,e|=e>>>4,e|=e>>>8,e|=e>>>16,e++),e}(e)),e<=t.length?e:t.ended?t.length:(t.needReadable=!0,0))}function _(e){var t=e._readableState;t.needReadable=!1,t.emittedReadable||(p("emitReadable",t.flowing),t.emittedReadable=!0,t.sync?o.nextTick(I,e):I(e))}function I(e){p("emit readable"),e.emit("readable"),R(e)}function T(e,t){t.readingMore||(t.readingMore=!0,o.nextTick(k,e,t))}function k(e,t){for(var n=t.length;!t.reading&&!t.flowing&&!t.ended&&t.length=t.length?(n=t.decoder?t.buffer.join(""):1===t.buffer.length?t.buffer.head.data:t.buffer.concat(t.length),t.buffer.clear()):n=function(e,t,n){var r;ei.length?i.length:e;if(s===i.length?o+=i:o+=i.slice(0,e),0===(e-=s)){s===i.length?(++r,n.next?t.head=n.next:t.head=t.tail=null):(t.head=n,n.data=i.slice(s));break}++r}return t.length-=r,o}(e,t):function(e,t){var n=u.allocUnsafe(e),r=t.head,o=1;r.data.copy(n),e-=r.data.length;for(;r=r.next;){var i=r.data,s=e>i.length?i.length:e;if(i.copy(n,n.length-e,0,s),0===(e-=s)){s===i.length?(++o,r.next?t.head=r.next:t.head=t.tail=null):(t.head=r,r.data=i.slice(s));break}++o}return t.length-=o,n}(e,t);return r}(e,t.buffer,t.decoder),n);var n}function O(e){var t=e._readableState;if(t.length>0)throw new Error('"endReadable()" called on non-empty stream');t.endEmitted||(t.ended=!0,o.nextTick(M,t,e))}function M(e,t){e.endEmitted||0!==e.length||(e.endEmitted=!0,t.readable=!1,t.emit("end"))}function L(e,t){for(var n=0,r=e.length;n=t.highWaterMark||t.ended))return p("read: emitReadable",t.length,t.ended),0===t.length&&t.ended?O(this):_(this),null;if(0===(e=C(e,t))&&t.ended)return 0===t.length&&O(this),null;var r,o=t.needReadable;return p("need readable",o),(0===t.length||t.length-e0?D(e,t):null)?(t.needReadable=!0,e=0):t.length-=e,0===t.length&&(t.ended||(t.needReadable=!0),n!==e&&t.ended&&O(this)),null!==r&&this.emit("data",r),r},m.prototype._read=function(e){this.emit("error",new Error("_read() is not implemented"))},m.prototype.pipe=function(e,t){var n=this,i=this._readableState;switch(i.pipesCount){case 0:i.pipes=e;break;case 1:i.pipes=[i.pipes,e];break;default:i.pipes.push(e)}i.pipesCount+=1,p("pipe count=%d opts=%j",i.pipesCount,t);var c=(!t||!1!==t.end)&&e!==r.stdout&&e!==r.stderr?l:m;function u(t,r){p("onunpipe"),t===n&&r&&!1===r.hasUnpiped&&(r.hasUnpiped=!0,p("cleanup"),e.removeListener("close",v),e.removeListener("finish",b),e.removeListener("drain",f),e.removeListener("error",y),e.removeListener("unpipe",u),n.removeListener("end",l),n.removeListener("end",m),n.removeListener("data",g),h=!0,!i.awaitDrain||e._writableState&&!e._writableState.needDrain||f())}function l(){p("onend"),e.end()}i.endEmitted?o.nextTick(c):n.once("end",c),e.on("unpipe",u);var f=function(e){return function(){var t=e._readableState;p("pipeOnDrain",t.awaitDrain),t.awaitDrain&&t.awaitDrain--,0===t.awaitDrain&&a(e,"data")&&(t.flowing=!0,R(e))}}(n);e.on("drain",f);var h=!1;var d=!1;function g(t){p("ondata"),d=!1,!1!==e.write(t)||d||((1===i.pipesCount&&i.pipes===e||i.pipesCount>1&&-1!==L(i.pipes,e))&&!h&&(p("false write response, pause",n._readableState.awaitDrain),n._readableState.awaitDrain++,d=!0),n.pause())}function y(t){p("onerror",t),m(),e.removeListener("error",y),0===a(e,"error")&&e.emit("error",t)}function v(){e.removeListener("finish",b),m()}function b(){p("onfinish"),e.removeListener("close",v),m()}function m(){p("unpipe"),n.unpipe(e)}return n.on("data",g),function(e,t,n){if("function"==typeof e.prependListener)return e.prependListener(t,n);e._events&&e._events[t]?s(e._events[t])?e._events[t].unshift(n):e._events[t]=[n,e._events[t]]:e.on(t,n)}(e,"error",y),e.once("close",v),e.once("finish",b),e.emit("pipe",n),i.flowing||(p("pipe resume"),n.resume()),e},m.prototype.unpipe=function(e){var t=this._readableState,n={hasUnpiped:!1};if(0===t.pipesCount)return this;if(1===t.pipesCount)return e&&e!==t.pipes?this:(e||(e=t.pipes),t.pipes=null,t.pipesCount=0,t.flowing=!1,e&&e.emit("unpipe",this,n),this);if(!e){var r=t.pipes,o=t.pipesCount;t.pipes=null,t.pipesCount=0,t.flowing=!1;for(var i=0;i0&&s.length>o&&!s.warned){s.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+s.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=e,c.type=t,c.count=s.length,a=c,console&&console.warn&&console.warn(a)}return e}function f(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},o=function(){for(var e=[],t=0;t0&&(s=t[0]),s instanceof Error)throw s;var a=new Error("Unhandled error."+(s?" ("+s.message+")":""));throw a.context=s,a}var c=o[e];if(void 0===c)return!1;if("function"==typeof c)i(c,this,t);else{var u=c.length,l=d(c,u);for(n=0;n=0;i--)if(n[i]===t||n[i].listener===t){s=n[i].listener,o=i;break}if(o<0)return this;0===o?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},a.prototype.listeners=function(e){return h(this,e,!0)},a.prototype.rawListeners=function(e){return h(this,e,!1)},a.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):p.call(e,t)},a.prototype.listenerCount=p,a.prototype.eventNames=function(){return this._eventsCount>0?r(this._events):[]}},function(e,t,n){e.exports=n(37).EventEmitter},function(e,t,n){"use strict";var r=n(23);function o(e,t){e.emit("error",t)}e.exports={destroy:function(e,t){var n=this,i=this._readableState&&this._readableState.destroyed,s=this._writableState&&this._writableState.destroyed;return i||s?(t?t(e):!e||this._writableState&&this._writableState.errorEmitted||r.nextTick(o,this,e),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(e||null,function(e){!t&&e?(r.nextTick(o,n,e),n._writableState&&(n._writableState.errorEmitted=!0)):t&&t(e)}),this)},undestroy:function(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)}}},function(e,t,n){"use strict";var r=n(61).Buffer,o=r.isEncoding||function(e){switch((e=""+e)&&e.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function i(e){var t;switch(this.encoding=function(e){var t=function(e){if(!e)return"utf8";for(var t;;)switch(e){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return e;default:if(t)return;e=(""+e).toLowerCase(),t=!0}}(e);if("string"!=typeof t&&(r.isEncoding===o||!o(e)))throw new Error("Unknown encoding: "+e);return t||e}(e),this.encoding){case"utf16le":this.text=c,this.end=u,t=4;break;case"utf8":this.fillLast=a,t=4;break;case"base64":this.text=l,this.end=f,t=3;break;default:return this.write=h,void(this.end=p)}this.lastNeed=0,this.lastTotal=0,this.lastChar=r.allocUnsafe(t)}function s(e){return e<=127?0:e>>5==6?2:e>>4==14?3:e>>3==30?4:e>>6==2?-1:-2}function a(e){var t=this.lastTotal-this.lastNeed,n=function(e,t,n){if(128!=(192&t[0]))return e.lastNeed=0,"�";if(e.lastNeed>1&&t.length>1){if(128!=(192&t[1]))return e.lastNeed=1,"�";if(e.lastNeed>2&&t.length>2&&128!=(192&t[2]))return e.lastNeed=2,"�"}}(this,e);return void 0!==n?n:this.lastNeed<=e.length?(e.copy(this.lastChar,t,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal)):(e.copy(this.lastChar,t,0,e.length),void(this.lastNeed-=e.length))}function c(e,t){if((e.length-t)%2==0){var n=e.toString("utf16le",t);if(n){var r=n.charCodeAt(n.length-1);if(r>=55296&&r<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1],n.slice(0,-1)}return n}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=e[e.length-1],e.toString("utf16le",t,e.length-1)}function u(e){var t=e&&e.length?this.write(e):"";if(this.lastNeed){var n=this.lastTotal-this.lastNeed;return t+this.lastChar.toString("utf16le",0,n)}return t}function l(e,t){var n=(e.length-t)%3;return 0===n?e.toString("base64",t):(this.lastNeed=3-n,this.lastTotal=3,1===n?this.lastChar[0]=e[e.length-1]:(this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1]),e.toString("base64",t,e.length-n))}function f(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+this.lastChar.toString("base64",0,3-this.lastNeed):t}function h(e){return e.toString(this.encoding)}function p(e){return e&&e.length?this.write(e):""}t.StringDecoder=i,i.prototype.write=function(e){if(0===e.length)return"";var t,n;if(this.lastNeed){if(void 0===(t=this.fillLast(e)))return"";n=this.lastNeed,this.lastNeed=0}else n=0;return n=0)return o>0&&(e.lastNeed=o-1),o;if(--r=0)return o>0&&(e.lastNeed=o-2),o;if(--r=0)return o>0&&(2===o?o=0:e.lastNeed=o-3),o;return 0}(this,e,t);if(!this.lastNeed)return e.toString("utf8",t);this.lastTotal=n;var r=e.length-(n-this.lastNeed);return e.copy(this.lastChar,0,r),e.toString("utf8",t,r)},i.prototype.fillLast=function(e){if(this.lastNeed<=e.length)return e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,e.length),this.lastNeed-=e.length}},function(e,t,n){"use strict";(function(t,r,o){var i=n(23);function s(e){var t=this;this.next=null,this.entry=null,this.finish=function(){!function(e,t,n){var r=e.entry;e.entry=null;for(;r;){var o=r.callback;t.pendingcb--,o(n),r=r.next}t.corkedRequestsFree?t.corkedRequestsFree.next=e:t.corkedRequestsFree=e}(t,e)}}e.exports=b;var a,c=!t.browser&&["v0.10","v0.9."].indexOf(t.version.slice(0,5))>-1?r:i.nextTick;b.WritableState=v;var u=n(19);u.inherits=n(14);var l={deprecate:n(64)},f=n(38),h=n(13).Buffer,p=o.Uint8Array||function(){};var d,g=n(39);function y(){}function v(e,t){a=a||n(11),e=e||{};var r=t instanceof a;this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.writableObjectMode);var o=e.highWaterMark,u=e.writableHighWaterMark,l=this.objectMode?16:16384;this.highWaterMark=o||0===o?o:r&&(u||0===u)?u:l,this.highWaterMark=Math.floor(this.highWaterMark),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var f=!1===e.decodeStrings;this.decodeStrings=!f,this.defaultEncoding=e.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(e){!function(e,t){var n=e._writableState,r=n.sync,o=n.writecb;if(function(e){e.writing=!1,e.writecb=null,e.length-=e.writelen,e.writelen=0}(n),t)!function(e,t,n,r,o){--t.pendingcb,n?(i.nextTick(o,r),i.nextTick(_,e,t),e._writableState.errorEmitted=!0,e.emit("error",r)):(o(r),e._writableState.errorEmitted=!0,e.emit("error",r),_(e,t))}(e,n,r,t,o);else{var s=S(n);s||n.corked||n.bufferProcessing||!n.bufferedRequest||E(e,n),r?c(w,e,n,s,o):w(e,n,s,o)}}(t,e)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.bufferedRequestCount=0,this.corkedRequestsFree=new s(this)}function b(e){if(a=a||n(11),!(d.call(b,this)||this instanceof a))return new b(e);this._writableState=new v(e,this),this.writable=!0,e&&("function"==typeof e.write&&(this._write=e.write),"function"==typeof e.writev&&(this._writev=e.writev),"function"==typeof e.destroy&&(this._destroy=e.destroy),"function"==typeof e.final&&(this._final=e.final)),f.call(this)}function m(e,t,n,r,o,i,s){t.writelen=r,t.writecb=s,t.writing=!0,t.sync=!0,n?e._writev(o,t.onwrite):e._write(o,i,t.onwrite),t.sync=!1}function w(e,t,n,r){n||function(e,t){0===t.length&&t.needDrain&&(t.needDrain=!1,e.emit("drain"))}(e,t),t.pendingcb--,r(),_(e,t)}function E(e,t){t.bufferProcessing=!0;var n=t.bufferedRequest;if(e._writev&&n&&n.next){var r=t.bufferedRequestCount,o=new Array(r),i=t.corkedRequestsFree;i.entry=n;for(var a=0,c=!0;n;)o[a]=n,n.isBuf||(c=!1),n=n.next,a+=1;o.allBuffers=c,m(e,t,!0,t.length,o,"",i.finish),t.pendingcb++,t.lastBufferedRequest=null,i.next?(t.corkedRequestsFree=i.next,i.next=null):t.corkedRequestsFree=new s(t),t.bufferedRequestCount=0}else{for(;n;){var u=n.chunk,l=n.encoding,f=n.callback;if(m(e,t,!1,t.objectMode?1:u.length,u,l,f),n=n.next,t.bufferedRequestCount--,t.writing)break}null===n&&(t.lastBufferedRequest=null)}t.bufferedRequest=n,t.bufferProcessing=!1}function S(e){return e.ending&&0===e.length&&null===e.bufferedRequest&&!e.finished&&!e.writing}function C(e,t){e._final(function(n){t.pendingcb--,n&&e.emit("error",n),t.prefinished=!0,e.emit("prefinish"),_(e,t)})}function _(e,t){var n=S(t);return n&&(!function(e,t){t.prefinished||t.finalCalled||("function"==typeof e._final?(t.pendingcb++,t.finalCalled=!0,i.nextTick(C,e,t)):(t.prefinished=!0,e.emit("prefinish")))}(e,t),0===t.pendingcb&&(t.finished=!0,e.emit("finish"))),n}u.inherits(b,f),v.prototype.getBuffer=function(){for(var e=this.bufferedRequest,t=[];e;)t.push(e),e=e.next;return t},function(){try{Object.defineProperty(v.prototype,"buffer",{get:l.deprecate(function(){return this.getBuffer()},"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch(e){}}(),"function"==typeof Symbol&&Symbol.hasInstance&&"function"==typeof Function.prototype[Symbol.hasInstance]?(d=Function.prototype[Symbol.hasInstance],Object.defineProperty(b,Symbol.hasInstance,{value:function(e){return!!d.call(this,e)||this===b&&(e&&e._writableState instanceof v)}})):d=function(e){return e instanceof this},b.prototype.pipe=function(){this.emit("error",new Error("Cannot pipe, not readable"))},b.prototype.write=function(e,t,n){var r,o=this._writableState,s=!1,a=!o.objectMode&&(r=e,h.isBuffer(r)||r instanceof p);return a&&!h.isBuffer(e)&&(e=function(e){return h.from(e)}(e)),"function"==typeof t&&(n=t,t=null),a?t="buffer":t||(t=o.defaultEncoding),"function"!=typeof n&&(n=y),o.ended?function(e,t){var n=new Error("write after end");e.emit("error",n),i.nextTick(t,n)}(this,n):(a||function(e,t,n,r){var o=!0,s=!1;return null===n?s=new TypeError("May not write null values to stream"):"string"==typeof n||void 0===n||t.objectMode||(s=new TypeError("Invalid non-string/buffer chunk")),s&&(e.emit("error",s),i.nextTick(r,s),o=!1),o}(this,o,e,n))&&(o.pendingcb++,s=function(e,t,n,r,o,i){if(!n){var s=function(e,t,n){e.objectMode||!1===e.decodeStrings||"string"!=typeof t||(t=h.from(t,n));return t}(t,r,o);r!==s&&(n=!0,o="buffer",r=s)}var a=t.objectMode?1:r.length;t.length+=a;var c=t.length-1))throw new TypeError("Unknown encoding: "+e);return this._writableState.defaultEncoding=e,this},Object.defineProperty(b.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),b.prototype._write=function(e,t,n){n(new Error("_write() is not implemented"))},b.prototype._writev=null,b.prototype.end=function(e,t,n){var r=this._writableState;"function"==typeof e?(n=e,e=null,t=null):"function"==typeof t&&(n=t,t=null),null!=e&&this.write(e,t),r.corked&&(r.corked=1,this.uncork()),r.ending||r.finished||function(e,t,n){t.ending=!0,_(e,t),n&&(t.finished?i.nextTick(n):e.once("finish",n));t.ended=!0,e.writable=!1}(this,r,n)},Object.defineProperty(b.prototype,"destroyed",{get:function(){return void 0!==this._writableState&&this._writableState.destroyed},set:function(e){this._writableState&&(this._writableState.destroyed=e)}}),b.prototype.destroy=g.destroy,b.prototype._undestroy=g.undestroy,b.prototype._destroy=function(e,t){this.end(),t(e)}}).call(this,n(18),n(62).setImmediate,n(10))},function(e,t,n){"use strict";e.exports=s;var r=n(11),o=n(19);function i(e,t){var n=this._transformState;n.transforming=!1;var r=n.writecb;if(!r)return this.emit("error",new Error("write callback called multiple times"));n.writechunk=null,n.writecb=null,null!=t&&this.push(t),r(e);var o=this._readableState;o.reading=!1,(o.needReadable||o.length=200&&c.statusCode<300?r(new s.b(c.statusCode,c.statusMessage||"",u)):o(new i.b(c.statusMessage||"",c.statusCode||0))});t.abortSignal&&(t.abortSignal.onabort=function(){f.abort(),o(new i.a)})})},n.prototype.getCookieString=function(e){return this.cookieJar.getCookieString(e)},n}(s.a)}).call(this,n(5).Buffer)},function(e,t,n){"use strict";(function(e){n.d(t,"a",function(){return i});var r=n(7),o=n(1),i=function(){function t(){}return t.prototype.writeHandshakeRequest=function(e){return r.a.write(JSON.stringify(e))},t.prototype.parseHandshakeResponse=function(t){var n,i;if(Object(o.g)(t)||void 0!==e&&t instanceof e){var s=new Uint8Array(t);if(-1===(c=s.indexOf(r.a.RecordSeparatorCode)))throw new Error("Message is incomplete.");var a=c+1;n=String.fromCharCode.apply(null,s.slice(0,a)),i=s.byteLength>a?s.slice(a).buffer:null}else{var c,u=t;if(-1===(c=u.indexOf(r.a.RecordSeparator)))throw new Error("Message is incomplete.");a=c+1;n=u.substring(0,a),i=u.length>a?u.substring(a):null}var l=r.a.parse(n),f=JSON.parse(l[0]);if(f.type)throw new Error("Expected a handshake response from the server.");return[i,f]},t}()}).call(this,n(5).Buffer)},,,,,function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function s(e){try{c(r.next(e))}catch(e){i(e)}}function a(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(s,a)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||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]0)&&!(r=i.next()).done;)s.push(r.value)}catch(e){o={error:e}}finally{try{r&&!r.done&&(n=i.return)&&n.call(i)}finally{if(o)throw o.error}}return s},s=this&&this.__spread||function(){for(var e=[],t=0;t0?r-4:r,f=0;f>16&255,a[c++]=t>>8&255,a[c++]=255&t;2===s&&(t=o[e.charCodeAt(f)]<<2|o[e.charCodeAt(f+1)]>>4,a[c++]=255&t);1===s&&(t=o[e.charCodeAt(f)]<<10|o[e.charCodeAt(f+1)]<<4|o[e.charCodeAt(f+2)]>>2,a[c++]=t>>8&255,a[c++]=255&t);return a},t.fromByteArray=function(e){for(var t,n=e.length,o=n%3,i=[],s=0,a=n-o;sa?a:s+16383));1===o?(t=e[n-1],i.push(r[t>>2]+r[t<<4&63]+"==")):2===o&&(t=(e[n-2]<<8)+e[n-1],i.push(r[t>>10]+r[t>>4&63]+r[t<<2&63]+"="));return i.join("")};for(var r=[],o=[],i="undefined"!=typeof Uint8Array?Uint8Array:Array,s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",a=0,c=s.length;a0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");return-1===n&&(n=t),[n,n===t?0:4-n%4]}function l(e,t,n){for(var o,i,s=[],a=t;a>18&63]+r[i>>12&63]+r[i>>6&63]+r[63&i]);return s.join("")}o["-".charCodeAt(0)]=62,o["_".charCodeAt(0)]=63},function(e,t){t.read=function(e,t,n,r,o){var i,s,a=8*o-r-1,c=(1<>1,l=-7,f=n?o-1:0,h=n?-1:1,p=e[t+f];for(f+=h,i=p&(1<<-l)-1,p>>=-l,l+=a;l>0;i=256*i+e[t+f],f+=h,l-=8);for(s=i&(1<<-l)-1,i>>=-l,l+=r;l>0;s=256*s+e[t+f],f+=h,l-=8);if(0===i)i=1-u;else{if(i===c)return s?NaN:1/0*(p?-1:1);s+=Math.pow(2,r),i-=u}return(p?-1:1)*s*Math.pow(2,i-r)},t.write=function(e,t,n,r,o,i){var s,a,c,u=8*i-o-1,l=(1<>1,h=23===o?Math.pow(2,-24)-Math.pow(2,-77):0,p=r?0:i-1,d=r?1:-1,g=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(a=isNaN(t)?1:0,s=l):(s=Math.floor(Math.log(t)/Math.LN2),t*(c=Math.pow(2,-s))<1&&(s--,c*=2),(t+=s+f>=1?h/c:h*Math.pow(2,1-f))*c>=2&&(s++,c/=2),s+f>=l?(a=0,s=l):s+f>=1?(a=(t*c-1)*Math.pow(2,o),s+=f):(a=t*Math.pow(2,f-1)*Math.pow(2,o),s=0));o>=8;e[n+p]=255&a,p+=d,a/=256,o-=8);for(s=s<0;e[n+p]=255&s,p+=d,s/=256,u-=8);e[n+p-d]|=128*g}},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t,n){"use strict";(function(t){ +var r=n(50),o=n(51),i=n(52);function a(){return c.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function s(e,t){if(a()=a())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a().toString(16)+" bytes");return 0|e}function d(e,t){if(c.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return F(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return H(e).length;default:if(r)return F(e).length;t=(""+t).toLowerCase(),r=!0}}function g(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function y(e,t,n,r,o){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=o?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(o)return-1;n=e.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof t&&(t=c.from(t,r)),c.isBuffer(t))return 0===t.length?-1:v(e,t,n,r,o);if("number"==typeof t)return t&=255,c.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):v(e,[t],n,r,o);throw new TypeError("val must be string, number or Buffer")}function v(e,t,n,r,o){var i,a=1,s=e.length,c=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;a=2,s/=2,c/=2,n/=2}function u(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(o){var l=-1;for(i=n;is&&(n=s-c),i=n;i>=0;i--){for(var f=!0,h=0;ho&&(r=o):r=o;var i=t.length;if(i%2!=0)throw new TypeError("Invalid hex string");r>i/2&&(r=i/2);for(var a=0;a>8,o=n%256,i.push(o),i.push(r);return i}(t,e.length-n),e,n,r)}function _(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function I(e,t,n){n=Math.min(e.length,n);for(var r=[],o=t;o239?4:u>223?3:u>191?2:1;if(o+f<=n)switch(f){case 1:u<128&&(l=u);break;case 2:128==(192&(i=e[o+1]))&&(c=(31&u)<<6|63&i)>127&&(l=c);break;case 3:i=e[o+1],a=e[o+2],128==(192&i)&&128==(192&a)&&(c=(15&u)<<12|(63&i)<<6|63&a)>2047&&(c<55296||c>57343)&&(l=c);break;case 4:i=e[o+1],a=e[o+2],s=e[o+3],128==(192&i)&&128==(192&a)&&128==(192&s)&&(c=(15&u)<<18|(63&i)<<12|(63&a)<<6|63&s)>65535&&c<1114112&&(l=c)}null===l?(l=65533,f=1):l>65535&&(l-=65536,r.push(l>>>10&1023|55296),l=56320|1023&l),r.push(l),o+=f}return function(e){var t=e.length;if(t<=T)return String.fromCharCode.apply(String,e);var n="",r=0;for(;rthis.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return x(this,t,n);case"utf8":case"utf-8":return I(this,t,n);case"ascii":return k(this,t,n);case"latin1":case"binary":return P(this,t,n);case"base64":return _(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return R(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}.apply(this,arguments)},c.prototype.equals=function(e){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");return this===e||0===c.compare(this,e)},c.prototype.inspect=function(){var e="",n=t.INSPECT_MAX_BYTES;return this.length>0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},c.prototype.compare=function(e,t,n,r,o){if(!c.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===o&&(o=this.length),t<0||n>e.length||r<0||o>this.length)throw new RangeError("out of range index");if(r>=o&&t>=n)return 0;if(r>=o)return-1;if(t>=n)return 1;if(this===e)return 0;for(var i=(o>>>=0)-(r>>>=0),a=(n>>>=0)-(t>>>=0),s=Math.min(i,a),u=this.slice(r,o),l=e.slice(t,n),f=0;fo)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var i=!1;;)switch(r){case"hex":return b(this,e,t,n);case"utf8":case"utf-8":return m(this,e,t,n);case"ascii":return w(this,e,t,n);case"latin1":case"binary":return E(this,e,t,n);case"base64":return S(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return C(this,e,t,n);default:if(i)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),i=!0}},c.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var T=4096;function k(e,t,n){var r="";n=Math.min(e.length,n);for(var o=t;or)&&(n=r);for(var o="",i=t;in)throw new RangeError("Trying to access beyond buffer length")}function O(e,t,n,r,o,i){if(!c.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function L(e,t,n,r){t<0&&(t=65535+t+1);for(var o=0,i=Math.min(e.length-n,2);o>>8*(r?o:1-o)}function M(e,t,n,r){t<0&&(t=4294967295+t+1);for(var o=0,i=Math.min(e.length-n,4);o>>8*(r?o:3-o)&255}function A(e,t,n,r,o,i){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function B(e,t,n,r,i){return i||A(e,0,n,4),o.write(e,t,n,r,23,4),n+4}function j(e,t,n,r,i){return i||A(e,0,n,8),o.write(e,t,n,r,52,8),n+8}c.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t0&&(o*=256);)r+=this[e+--t]*o;return r},c.prototype.readUInt8=function(e,t){return t||D(e,1,this.length),this[e]},c.prototype.readUInt16LE=function(e,t){return t||D(e,2,this.length),this[e]|this[e+1]<<8},c.prototype.readUInt16BE=function(e,t){return t||D(e,2,this.length),this[e]<<8|this[e+1]},c.prototype.readUInt32LE=function(e,t){return t||D(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},c.prototype.readUInt32BE=function(e,t){return t||D(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},c.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||D(e,t,this.length);for(var r=this[e],o=1,i=0;++i=(o*=128)&&(r-=Math.pow(2,8*t)),r},c.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||D(e,t,this.length);for(var r=t,o=1,i=this[e+--r];r>0&&(o*=256);)i+=this[e+--r]*o;return i>=(o*=128)&&(i-=Math.pow(2,8*t)),i},c.prototype.readInt8=function(e,t){return t||D(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},c.prototype.readInt16LE=function(e,t){t||D(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt16BE=function(e,t){t||D(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},c.prototype.readInt32LE=function(e,t){return t||D(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},c.prototype.readInt32BE=function(e,t){return t||D(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},c.prototype.readFloatLE=function(e,t){return t||D(e,4,this.length),o.read(this,e,!0,23,4)},c.prototype.readFloatBE=function(e,t){return t||D(e,4,this.length),o.read(this,e,!1,23,4)},c.prototype.readDoubleLE=function(e,t){return t||D(e,8,this.length),o.read(this,e,!0,52,8)},c.prototype.readDoubleBE=function(e,t){return t||D(e,8,this.length),o.read(this,e,!1,52,8)},c.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||O(this,e,t,n,Math.pow(2,8*n)-1,0);var o=1,i=0;for(this[t]=255&e;++i=0&&(i*=256);)this[t+o]=e/i&255;return t+n},c.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,1,255,0),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},c.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):L(this,e,t,!0),t+2},c.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,65535,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):L(this,e,t,!1),t+2},c.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):M(this,e,t,!0),t+4},c.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,4294967295,0),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):M(this,e,t,!1),t+4},c.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);O(this,e,t,n,o-1,-o)}var i=0,a=1,s=0;for(this[t]=255&e;++i>0)-s&255;return t+n},c.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);O(this,e,t,n,o-1,-o)}var i=n-1,a=1,s=0;for(this[t+i]=255&e;--i>=0&&(a*=256);)e<0&&0===s&&0!==this[t+i+1]&&(s=1),this[t+i]=(e/a>>0)-s&255;return t+n},c.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,1,127,-128),c.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},c.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):L(this,e,t,!0),t+2},c.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,2,32767,-32768),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):L(this,e,t,!1),t+2},c.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,2147483647,-2147483648),c.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):M(this,e,t,!0),t+4},c.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||O(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),c.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):M(this,e,t,!1),t+4},c.prototype.writeFloatLE=function(e,t,n){return B(this,e,t,!0,n)},c.prototype.writeFloatBE=function(e,t,n){return B(this,e,t,!1,n)},c.prototype.writeDoubleLE=function(e,t,n){return j(this,e,t,!0,n)},c.prototype.writeDoubleBE=function(e,t,n){return j(this,e,t,!1,n)},c.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--o)e[o+t]=this[o+n];else if(i<1e3||!c.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(i=t;i55295&&n<57344){if(!o){if(n>56319){(t-=3)>-1&&i.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&i.push(239,191,189);continue}o=n;continue}if(n<56320){(t-=3)>-1&&i.push(239,191,189),o=n;continue}n=65536+(o-55296<<10|n-56320)}else o&&(t-=3)>-1&&i.push(239,191,189);if(o=null,n<128){if((t-=1)<0)break;i.push(n)}else if(n<2048){if((t-=2)<0)break;i.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;i.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;i.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return i}function H(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(U,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function q(e,t,n,r){for(var o=0;o=t.length||o>=e.length);++o)t[o+n]=e[o];return o}}).call(this,n(10))},function(e,t,n){"use strict";n.d(t,"a",function(){return r});var r=function(){function e(){}return e.prototype.log=function(e,t){},e.instance=new e,e}()},function(e,t,n){"use strict";n.d(t,"a",function(){return r});var r=function(){function e(){}return e.write=function(t){return""+t+e.RecordSeparator},e.parse=function(t){if(t[t.length-1]!==e.RecordSeparator)throw new Error("Message is incomplete.");var n=t.split(e.RecordSeparator);return n.pop(),n},e.RecordSeparatorCode=30,e.RecordSeparator=String.fromCharCode(e.RecordSeparatorCode),e}()},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(25),n(9);var r=n(26),o=n(16),i={},a=!1;function s(e,t,n){var o=i[e];o||(o=i[e]=new r.BrowserRenderer(e)),o.attachRootComponentToLogicalElement(n,t)}t.attachRootComponentToLogicalElement=s,t.attachRootComponentToElement=function(e,t,n){var r=document.querySelector(e);if(!r)throw new Error("Could not find any element matching selector '"+e+"'.");s(n||0,o.toLogicalElement(r,!0),t)},t.renderBatch=function(e,t){var n=i[e];if(!n)throw new Error("There is no browser renderer with ID "+e+".");for(var r=t.arrayRangeReader,o=t.updatedComponents(),s=r.values(o),c=r.count(o),u=t.referenceFrames(),l=r.values(u),f=t.diffReader,h=0;h=0,"must have a non-negative type"),o(a,"must have a decode function"),this.registerEncoder(function(e){return e instanceof t},function(t){var o=i(),a=r.allocUnsafe(1);return a.writeInt8(e,0),o.append(a),o.append(n(t)),o}),this.registerDecoder(e,a),this},registerEncoder:function(e,n){return o(e,"must have an encode function"),o(n,"must have an encode function"),t.push({check:e,encode:n}),this},registerDecoder:function(e,t){return o(e>=0,"must have a non-negative type"),o(t,"must have a decode function"),n.push({type:e,decode:t}),this},encoder:a.encoder,decoder:a.decoder,buffer:!0,type:"msgpack5",IncompleteBufferError:s.IncompleteBufferError}}},function(e,t,n){var r=n(5),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function a(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=a),i(o,a),a.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},a.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},a.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},a.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,i=null;function a(e){t.push(e)}function s(e,t){for(var n=[],r=2;r0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function s(e,t,n){var i=e;if(e instanceof Comment&&(u(i)&&u(i).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(c(i))throw new Error("Not implemented: moving existing logical children");var a=u(t);if(n0;)e(r,0);var i=r;i.parentNode.removeChild(i)},t.getLogicalParent=c,t.getLogicalSiblingEnd=function(e){return e[i]||null},t.getLogicalChild=function(e,t){return u(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===l(e).namespaceURI},t.getLogicalChildrenArray=u,t.permuteLogicalChildren=function(e,t){var n=u(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=c(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):h(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,i=r;i;){var a=i.nextSibling;if(n.insertBefore(i,t),i===o)break;i=a}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=l},function(e,t,n){"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),t.dispatchEvent=function(e,t){if(!r)throw new Error("eventDispatcher not initialized. Call 'setEventDispatcher' to configure it.");return r(e,t)},t.setEventDispatcher=function(e){r=e}},function(e,t){var n,r,o=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function a(){throw new Error("clearTimeout has not been defined")}function s(e){if(n===setTimeout)return setTimeout(e,0);if((n===i||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:i}catch(e){n=i}try{r="function"==typeof clearTimeout?clearTimeout:a}catch(e){r=a}}();var c,u=[],l=!1,f=-1;function h(){l&&c&&(l=!1,c.length?u=c.concat(u):f=-1,u.length&&p())}function p(){if(!l){var e=s(h);l=!0;for(var t=u.length;t;){for(c=u,u=[];++f1)for(var n=1;n0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]this.length)&&(r=this.length),n>=this.length)return e||i.alloc(0);if(r<=0)return e||i.alloc(0);var o,a,s=!!e,c=this._offset(n),u=r-n,l=u,f=s&&t||0,h=c[1];if(0===n&&r==this.length){if(!s)return 1===this._bufs.length?this._bufs[0]:i.concat(this._bufs,this.length);for(a=0;a(o=this._bufs[a].length-h))){this._bufs[a].copy(e,f,h,h+l);break}this._bufs[a].copy(e,f,h),f+=o,l-=o,h&&(h=0)}return e},a.prototype.shallowSlice=function(e,t){e=e||0,t=t||this.length,e<0&&(e+=this.length),t<0&&(t+=this.length);var n=this._offset(e),r=this._offset(t),o=this._bufs.slice(n[0],r[0]+1);return 0==r[1]?o.pop():o[o.length-1]=o[o.length-1].slice(0,r[1]),0!=n[1]&&(o[0]=o[0].slice(n[1])),new a(o)},a.prototype.toString=function(e,t,n){return this.slice(t,n).toString(e)},a.prototype.consume=function(e){for(;this._bufs.length;){if(!(e>=this._bufs[0].length)){this._bufs[0]=this._bufs[0].slice(e),this.length-=e;break}e-=this._bufs[0].length,this.length-=this._bufs[0].length,this._bufs.shift()}return this},a.prototype.duplicate=function(){for(var e=0,t=new a;e0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]=i)return e;switch(e){case"%s":return String(r[n++]);case"%d":return Number(r[n++]);case"%j":try{return JSON.stringify(r[n++])}catch(e){return"[Circular]"}default:return e}}),c=r[n];n=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),d(n)?r.showHidden=n:n&&t._extend(r,n),b(r.showHidden)&&(r.showHidden=!1),b(r.depth)&&(r.depth=2),b(r.colors)&&(r.colors=!1),b(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=c),l(r,e,r.depth)}function c(e,t){var n=s.styles[t];return n?"["+s.colors[n][0]+"m"+e+"["+s.colors[n][1]+"m":e}function u(e,t){return e}function l(e,n,r){if(e.customInspect&&n&&C(n.inspect)&&n.inspect!==t.inspect&&(!n.constructor||n.constructor.prototype!==n)){var o=n.inspect(r,e);return v(o)||(o=l(e,o,r)),o}var i=function(e,t){if(b(t))return e.stylize("undefined","undefined");if(v(t)){var n="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(n,"string")}if(y(t))return e.stylize(""+t,"number");if(d(t))return e.stylize(""+t,"boolean");if(g(t))return e.stylize("null","null")}(e,n);if(i)return i;var a=Object.keys(n),s=function(e){var t={};return e.forEach(function(e,n){t[e]=!0}),t}(a);if(e.showHidden&&(a=Object.getOwnPropertyNames(n)),S(n)&&(a.indexOf("message")>=0||a.indexOf("description")>=0))return f(n);if(0===a.length){if(C(n)){var c=n.name?": "+n.name:"";return e.stylize("[Function"+c+"]","special")}if(m(n))return e.stylize(RegExp.prototype.toString.call(n),"regexp");if(E(n))return e.stylize(Date.prototype.toString.call(n),"date");if(S(n))return f(n)}var u,w="",_=!1,I=["{","}"];(p(n)&&(_=!0,I=["[","]"]),C(n))&&(w=" [Function"+(n.name?": "+n.name:"")+"]");return m(n)&&(w=" "+RegExp.prototype.toString.call(n)),E(n)&&(w=" "+Date.prototype.toUTCString.call(n)),S(n)&&(w=" "+f(n)),0!==a.length||_&&0!=n.length?r<0?m(n)?e.stylize(RegExp.prototype.toString.call(n),"regexp"):e.stylize("[Object]","special"):(e.seen.push(n),u=_?function(e,t,n,r,o){for(var i=[],a=0,s=t.length;a=0&&0,e+t.replace(/\u001b\[\d\d?m/g,"").length+1},0)>60)return n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1];return n[0]+t+" "+e.join(", ")+" "+n[1]}(u,w,I)):I[0]+w+I[1]}function f(e){return"["+Error.prototype.toString.call(e)+"]"}function h(e,t,n,r,o,i){var a,s,c;if((c=Object.getOwnPropertyDescriptor(t,o)||{value:t[o]}).get?s=c.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):c.set&&(s=e.stylize("[Setter]","special")),k(r,o)||(a="["+o+"]"),s||(e.seen.indexOf(c.value)<0?(s=g(n)?l(e,c.value,null):l(e,c.value,n-1)).indexOf("\n")>-1&&(s=i?s.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+s.split("\n").map(function(e){return" "+e}).join("\n")):s=e.stylize("[Circular]","special")),b(a)){if(i&&o.match(/^\d+$/))return s;(a=JSON.stringify(""+o)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=e.stylize(a,"name")):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=e.stylize(a,"string"))}return a+": "+s}function p(e){return Array.isArray(e)}function d(e){return"boolean"==typeof e}function g(e){return null===e}function y(e){return"number"==typeof e}function v(e){return"string"==typeof e}function b(e){return void 0===e}function m(e){return w(e)&&"[object RegExp]"===_(e)}function w(e){return"object"==typeof e&&null!==e}function E(e){return w(e)&&"[object Date]"===_(e)}function S(e){return w(e)&&("[object Error]"===_(e)||e instanceof Error)}function C(e){return"function"==typeof e}function _(e){return Object.prototype.toString.call(e)}function I(e){return e<10?"0"+e.toString(10):e.toString(10)}t.debuglog=function(n){if(b(i)&&(i=e.env.NODE_DEBUG||""),n=n.toUpperCase(),!a[n])if(new RegExp("\\b"+n+"\\b","i").test(i)){var r=e.pid;a[n]=function(){var e=t.format.apply(t,arguments);console.error("%s %d: %s",n,r,e)}}else a[n]=function(){};return a[n]},t.inspect=s,s.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},s.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.isArray=p,t.isBoolean=d,t.isNull=g,t.isNullOrUndefined=function(e){return null==e},t.isNumber=y,t.isString=v,t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=b,t.isRegExp=m,t.isObject=w,t.isDate=E,t.isError=S,t.isFunction=C,t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=n(54);var T=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function k(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.log=function(){var e,n;console.log("%s - %s",(e=new Date,n=[I(e.getHours()),I(e.getMinutes()),I(e.getSeconds())].join(":"),[e.getDate(),T[e.getMonth()],n].join(" ")),t.format.apply(t,arguments))},t.inherits=n(55),t._extend=function(e,t){if(!t||!w(t))return e;for(var n=Object.keys(t),r=n.length;r--;)e[n[r]]=t[n[r]];return e};var P="undefined"!=typeof Symbol?Symbol("util.promisify.custom"):void 0;function x(e,t){if(!e){var n=new Error("Promise was rejected with a falsy value");n.reason=e,e=n}return t(e)}t.promisify=function(e){if("function"!=typeof e)throw new TypeError('The "original" argument must be of type Function');if(P&&e[P]){var t;if("function"!=typeof(t=e[P]))throw new TypeError('The "util.promisify.custom" argument must be of type Function');return Object.defineProperty(t,P,{value:t,enumerable:!1,writable:!1,configurable:!0}),t}function t(){for(var t,n,r=new Promise(function(e,r){t=e,n=r}),o=[],i=0;i0?("string"==typeof t||a.objectMode||Object.getPrototypeOf(t)===u.prototype||(t=function(e){return u.from(e)}(t)),r?a.endEmitted?e.emit("error",new Error("stream.unshift() after end event")):E(e,a,t,!0):a.ended?e.emit("error",new Error("stream.push() after EOF")):(a.reading=!1,a.decoder&&!n?(t=a.decoder.write(t),a.objectMode||0!==t.length?E(e,a,t,!1):T(e,a)):E(e,a,t,!1))):r||(a.reading=!1));return function(e){return!e.ended&&(e.needReadable||e.lengtht.highWaterMark&&(t.highWaterMark=function(e){return e>=S?e=S:(e--,e|=e>>>1,e|=e>>>2,e|=e>>>4,e|=e>>>8,e|=e>>>16,e++),e}(e)),e<=t.length?e:t.ended?t.length:(t.needReadable=!0,0))}function _(e){var t=e._readableState;t.needReadable=!1,t.emittedReadable||(p("emitReadable",t.flowing),t.emittedReadable=!0,t.sync?o.nextTick(I,e):I(e))}function I(e){p("emit readable"),e.emit("readable"),R(e)}function T(e,t){t.readingMore||(t.readingMore=!0,o.nextTick(k,e,t))}function k(e,t){for(var n=t.length;!t.reading&&!t.flowing&&!t.ended&&t.length=t.length?(n=t.decoder?t.buffer.join(""):1===t.buffer.length?t.buffer.head.data:t.buffer.concat(t.length),t.buffer.clear()):n=function(e,t,n){var r;ei.length?i.length:e;if(a===i.length?o+=i:o+=i.slice(0,e),0===(e-=a)){a===i.length?(++r,n.next?t.head=n.next:t.head=t.tail=null):(t.head=n,n.data=i.slice(a));break}++r}return t.length-=r,o}(e,t):function(e,t){var n=u.allocUnsafe(e),r=t.head,o=1;r.data.copy(n),e-=r.data.length;for(;r=r.next;){var i=r.data,a=e>i.length?i.length:e;if(i.copy(n,n.length-e,0,a),0===(e-=a)){a===i.length?(++o,r.next?t.head=r.next:t.head=t.tail=null):(t.head=r,r.data=i.slice(a));break}++o}return t.length-=o,n}(e,t);return r}(e,t.buffer,t.decoder),n);var n}function O(e){var t=e._readableState;if(t.length>0)throw new Error('"endReadable()" called on non-empty stream');t.endEmitted||(t.ended=!0,o.nextTick(L,t,e))}function L(e,t){e.endEmitted||0!==e.length||(e.endEmitted=!0,t.readable=!1,t.emit("end"))}function M(e,t){for(var n=0,r=e.length;n=t.highWaterMark||t.ended))return p("read: emitReadable",t.length,t.ended),0===t.length&&t.ended?O(this):_(this),null;if(0===(e=C(e,t))&&t.ended)return 0===t.length&&O(this),null;var r,o=t.needReadable;return p("need readable",o),(0===t.length||t.length-e0?D(e,t):null)?(t.needReadable=!0,e=0):t.length-=e,0===t.length&&(t.ended||(t.needReadable=!0),n!==e&&t.ended&&O(this)),null!==r&&this.emit("data",r),r},m.prototype._read=function(e){this.emit("error",new Error("_read() is not implemented"))},m.prototype.pipe=function(e,t){var n=this,i=this._readableState;switch(i.pipesCount){case 0:i.pipes=e;break;case 1:i.pipes=[i.pipes,e];break;default:i.pipes.push(e)}i.pipesCount+=1,p("pipe count=%d opts=%j",i.pipesCount,t);var c=(!t||!1!==t.end)&&e!==r.stdout&&e!==r.stderr?l:m;function u(t,r){p("onunpipe"),t===n&&r&&!1===r.hasUnpiped&&(r.hasUnpiped=!0,p("cleanup"),e.removeListener("close",v),e.removeListener("finish",b),e.removeListener("drain",f),e.removeListener("error",y),e.removeListener("unpipe",u),n.removeListener("end",l),n.removeListener("end",m),n.removeListener("data",g),h=!0,!i.awaitDrain||e._writableState&&!e._writableState.needDrain||f())}function l(){p("onend"),e.end()}i.endEmitted?o.nextTick(c):n.once("end",c),e.on("unpipe",u);var f=function(e){return function(){var t=e._readableState;p("pipeOnDrain",t.awaitDrain),t.awaitDrain&&t.awaitDrain--,0===t.awaitDrain&&s(e,"data")&&(t.flowing=!0,R(e))}}(n);e.on("drain",f);var h=!1;var d=!1;function g(t){p("ondata"),d=!1,!1!==e.write(t)||d||((1===i.pipesCount&&i.pipes===e||i.pipesCount>1&&-1!==M(i.pipes,e))&&!h&&(p("false write response, pause",n._readableState.awaitDrain),n._readableState.awaitDrain++,d=!0),n.pause())}function y(t){p("onerror",t),m(),e.removeListener("error",y),0===s(e,"error")&&e.emit("error",t)}function v(){e.removeListener("finish",b),m()}function b(){p("onfinish"),e.removeListener("close",v),m()}function m(){p("unpipe"),n.unpipe(e)}return n.on("data",g),function(e,t,n){if("function"==typeof e.prependListener)return e.prependListener(t,n);e._events&&e._events[t]?a(e._events[t])?e._events[t].unshift(n):e._events[t]=[n,e._events[t]]:e.on(t,n)}(e,"error",y),e.once("close",v),e.once("finish",b),e.emit("pipe",n),i.flowing||(p("pipe resume"),n.resume()),e},m.prototype.unpipe=function(e){var t=this._readableState,n={hasUnpiped:!1};if(0===t.pipesCount)return this;if(1===t.pipesCount)return e&&e!==t.pipes?this:(e||(e=t.pipes),t.pipes=null,t.pipesCount=0,t.flowing=!1,e&&e.emit("unpipe",this,n),this);if(!e){var r=t.pipes,o=t.pipesCount;t.pipes=null,t.pipesCount=0,t.flowing=!1;for(var i=0;i0&&a.length>o&&!a.warned){a.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+a.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=e,c.type=t,c.count=a.length,s=c,console&&console.warn&&console.warn(s)}return e}function f(e,t,n){var r={fired:!1,wrapFn:void 0,target:e,type:t,listener:n},o=function(){for(var e=[],t=0;t0&&(a=t[0]),a instanceof Error)throw a;var s=new Error("Unhandled error."+(a?" ("+a.message+")":""));throw s.context=a,s}var c=o[e];if(void 0===c)return!1;if("function"==typeof c)i(c,this,t);else{var u=c.length,l=d(c,u);for(n=0;n=0;i--)if(n[i]===t||n[i].listener===t){a=n[i].listener,o=i;break}if(o<0)return this;0===o?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},s.prototype.listeners=function(e){return h(this,e,!0)},s.prototype.rawListeners=function(e){return h(this,e,!1)},s.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):p.call(e,t)},s.prototype.listenerCount=p,s.prototype.eventNames=function(){return this._eventsCount>0?r(this._events):[]}},function(e,t,n){e.exports=n(37).EventEmitter},function(e,t,n){"use strict";var r=n(23);function o(e,t){e.emit("error",t)}e.exports={destroy:function(e,t){var n=this,i=this._readableState&&this._readableState.destroyed,a=this._writableState&&this._writableState.destroyed;return i||a?(t?t(e):!e||this._writableState&&this._writableState.errorEmitted||r.nextTick(o,this,e),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(e||null,function(e){!t&&e?(r.nextTick(o,n,e),n._writableState&&(n._writableState.errorEmitted=!0)):t&&t(e)}),this)},undestroy:function(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)}}},function(e,t,n){"use strict";var r=n(61).Buffer,o=r.isEncoding||function(e){switch((e=""+e)&&e.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function i(e){var t;switch(this.encoding=function(e){var t=function(e){if(!e)return"utf8";for(var t;;)switch(e){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return e;default:if(t)return;e=(""+e).toLowerCase(),t=!0}}(e);if("string"!=typeof t&&(r.isEncoding===o||!o(e)))throw new Error("Unknown encoding: "+e);return t||e}(e),this.encoding){case"utf16le":this.text=c,this.end=u,t=4;break;case"utf8":this.fillLast=s,t=4;break;case"base64":this.text=l,this.end=f,t=3;break;default:return this.write=h,void(this.end=p)}this.lastNeed=0,this.lastTotal=0,this.lastChar=r.allocUnsafe(t)}function a(e){return e<=127?0:e>>5==6?2:e>>4==14?3:e>>3==30?4:e>>6==2?-1:-2}function s(e){var t=this.lastTotal-this.lastNeed,n=function(e,t,n){if(128!=(192&t[0]))return e.lastNeed=0,"�";if(e.lastNeed>1&&t.length>1){if(128!=(192&t[1]))return e.lastNeed=1,"�";if(e.lastNeed>2&&t.length>2&&128!=(192&t[2]))return e.lastNeed=2,"�"}}(this,e);return void 0!==n?n:this.lastNeed<=e.length?(e.copy(this.lastChar,t,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal)):(e.copy(this.lastChar,t,0,e.length),void(this.lastNeed-=e.length))}function c(e,t){if((e.length-t)%2==0){var n=e.toString("utf16le",t);if(n){var r=n.charCodeAt(n.length-1);if(r>=55296&&r<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1],n.slice(0,-1)}return n}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=e[e.length-1],e.toString("utf16le",t,e.length-1)}function u(e){var t=e&&e.length?this.write(e):"";if(this.lastNeed){var n=this.lastTotal-this.lastNeed;return t+this.lastChar.toString("utf16le",0,n)}return t}function l(e,t){var n=(e.length-t)%3;return 0===n?e.toString("base64",t):(this.lastNeed=3-n,this.lastTotal=3,1===n?this.lastChar[0]=e[e.length-1]:(this.lastChar[0]=e[e.length-2],this.lastChar[1]=e[e.length-1]),e.toString("base64",t,e.length-n))}function f(e){var t=e&&e.length?this.write(e):"";return this.lastNeed?t+this.lastChar.toString("base64",0,3-this.lastNeed):t}function h(e){return e.toString(this.encoding)}function p(e){return e&&e.length?this.write(e):""}t.StringDecoder=i,i.prototype.write=function(e){if(0===e.length)return"";var t,n;if(this.lastNeed){if(void 0===(t=this.fillLast(e)))return"";n=this.lastNeed,this.lastNeed=0}else n=0;return n=0)return o>0&&(e.lastNeed=o-1),o;if(--r=0)return o>0&&(e.lastNeed=o-2),o;if(--r=0)return o>0&&(2===o?o=0:e.lastNeed=o-3),o;return 0}(this,e,t);if(!this.lastNeed)return e.toString("utf8",t);this.lastTotal=n;var r=e.length-(n-this.lastNeed);return e.copy(this.lastChar,0,r),e.toString("utf8",t,r)},i.prototype.fillLast=function(e){if(this.lastNeed<=e.length)return e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);e.copy(this.lastChar,this.lastTotal-this.lastNeed,0,e.length),this.lastNeed-=e.length}},function(e,t,n){"use strict";(function(t,r,o){var i=n(23);function a(e){var t=this;this.next=null,this.entry=null,this.finish=function(){!function(e,t,n){var r=e.entry;e.entry=null;for(;r;){var o=r.callback;t.pendingcb--,o(n),r=r.next}t.corkedRequestsFree?t.corkedRequestsFree.next=e:t.corkedRequestsFree=e}(t,e)}}e.exports=b;var s,c=!t.browser&&["v0.10","v0.9."].indexOf(t.version.slice(0,5))>-1?r:i.nextTick;b.WritableState=v;var u=n(19);u.inherits=n(14);var l={deprecate:n(64)},f=n(38),h=n(13).Buffer,p=o.Uint8Array||function(){};var d,g=n(39);function y(){}function v(e,t){s=s||n(11),e=e||{};var r=t instanceof s;this.objectMode=!!e.objectMode,r&&(this.objectMode=this.objectMode||!!e.writableObjectMode);var o=e.highWaterMark,u=e.writableHighWaterMark,l=this.objectMode?16:16384;this.highWaterMark=o||0===o?o:r&&(u||0===u)?u:l,this.highWaterMark=Math.floor(this.highWaterMark),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var f=!1===e.decodeStrings;this.decodeStrings=!f,this.defaultEncoding=e.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(e){!function(e,t){var n=e._writableState,r=n.sync,o=n.writecb;if(function(e){e.writing=!1,e.writecb=null,e.length-=e.writelen,e.writelen=0}(n),t)!function(e,t,n,r,o){--t.pendingcb,n?(i.nextTick(o,r),i.nextTick(_,e,t),e._writableState.errorEmitted=!0,e.emit("error",r)):(o(r),e._writableState.errorEmitted=!0,e.emit("error",r),_(e,t))}(e,n,r,t,o);else{var a=S(n);a||n.corked||n.bufferProcessing||!n.bufferedRequest||E(e,n),r?c(w,e,n,a,o):w(e,n,a,o)}}(t,e)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.bufferedRequestCount=0,this.corkedRequestsFree=new a(this)}function b(e){if(s=s||n(11),!(d.call(b,this)||this instanceof s))return new b(e);this._writableState=new v(e,this),this.writable=!0,e&&("function"==typeof e.write&&(this._write=e.write),"function"==typeof e.writev&&(this._writev=e.writev),"function"==typeof e.destroy&&(this._destroy=e.destroy),"function"==typeof e.final&&(this._final=e.final)),f.call(this)}function m(e,t,n,r,o,i,a){t.writelen=r,t.writecb=a,t.writing=!0,t.sync=!0,n?e._writev(o,t.onwrite):e._write(o,i,t.onwrite),t.sync=!1}function w(e,t,n,r){n||function(e,t){0===t.length&&t.needDrain&&(t.needDrain=!1,e.emit("drain"))}(e,t),t.pendingcb--,r(),_(e,t)}function E(e,t){t.bufferProcessing=!0;var n=t.bufferedRequest;if(e._writev&&n&&n.next){var r=t.bufferedRequestCount,o=new Array(r),i=t.corkedRequestsFree;i.entry=n;for(var s=0,c=!0;n;)o[s]=n,n.isBuf||(c=!1),n=n.next,s+=1;o.allBuffers=c,m(e,t,!0,t.length,o,"",i.finish),t.pendingcb++,t.lastBufferedRequest=null,i.next?(t.corkedRequestsFree=i.next,i.next=null):t.corkedRequestsFree=new a(t),t.bufferedRequestCount=0}else{for(;n;){var u=n.chunk,l=n.encoding,f=n.callback;if(m(e,t,!1,t.objectMode?1:u.length,u,l,f),n=n.next,t.bufferedRequestCount--,t.writing)break}null===n&&(t.lastBufferedRequest=null)}t.bufferedRequest=n,t.bufferProcessing=!1}function S(e){return e.ending&&0===e.length&&null===e.bufferedRequest&&!e.finished&&!e.writing}function C(e,t){e._final(function(n){t.pendingcb--,n&&e.emit("error",n),t.prefinished=!0,e.emit("prefinish"),_(e,t)})}function _(e,t){var n=S(t);return n&&(!function(e,t){t.prefinished||t.finalCalled||("function"==typeof e._final?(t.pendingcb++,t.finalCalled=!0,i.nextTick(C,e,t)):(t.prefinished=!0,e.emit("prefinish")))}(e,t),0===t.pendingcb&&(t.finished=!0,e.emit("finish"))),n}u.inherits(b,f),v.prototype.getBuffer=function(){for(var e=this.bufferedRequest,t=[];e;)t.push(e),e=e.next;return t},function(){try{Object.defineProperty(v.prototype,"buffer",{get:l.deprecate(function(){return this.getBuffer()},"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch(e){}}(),"function"==typeof Symbol&&Symbol.hasInstance&&"function"==typeof Function.prototype[Symbol.hasInstance]?(d=Function.prototype[Symbol.hasInstance],Object.defineProperty(b,Symbol.hasInstance,{value:function(e){return!!d.call(this,e)||this===b&&(e&&e._writableState instanceof v)}})):d=function(e){return e instanceof this},b.prototype.pipe=function(){this.emit("error",new Error("Cannot pipe, not readable"))},b.prototype.write=function(e,t,n){var r,o=this._writableState,a=!1,s=!o.objectMode&&(r=e,h.isBuffer(r)||r instanceof p);return s&&!h.isBuffer(e)&&(e=function(e){return h.from(e)}(e)),"function"==typeof t&&(n=t,t=null),s?t="buffer":t||(t=o.defaultEncoding),"function"!=typeof n&&(n=y),o.ended?function(e,t){var n=new Error("write after end");e.emit("error",n),i.nextTick(t,n)}(this,n):(s||function(e,t,n,r){var o=!0,a=!1;return null===n?a=new TypeError("May not write null values to stream"):"string"==typeof n||void 0===n||t.objectMode||(a=new TypeError("Invalid non-string/buffer chunk")),a&&(e.emit("error",a),i.nextTick(r,a),o=!1),o}(this,o,e,n))&&(o.pendingcb++,a=function(e,t,n,r,o,i){if(!n){var a=function(e,t,n){e.objectMode||!1===e.decodeStrings||"string"!=typeof t||(t=h.from(t,n));return t}(t,r,o);r!==a&&(n=!0,o="buffer",r=a)}var s=t.objectMode?1:r.length;t.length+=s;var c=t.length-1))throw new TypeError("Unknown encoding: "+e);return this._writableState.defaultEncoding=e,this},Object.defineProperty(b.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}}),b.prototype._write=function(e,t,n){n(new Error("_write() is not implemented"))},b.prototype._writev=null,b.prototype.end=function(e,t,n){var r=this._writableState;"function"==typeof e?(n=e,e=null,t=null):"function"==typeof t&&(n=t,t=null),null!=e&&this.write(e,t),r.corked&&(r.corked=1,this.uncork()),r.ending||r.finished||function(e,t,n){t.ending=!0,_(e,t),n&&(t.finished?i.nextTick(n):e.once("finish",n));t.ended=!0,e.writable=!1}(this,r,n)},Object.defineProperty(b.prototype,"destroyed",{get:function(){return void 0!==this._writableState&&this._writableState.destroyed},set:function(e){this._writableState&&(this._writableState.destroyed=e)}}),b.prototype.destroy=g.destroy,b.prototype._undestroy=g.undestroy,b.prototype._destroy=function(e,t){this.end(),t(e)}}).call(this,n(18),n(62).setImmediate,n(10))},function(e,t,n){"use strict";e.exports=a;var r=n(11),o=n(19);function i(e,t){var n=this._transformState;n.transforming=!1;var r=n.writecb;if(!r)return this.emit("error",new Error("write callback called multiple times"));n.writechunk=null,n.writecb=null,null!=t&&this.push(t),r(e);var o=this._readableState;o.reading=!1,(o.needReadable||o.length=200&&c.statusCode<300?r(new a.b(c.statusCode,c.statusMessage||"",u)):o(new i.b(c.statusMessage||"",c.statusCode||0))});t.abortSignal&&(t.abortSignal.onabort=function(){f.abort(),o(new i.a)})})},n.prototype.getCookieString=function(e){return this.cookieJar.getCookieString(e)},n}(a.a)}).call(this,n(5).Buffer)},function(e,t,n){"use strict";(function(e){n.d(t,"a",function(){return i});var r=n(7),o=n(1),i=function(){function t(){}return t.prototype.writeHandshakeRequest=function(e){return r.a.write(JSON.stringify(e))},t.prototype.parseHandshakeResponse=function(t){var n,i;if(Object(o.g)(t)||void 0!==e&&t instanceof e){var a=new Uint8Array(t);if(-1===(c=a.indexOf(r.a.RecordSeparatorCode)))throw new Error("Message is incomplete.");var s=c+1;n=String.fromCharCode.apply(null,a.slice(0,s)),i=a.byteLength>s?a.slice(s).buffer:null}else{var c,u=t;if(-1===(c=u.indexOf(r.a.RecordSeparator)))throw new Error("Message is incomplete.");s=c+1;n=u.substring(0,s),i=u.length>s?u.substring(s):null}var l=r.a.parse(n),f=JSON.parse(l[0]);if(f.type)throw new Error("Expected a handshake response from the server.");return[i,f]},t}()}).call(this,n(5).Buffer)},,,,,function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)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 a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0)&&!(r=i.next()).done;)a.push(r.value)}catch(e){o={error:e}}finally{try{r&&!r.done&&(n=i.return)&&n.call(i)}finally{if(o)throw o.error}}return a},a=this&&this.__spread||function(){for(var e=[],t=0;t0?r-4:r,f=0;f>16&255,s[c++]=t>>8&255,s[c++]=255&t;2===a&&(t=o[e.charCodeAt(f)]<<2|o[e.charCodeAt(f+1)]>>4,s[c++]=255&t);1===a&&(t=o[e.charCodeAt(f)]<<10|o[e.charCodeAt(f+1)]<<4|o[e.charCodeAt(f+2)]>>2,s[c++]=t>>8&255,s[c++]=255&t);return s},t.fromByteArray=function(e){for(var t,n=e.length,o=n%3,i=[],a=0,s=n-o;as?s:a+16383));1===o?(t=e[n-1],i.push(r[t>>2]+r[t<<4&63]+"==")):2===o&&(t=(e[n-2]<<8)+e[n-1],i.push(r[t>>10]+r[t>>4&63]+r[t<<2&63]+"="));return i.join("")};for(var r=[],o=[],i="undefined"!=typeof Uint8Array?Uint8Array:Array,a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s=0,c=a.length;s0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");return-1===n&&(n=t),[n,n===t?0:4-n%4]}function l(e,t,n){for(var o,i,a=[],s=t;s>18&63]+r[i>>12&63]+r[i>>6&63]+r[63&i]);return a.join("")}o["-".charCodeAt(0)]=62,o["_".charCodeAt(0)]=63},function(e,t){t.read=function(e,t,n,r,o){var i,a,s=8*o-r-1,c=(1<>1,l=-7,f=n?o-1:0,h=n?-1:1,p=e[t+f];for(f+=h,i=p&(1<<-l)-1,p>>=-l,l+=s;l>0;i=256*i+e[t+f],f+=h,l-=8);for(a=i&(1<<-l)-1,i>>=-l,l+=r;l>0;a=256*a+e[t+f],f+=h,l-=8);if(0===i)i=1-u;else{if(i===c)return a?NaN:1/0*(p?-1:1);a+=Math.pow(2,r),i-=u}return(p?-1:1)*a*Math.pow(2,i-r)},t.write=function(e,t,n,r,o,i){var a,s,c,u=8*i-o-1,l=(1<>1,h=23===o?Math.pow(2,-24)-Math.pow(2,-77):0,p=r?0:i-1,d=r?1:-1,g=t<0||0===t&&1/t<0?1:0;for(t=Math.abs(t),isNaN(t)||t===1/0?(s=isNaN(t)?1:0,a=l):(a=Math.floor(Math.log(t)/Math.LN2),t*(c=Math.pow(2,-a))<1&&(a--,c*=2),(t+=a+f>=1?h/c:h*Math.pow(2,1-f))*c>=2&&(a++,c/=2),a+f>=l?(s=0,a=l):a+f>=1?(s=(t*c-1)*Math.pow(2,o),a+=f):(s=t*Math.pow(2,f-1)*Math.pow(2,o),a=0));o>=8;e[n+p]=255&s,p+=d,s/=256,o-=8);for(a=a<0;e[n+p]=255&a,p+=d,a/=256,u-=8);e[n+p-d]|=128*g}},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t,n){"use strict";(function(t){ /*! * The buffer module from node.js, for the browser. * * @author Feross Aboukhadijeh * @license MIT */ -function r(e,t){if(e===t)return 0;for(var n=e.length,r=t.length,o=0,i=Math.min(n,r);o=0;u--)if(l[u]!==f[u])return!1;for(u=l.length-1;u>=0;u--)if(c=l[u],!b(e[c],t[c],n,r))return!1;return!0}(e,t,n,s))}return n?e===t:e==t}function m(e){return"[object Arguments]"==Object.prototype.toString.call(e)}function w(e,t){if(!e||!t)return!1;if("[object RegExp]"==Object.prototype.toString.call(t))return t.test(e);try{if(e instanceof t)return!0}catch(e){}return!Error.isPrototypeOf(t)&&!0===t.call({},e)}function E(e,t,n,r){var o;if("function"!=typeof t)throw new TypeError('"block" argument must be a function');"string"==typeof n&&(r=n,n=null),o=function(e){var t;try{e()}catch(e){t=e}return t}(t),r=(n&&n.name?" ("+n.name+").":".")+(r?" "+r:"."),e&&!o&&y(o,n,"Missing expected exception"+r);var s="string"==typeof r,a=!e&&o&&!n;if((!e&&i.isError(o)&&s&&w(o,n)||a)&&y(o,n,"Got unwanted exception"+r),e&&o&&n&&!w(o,n)||!e&&o)throw o}f.AssertionError=function(e){var t;this.name="AssertionError",this.actual=e.actual,this.expected=e.expected,this.operator=e.operator,e.message?(this.message=e.message,this.generatedMessage=!1):(this.message=d(g((t=this).actual),128)+" "+t.operator+" "+d(g(t.expected),128),this.generatedMessage=!0);var n=e.stackStartFunction||y;if(Error.captureStackTrace)Error.captureStackTrace(this,n);else{var r=new Error;if(r.stack){var o=r.stack,i=p(n),s=o.indexOf("\n"+i);if(s>=0){var a=o.indexOf("\n",s+1);o=o.substring(a+1)}this.stack=o}}},i.inherits(f.AssertionError,Error),f.fail=y,f.ok=v,f.equal=function(e,t,n){e!=t&&y(e,t,n,"==",f.equal)},f.notEqual=function(e,t,n){e==t&&y(e,t,n,"!=",f.notEqual)},f.deepEqual=function(e,t,n){b(e,t,!1)||y(e,t,n,"deepEqual",f.deepEqual)},f.deepStrictEqual=function(e,t,n){b(e,t,!0)||y(e,t,n,"deepStrictEqual",f.deepStrictEqual)},f.notDeepEqual=function(e,t,n){b(e,t,!1)&&y(e,t,n,"notDeepEqual",f.notDeepEqual)},f.notDeepStrictEqual=function e(t,n,r){b(t,n,!0)&&y(t,n,r,"notDeepStrictEqual",e)},f.strictEqual=function(e,t,n){e!==t&&y(e,t,n,"===",f.strictEqual)},f.notStrictEqual=function(e,t,n){e===t&&y(e,t,n,"!==",f.notStrictEqual)},f.throws=function(e,t,n){E(!0,e,t,n)},f.doesNotThrow=function(e,t,n){E(!1,e,t,n)},f.ifError=function(e){if(e)throw e};var S=Object.keys||function(e){var t=[];for(var n in e)s.call(e,n)&&t.push(n);return t}}).call(this,n(10))},function(e,t){e.exports=function(e){return e&&"object"==typeof e&&"function"==typeof e.copy&&"function"==typeof e.fill&&"function"==typeof e.readUInt8}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){e.exports=n(11)},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t){},function(e,t,n){"use strict";var r=n(13).Buffer,o=n(60);e.exports=function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.head=null,this.tail=null,this.length=0}return e.prototype.push=function(e){var t={data:e,next:null};this.length>0?this.tail.next=t:this.head=t,this.tail=t,++this.length},e.prototype.unshift=function(e){var t={data:e,next:this.head};0===this.length&&(this.tail=t),this.head=t,++this.length},e.prototype.shift=function(){if(0!==this.length){var e=this.head.data;return 1===this.length?this.head=this.tail=null:this.head=this.head.next,--this.length,e}},e.prototype.clear=function(){this.head=this.tail=null,this.length=0},e.prototype.join=function(e){if(0===this.length)return"";for(var t=this.head,n=""+t.data;t=t.next;)n+=e+t.data;return n},e.prototype.concat=function(e){if(0===this.length)return r.alloc(0);if(1===this.length)return this.head.data;for(var t,n,o,i=r.allocUnsafe(e>>>0),s=this.head,a=0;s;)t=s.data,n=i,o=a,t.copy(n,o),a+=s.data.length,s=s.next;return i},e}(),o&&o.inspect&&o.inspect.custom&&(e.exports.prototype[o.inspect.custom]=function(){var e=o.inspect({length:this.length});return this.constructor.name+" "+e})},function(e,t){},function(e,t,n){var r=n(5),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function s(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=s),i(o,s),s.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},s.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},s.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},s.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){(function(e){var r=void 0!==e&&e||"undefined"!=typeof self&&self||window,o=Function.prototype.apply;function i(e,t){this._id=e,this._clearFn=t}t.setTimeout=function(){return new i(o.call(setTimeout,r,arguments),clearTimeout)},t.setInterval=function(){return new i(o.call(setInterval,r,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},i.prototype.unref=i.prototype.ref=function(){},i.prototype.close=function(){this._clearFn.call(r,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;t>=0&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},n(63),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(10))},function(e,t,n){(function(e,t){!function(e,n){"use strict";if(!e.setImmediate){var r,o,i,s,a,c=1,u={},l=!1,f=e.document,h=Object.getPrototypeOf&&Object.getPrototypeOf(e);h=h&&h.setTimeout?h:e,"[object process]"==={}.toString.call(e.process)?r=function(e){t.nextTick(function(){d(e)})}:!function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?e.MessageChannel?((i=new MessageChannel).port1.onmessage=function(e){d(e.data)},r=function(e){i.port2.postMessage(e)}):f&&"onreadystatechange"in f.createElement("script")?(o=f.documentElement,r=function(e){var t=f.createElement("script");t.onreadystatechange=function(){d(e),t.onreadystatechange=null,o.removeChild(t),t=null},o.appendChild(t)}):r=function(e){setTimeout(d,0,e)}:(s="setImmediate$"+Math.random()+"$",a=function(t){t.source===e&&"string"==typeof t.data&&0===t.data.indexOf(s)&&d(+t.data.slice(s.length))},e.addEventListener?e.addEventListener("message",a,!1):e.attachEvent("onmessage",a),r=function(t){e.postMessage(s+t,"*")}),h.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n0?this._transform(null,t,n):n()},e.exports.decoder=c,e.exports.encoder=a},function(e,t,n){(t=e.exports=n(36)).Stream=t,t.Readable=t,t.Writable=n(41),t.Duplex=n(11),t.Transform=n(42),t.PassThrough=n(67)},function(e,t,n){"use strict";e.exports=i;var r=n(42),o=n(19);function i(e){if(!(this instanceof i))return new i(e);r.call(this,e)}o.inherits=n(14),o.inherits(i,r),i.prototype._transform=function(e,t,n){n(null,e)}},function(e,t,n){var r=n(22);function o(e){Error.call(this),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),this.name=this.constructor.name,this.message=e||"unable to decode"}n(35).inherits(o,Error),e.exports=function(e){return function(e){e instanceof r||(e=r().append(e));var t=i(e);if(t)return e.consume(t.bytesConsumed),t.value;throw new o};function t(e,t,n){return t>=n+e}function n(e,t){return{value:e,bytesConsumed:t}}function i(e,r){r=void 0===r?0:r;var o=e.length-r;if(o<=0)return null;var i,l,f,h=e.readUInt8(r),p=0;if(!function(e,t){var n=function(e){switch(e){case 196:return 2;case 197:return 3;case 198:return 5;case 199:return 3;case 200:return 4;case 201:return 6;case 202:return 5;case 203:return 9;case 204:return 2;case 205:return 3;case 206:return 5;case 207:return 9;case 208:return 2;case 209:return 3;case 210:return 5;case 211:return 9;case 212:return 3;case 213:return 4;case 214:return 6;case 215:return 10;case 216:return 18;case 217:return 2;case 218:return 3;case 219:return 5;case 222:return 3;default:return-1}}(e);return!(-1!==n&&t=0;f--)p+=e.readUInt8(r+f+1)*Math.pow(2,8*(7-f));return n(p,9);case 208:return n(p=e.readInt8(r+1),2);case 209:return n(p=e.readInt16BE(r+1),3);case 210:return n(p=e.readInt32BE(r+1),5);case 211:return n(p=function(e,t){var n=128==(128&e[t]);if(n)for(var r=1,o=t+7;o>=t;o--){var i=(255^e[o])+r;e[o]=255&i,r=i>>8}var s=e.readUInt32BE(t+0),a=e.readUInt32BE(t+4);return(4294967296*s+a)*(n?-1:1)}(e.slice(r+1,r+9),0),9);case 202:return n(p=e.readFloatBE(r+1),5);case 203:return n(p=e.readDoubleBE(r+1),9);case 217:return t(i=e.readUInt8(r+1),o,2)?n(p=e.toString("utf8",r+2,r+2+i),2+i):null;case 218:return t(i=e.readUInt16BE(r+1),o,3)?n(p=e.toString("utf8",r+3,r+3+i),3+i):null;case 219:return t(i=e.readUInt32BE(r+1),o,5)?n(p=e.toString("utf8",r+5,r+5+i),5+i):null;case 196:return t(i=e.readUInt8(r+1),o,2)?n(p=e.slice(r+2,r+2+i),2+i):null;case 197:return t(i=e.readUInt16BE(r+1),o,3)?n(p=e.slice(r+3,r+3+i),3+i):null;case 198:return t(i=e.readUInt32BE(r+1),o,5)?n(p=e.slice(r+5,r+5+i),5+i):null;case 220:return o<3?null:(i=e.readUInt16BE(r+1),s(e,r,i,3));case 221:return o<5?null:(i=e.readUInt32BE(r+1),s(e,r,i,5));case 222:return i=e.readUInt16BE(r+1),a(e,r,i,3);case 223:throw new Error("map too big to decode in JS");case 212:return c(e,r,1);case 213:return c(e,r,2);case 214:return c(e,r,4);case 215:return c(e,r,8);case 216:return c(e,r,16);case 199:return i=e.readUInt8(r+1),l=e.readUInt8(r+2),t(i,o,3)?u(e,r,l,i,3):null;case 200:return i=e.readUInt16BE(r+1),l=e.readUInt8(r+3),t(i,o,4)?u(e,r,l,i,4):null;case 201:return i=e.readUInt32BE(r+1),l=e.readUInt8(r+5),t(i,o,6)?u(e,r,l,i,6):null}if(144==(240&h))return s(e,r,i=15&h,1);if(128==(240&h))return a(e,r,i=15&h,1);if(160==(224&h))return t(i=31&h,o,1)?n(p=e.toString("utf8",r+1,r+i+1),i+1):null;if(h>=224)return n(p=h-256,1);if(h<128)return n(h,1);throw new Error("not implemented yet")}function s(e,t,r,o){var s,a=[],c=0;for(t+=o,s=0;si)&&((n=r.allocUnsafe(9))[0]=203,n.writeDoubleBE(e,1)),n}e.exports=function(e,t,n,i){function a(c,u){var l,f,h;if(void 0===c)throw new Error("undefined is not encodable in msgpack!");if(null===c)(l=r.allocUnsafe(1))[0]=192;else if(!0===c)(l=r.allocUnsafe(1))[0]=195;else if(!1===c)(l=r.allocUnsafe(1))[0]=194;else if("string"==typeof c)(f=r.byteLength(c))<32?((l=r.allocUnsafe(1+f))[0]=160|f,f>0&&l.write(c,1)):f<=255&&!n?((l=r.allocUnsafe(2+f))[0]=217,l[1]=f,l.write(c,2)):f<=65535?((l=r.allocUnsafe(3+f))[0]=218,l.writeUInt16BE(f,1),l.write(c,3)):((l=r.allocUnsafe(5+f))[0]=219,l.writeUInt32BE(f,1),l.write(c,5));else if(c&&(c.readUInt32LE||c instanceof Uint8Array))c instanceof Uint8Array&&(c=r.from(c)),c.length<=255?((l=r.allocUnsafe(2))[0]=196,l[1]=c.length):c.length<=65535?((l=r.allocUnsafe(3))[0]=197,l.writeUInt16BE(c.length,1)):((l=r.allocUnsafe(5))[0]=198,l.writeUInt32BE(c.length,1)),l=o([l,c]);else if(Array.isArray(c))c.length<16?(l=r.allocUnsafe(1))[0]=144|c.length:c.length<65536?((l=r.allocUnsafe(3))[0]=220,l.writeUInt16BE(c.length,1)):((l=r.allocUnsafe(5))[0]=221,l.writeUInt32BE(c.length,1)),l=c.reduce(function(e,t){return e.append(a(t,!0)),e},o().append(l));else{if(!i&&"function"==typeof c.getDate)return function(e){var t,n=1*e,i=Math.floor(n/1e3),s=1e6*(n-1e3*i);if(s||i>4294967295){(t=new r(10))[0]=215,t[1]=-1;var a=4*s,c=i/Math.pow(2,32),u=a+c&4294967295,l=4294967295&i;t.writeInt32BE(u,2),t.writeInt32BE(l,6)}else(t=new r(6))[0]=214,t[1]=-1,t.writeUInt32BE(Math.floor(n/1e3),2);return o().append(t)}(c);if("object"==typeof c)l=function(t){var n,i,s=-1,a=[];for(n=0;n>8),a.push(255&s)):(a.push(201),a.push(s>>24),a.push(s>>16&255),a.push(s>>8&255),a.push(255&s));return o().append(r.from(a)).append(i)}(c)||function(e){var t,n,i=[],s=0;for(t in e)e.hasOwnProperty(t)&&void 0!==e[t]&&"function"!=typeof e[t]&&(++s,i.push(a(t,!0)),i.push(a(e[t],!0)));s<16?(n=r.allocUnsafe(1))[0]=128|s:((n=r.allocUnsafe(3))[0]=222,n.writeUInt16BE(s,1));return i.unshift(n),i.reduce(function(e,t){return e.append(t)},o())}(c);else if("number"==typeof c){if((h=c)!==Math.floor(h))return s(c,t);if(c>=0)if(c<128)(l=r.allocUnsafe(1))[0]=c;else if(c<256)(l=r.allocUnsafe(2))[0]=204,l[1]=c;else if(c<65536)(l=r.allocUnsafe(3))[0]=205,l.writeUInt16BE(c,1);else if(c<=4294967295)(l=r.allocUnsafe(5))[0]=206,l.writeUInt32BE(c,1);else{if(!(c<=9007199254740991))return s(c,!0);(l=r.allocUnsafe(9))[0]=207,function(e,t){for(var n=7;n>=0;n--)e[n+1]=255&t,t/=256}(l,c)}else if(c>=-32)(l=r.allocUnsafe(1))[0]=256+c;else if(c>=-128)(l=r.allocUnsafe(2))[0]=208,l.writeInt8(c,1);else if(c>=-32768)(l=r.allocUnsafe(3))[0]=209,l.writeInt16BE(c,1);else if(c>-214748365)(l=r.allocUnsafe(5))[0]=210,l.writeInt32BE(c,1);else{if(!(c>=-9007199254740991))return s(c,!0);(l=r.allocUnsafe(9))[0]=211,function(e,t,n){var r=n<0;r&&(n=Math.abs(n));var o=n%4294967296,i=n/4294967296;if(e.writeUInt32BE(Math.floor(i),t+0),e.writeUInt32BE(o,t+4),r)for(var s=1,a=t+7;a>=t;a--){var c=(255^e[a])+s;e[a]=255&c,s=c>>8}}(l,1,c)}}}if(!l)throw new Error("not implemented yet");return u?l:l.slice()}return a}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function s(e){try{c(r.next(e))}catch(e){i(e)}}function a(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(s,a)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||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]this.nextBatchId?this.fatalError?(this.logger.log(a.LogLevel.Debug,"Received a new batch "+e+" but errored out on a previous batch "+(this.nextBatchId-1)),[4,n.send("OnRenderCompleted",this.nextBatchId-1,this.fatalError.toString())]):[3,4]:[3,5];case 3:return o.sent(),[2];case 4:return this.logger.log(a.LogLevel.Debug,"Waiting for batch "+this.nextBatchId+". Batch "+e+" not processed."),[2];case 5:return o.trys.push([5,7,,8]),this.nextBatchId++,this.logger.log(a.LogLevel.Debug,"Applying batch "+e+"."),i.renderBatch(this.browserRendererId,new s.OutOfProcessRenderBatch(t)),[4,this.completeBatch(n,e)];case 6:return o.sent(),[3,8];case 7:throw r=o.sent(),this.fatalError=r.toString(),this.logger.log(a.LogLevel.Error,"There was an error applying batch "+e+"."),n.send("OnRenderCompleted",e,r.toString()),r;case 8:return[2]}})})},e.prototype.getLastBatchid=function(){return this.nextBatchId-1},e.prototype.completeBatch=function(e,t){return r(this,void 0,void 0,function(){return o(this,function(n){switch(n.label){case 0:return n.trys.push([0,2,,3]),[4,e.send("OnRenderCompleted",t,null)];case 1:return n.sent(),[3,3];case 2:return n.sent(),this.logger.log(a.LogLevel.Warning,"Failed to deliver completion notification for render '"+t+"'."),[3,3];case 3:return[2]}})})},e.renderQueues=new Map,e}();t.RenderQueue=c},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(72),o=Math.pow(2,32),i=Math.pow(2,21)-1,s=function(){function e(e){this.batchData=e;var t=new l(e);this.arrayRangeReader=new f(e),this.arrayBuilderSegmentReader=new h(e),this.diffReader=new a(e),this.editReader=new c(e,t),this.frameReader=new u(e,t)}return e.prototype.updatedComponents=function(){return p(this.batchData,this.batchData.length-20)},e.prototype.referenceFrames=function(){return p(this.batchData,this.batchData.length-16)},e.prototype.disposedComponentIds=function(){return p(this.batchData,this.batchData.length-12)},e.prototype.disposedEventHandlerIds=function(){return p(this.batchData,this.batchData.length-8)},e.prototype.updatedComponentsEntry=function(e,t){var n=e+4*t;return p(this.batchData,n)},e.prototype.referenceFramesEntry=function(e,t){return e+20*t},e.prototype.disposedComponentIdsEntry=function(e,t){var n=e+4*t;return p(this.batchData,n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=e+8*t;return g(this.batchData,n)},e}();t.OutOfProcessRenderBatch=s;var a=function(){function e(e){this.batchDataUint8=e}return e.prototype.componentId=function(e){return p(this.batchDataUint8,e)},e.prototype.edits=function(e){return e+4},e.prototype.editsEntry=function(e,t){return e+16*t},e}(),c=function(){function e(e,t){this.batchDataUint8=e,this.stringReader=t}return e.prototype.editType=function(e){return p(this.batchDataUint8,e)},e.prototype.siblingIndex=function(e){return p(this.batchDataUint8,e+4)},e.prototype.newTreeIndex=function(e){return p(this.batchDataUint8,e+8)},e.prototype.moveToSiblingIndex=function(e){return p(this.batchDataUint8,e+8)},e.prototype.removedAttributeName=function(e){var t=p(this.batchDataUint8,e+12);return this.stringReader.readString(t)},e}(),u=function(){function e(e,t){this.batchDataUint8=e,this.stringReader=t}return e.prototype.frameType=function(e){return p(this.batchDataUint8,e)},e.prototype.subtreeLength=function(e){return p(this.batchDataUint8,e+4)},e.prototype.elementReferenceCaptureId=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.componentId=function(e){return p(this.batchDataUint8,e+8)},e.prototype.elementName=function(e){var t=p(this.batchDataUint8,e+8);return this.stringReader.readString(t)},e.prototype.textContent=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.markupContent=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.attributeName=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.attributeValue=function(e){var t=p(this.batchDataUint8,e+8);return this.stringReader.readString(t)},e.prototype.attributeEventHandlerId=function(e){return g(this.batchDataUint8,e+12)},e}(),l=function(){function e(e){this.batchDataUint8=e,this.stringTableStartIndex=p(e,e.length-4)}return e.prototype.readString=function(e){if(-1===e)return null;var t,n=p(this.batchDataUint8,this.stringTableStartIndex+4*e),o=function(e,t){for(var n=0,r=0,o=0;o<4;o++){var i=e[t+o];if(n|=(127&i)<>>0)}function g(e,t){var n=d(e,t+4);if(n>i)throw new Error("Cannot read uint64 with high order part "+n+", because the result would exceed Number.MAX_SAFE_INTEGER.");return n*o+d(e,t)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r="function"==typeof TextDecoder?new TextDecoder("utf-8"):null;t.decodeUtf8=r?r.decode.bind(r):function(e){var t=0,n=e.length,r=[],o=[];for(;t65535&&(u-=65536,r.push(u>>>10&1023|55296),u=56320|1023&u),r.push(u)}r.length>1024&&(o.push(String.fromCharCode.apply(null,r)),r.length=0)}return o.push(String.fromCharCode.apply(null,r)),o.join("")}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(20),o=function(){function e(){}return e.prototype.log=function(e,t){},e.instance=new e,e}();t.NullLogger=o;var i=function(){function e(e){this.minimumLogLevel=e}return e.prototype.log=function(e,t){if(e>=this.minimumLogLevel)switch(e){case r.LogLevel.Critical:case r.LogLevel.Error:console.error("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;case r.LogLevel.Warning:console.warn("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;case r.LogLevel.Information:console.info("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;default:console.log("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t)}},e}();t.ConsoleLogger=i},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function s(e){try{c(r.next(e))}catch(e){i(e)}}function a(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(s,a)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||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]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]>=7)>0&&(r|=128),n.push(r)}while(t>0);t=e.byteLength||e.length;var o=new Uint8Array(n.length+t);return o.set(n,0),o.set(e,n.length),o.buffer},e.parse=function(e){for(var t=[],n=new Uint8Array(e),r=[0,7,14,21,28],o=0;o7)throw new Error("Messages bigger than 2GB are not supported.");if(!(n.byteLength>=o+i+s))throw new Error("Incomplete message.");t.push(n.slice?n.slice(o+i,o+i+s):n.subarray(o+i,o+i+s)),o=o+i+s}return t},e}();var a=new Uint8Array([145,i.MessageType.Ping]),c=function(){function e(){this.name="messagepack",this.version=1,this.transferFormat=i.TransferFormat.Binary,this.errorResult=1,this.voidResult=2,this.nonVoidResult=3}return e.prototype.parseMessages=function(e,t){if(!(e instanceof r.Buffer||(n=e,n&&"undefined"!=typeof ArrayBuffer&&(n instanceof ArrayBuffer||n.constructor&&"ArrayBuffer"===n.constructor.name))))throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer or Buffer.");var n;null===t&&(t=i.NullLogger.instance);for(var o=[],a=0,c=s.parse(e);a=0;u--)if(l[u]!==f[u])return!1;for(u=l.length-1;u>=0;u--)if(c=l[u],!b(e[c],t[c],n,r))return!1;return!0}(e,t,n,a))}return n?e===t:e==t}function m(e){return"[object Arguments]"==Object.prototype.toString.call(e)}function w(e,t){if(!e||!t)return!1;if("[object RegExp]"==Object.prototype.toString.call(t))return t.test(e);try{if(e instanceof t)return!0}catch(e){}return!Error.isPrototypeOf(t)&&!0===t.call({},e)}function E(e,t,n,r){var o;if("function"!=typeof t)throw new TypeError('"block" argument must be a function');"string"==typeof n&&(r=n,n=null),o=function(e){var t;try{e()}catch(e){t=e}return t}(t),r=(n&&n.name?" ("+n.name+").":".")+(r?" "+r:"."),e&&!o&&y(o,n,"Missing expected exception"+r);var a="string"==typeof r,s=!e&&o&&!n;if((!e&&i.isError(o)&&a&&w(o,n)||s)&&y(o,n,"Got unwanted exception"+r),e&&o&&n&&!w(o,n)||!e&&o)throw o}f.AssertionError=function(e){var t;this.name="AssertionError",this.actual=e.actual,this.expected=e.expected,this.operator=e.operator,e.message?(this.message=e.message,this.generatedMessage=!1):(this.message=d(g((t=this).actual),128)+" "+t.operator+" "+d(g(t.expected),128),this.generatedMessage=!0);var n=e.stackStartFunction||y;if(Error.captureStackTrace)Error.captureStackTrace(this,n);else{var r=new Error;if(r.stack){var o=r.stack,i=p(n),a=o.indexOf("\n"+i);if(a>=0){var s=o.indexOf("\n",a+1);o=o.substring(s+1)}this.stack=o}}},i.inherits(f.AssertionError,Error),f.fail=y,f.ok=v,f.equal=function(e,t,n){e!=t&&y(e,t,n,"==",f.equal)},f.notEqual=function(e,t,n){e==t&&y(e,t,n,"!=",f.notEqual)},f.deepEqual=function(e,t,n){b(e,t,!1)||y(e,t,n,"deepEqual",f.deepEqual)},f.deepStrictEqual=function(e,t,n){b(e,t,!0)||y(e,t,n,"deepStrictEqual",f.deepStrictEqual)},f.notDeepEqual=function(e,t,n){b(e,t,!1)&&y(e,t,n,"notDeepEqual",f.notDeepEqual)},f.notDeepStrictEqual=function e(t,n,r){b(t,n,!0)&&y(t,n,r,"notDeepStrictEqual",e)},f.strictEqual=function(e,t,n){e!==t&&y(e,t,n,"===",f.strictEqual)},f.notStrictEqual=function(e,t,n){e===t&&y(e,t,n,"!==",f.notStrictEqual)},f.throws=function(e,t,n){E(!0,e,t,n)},f.doesNotThrow=function(e,t,n){E(!1,e,t,n)},f.ifError=function(e){if(e)throw e};var S=Object.keys||function(e){var t=[];for(var n in e)a.call(e,n)&&t.push(n);return t}}).call(this,n(10))},function(e,t){e.exports=function(e){return e&&"object"==typeof e&&"function"==typeof e.copy&&"function"==typeof e.fill&&"function"==typeof e.readUInt8}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){e.exports=n(11)},function(e,t){var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},function(e,t){},function(e,t,n){"use strict";var r=n(13).Buffer,o=n(60);e.exports=function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.head=null,this.tail=null,this.length=0}return e.prototype.push=function(e){var t={data:e,next:null};this.length>0?this.tail.next=t:this.head=t,this.tail=t,++this.length},e.prototype.unshift=function(e){var t={data:e,next:this.head};0===this.length&&(this.tail=t),this.head=t,++this.length},e.prototype.shift=function(){if(0!==this.length){var e=this.head.data;return 1===this.length?this.head=this.tail=null:this.head=this.head.next,--this.length,e}},e.prototype.clear=function(){this.head=this.tail=null,this.length=0},e.prototype.join=function(e){if(0===this.length)return"";for(var t=this.head,n=""+t.data;t=t.next;)n+=e+t.data;return n},e.prototype.concat=function(e){if(0===this.length)return r.alloc(0);if(1===this.length)return this.head.data;for(var t,n,o,i=r.allocUnsafe(e>>>0),a=this.head,s=0;a;)t=a.data,n=i,o=s,t.copy(n,o),s+=a.data.length,a=a.next;return i},e}(),o&&o.inspect&&o.inspect.custom&&(e.exports.prototype[o.inspect.custom]=function(){var e=o.inspect({length:this.length});return this.constructor.name+" "+e})},function(e,t){},function(e,t,n){var r=n(5),o=r.Buffer;function i(e,t){for(var n in e)t[n]=e[n]}function a(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(i(r,t),t.Buffer=a),i(o,a),a.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},a.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},a.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},a.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){(function(e){var r=void 0!==e&&e||"undefined"!=typeof self&&self||window,o=Function.prototype.apply;function i(e,t){this._id=e,this._clearFn=t}t.setTimeout=function(){return new i(o.call(setTimeout,r,arguments),clearTimeout)},t.setInterval=function(){return new i(o.call(setInterval,r,arguments),clearInterval)},t.clearTimeout=t.clearInterval=function(e){e&&e.close()},i.prototype.unref=i.prototype.ref=function(){},i.prototype.close=function(){this._clearFn.call(r,this._id)},t.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},t.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},t._unrefActive=t.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;t>=0&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},n(63),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(this,n(10))},function(e,t,n){(function(e,t){!function(e,n){"use strict";if(!e.setImmediate){var r,o,i,a,s,c=1,u={},l=!1,f=e.document,h=Object.getPrototypeOf&&Object.getPrototypeOf(e);h=h&&h.setTimeout?h:e,"[object process]"==={}.toString.call(e.process)?r=function(e){t.nextTick(function(){d(e)})}:!function(){if(e.postMessage&&!e.importScripts){var t=!0,n=e.onmessage;return e.onmessage=function(){t=!1},e.postMessage("","*"),e.onmessage=n,t}}()?e.MessageChannel?((i=new MessageChannel).port1.onmessage=function(e){d(e.data)},r=function(e){i.port2.postMessage(e)}):f&&"onreadystatechange"in f.createElement("script")?(o=f.documentElement,r=function(e){var t=f.createElement("script");t.onreadystatechange=function(){d(e),t.onreadystatechange=null,o.removeChild(t),t=null},o.appendChild(t)}):r=function(e){setTimeout(d,0,e)}:(a="setImmediate$"+Math.random()+"$",s=function(t){t.source===e&&"string"==typeof t.data&&0===t.data.indexOf(a)&&d(+t.data.slice(a.length))},e.addEventListener?e.addEventListener("message",s,!1):e.attachEvent("onmessage",s),r=function(t){e.postMessage(a+t,"*")}),h.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n0?this._transform(null,t,n):n()},e.exports.decoder=c,e.exports.encoder=s},function(e,t,n){(t=e.exports=n(36)).Stream=t,t.Readable=t,t.Writable=n(41),t.Duplex=n(11),t.Transform=n(42),t.PassThrough=n(67)},function(e,t,n){"use strict";e.exports=i;var r=n(42),o=n(19);function i(e){if(!(this instanceof i))return new i(e);r.call(this,e)}o.inherits=n(14),o.inherits(i,r),i.prototype._transform=function(e,t,n){n(null,e)}},function(e,t,n){var r=n(22);function o(e){Error.call(this),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor),this.name=this.constructor.name,this.message=e||"unable to decode"}n(35).inherits(o,Error),e.exports=function(e){return function(e){e instanceof r||(e=r().append(e));var t=i(e);if(t)return e.consume(t.bytesConsumed),t.value;throw new o};function t(e,t,n){return t>=n+e}function n(e,t){return{value:e,bytesConsumed:t}}function i(e,r){r=void 0===r?0:r;var o=e.length-r;if(o<=0)return null;var i,l,f,h=e.readUInt8(r),p=0;if(!function(e,t){var n=function(e){switch(e){case 196:return 2;case 197:return 3;case 198:return 5;case 199:return 3;case 200:return 4;case 201:return 6;case 202:return 5;case 203:return 9;case 204:return 2;case 205:return 3;case 206:return 5;case 207:return 9;case 208:return 2;case 209:return 3;case 210:return 5;case 211:return 9;case 212:return 3;case 213:return 4;case 214:return 6;case 215:return 10;case 216:return 18;case 217:return 2;case 218:return 3;case 219:return 5;case 222:return 3;default:return-1}}(e);return!(-1!==n&&t=0;f--)p+=e.readUInt8(r+f+1)*Math.pow(2,8*(7-f));return n(p,9);case 208:return n(p=e.readInt8(r+1),2);case 209:return n(p=e.readInt16BE(r+1),3);case 210:return n(p=e.readInt32BE(r+1),5);case 211:return n(p=function(e,t){var n=128==(128&e[t]);if(n)for(var r=1,o=t+7;o>=t;o--){var i=(255^e[o])+r;e[o]=255&i,r=i>>8}var a=e.readUInt32BE(t+0),s=e.readUInt32BE(t+4);return(4294967296*a+s)*(n?-1:1)}(e.slice(r+1,r+9),0),9);case 202:return n(p=e.readFloatBE(r+1),5);case 203:return n(p=e.readDoubleBE(r+1),9);case 217:return t(i=e.readUInt8(r+1),o,2)?n(p=e.toString("utf8",r+2,r+2+i),2+i):null;case 218:return t(i=e.readUInt16BE(r+1),o,3)?n(p=e.toString("utf8",r+3,r+3+i),3+i):null;case 219:return t(i=e.readUInt32BE(r+1),o,5)?n(p=e.toString("utf8",r+5,r+5+i),5+i):null;case 196:return t(i=e.readUInt8(r+1),o,2)?n(p=e.slice(r+2,r+2+i),2+i):null;case 197:return t(i=e.readUInt16BE(r+1),o,3)?n(p=e.slice(r+3,r+3+i),3+i):null;case 198:return t(i=e.readUInt32BE(r+1),o,5)?n(p=e.slice(r+5,r+5+i),5+i):null;case 220:return o<3?null:(i=e.readUInt16BE(r+1),a(e,r,i,3));case 221:return o<5?null:(i=e.readUInt32BE(r+1),a(e,r,i,5));case 222:return i=e.readUInt16BE(r+1),s(e,r,i,3);case 223:throw new Error("map too big to decode in JS");case 212:return c(e,r,1);case 213:return c(e,r,2);case 214:return c(e,r,4);case 215:return c(e,r,8);case 216:return c(e,r,16);case 199:return i=e.readUInt8(r+1),l=e.readUInt8(r+2),t(i,o,3)?u(e,r,l,i,3):null;case 200:return i=e.readUInt16BE(r+1),l=e.readUInt8(r+3),t(i,o,4)?u(e,r,l,i,4):null;case 201:return i=e.readUInt32BE(r+1),l=e.readUInt8(r+5),t(i,o,6)?u(e,r,l,i,6):null}if(144==(240&h))return a(e,r,i=15&h,1);if(128==(240&h))return s(e,r,i=15&h,1);if(160==(224&h))return t(i=31&h,o,1)?n(p=e.toString("utf8",r+1,r+i+1),i+1):null;if(h>=224)return n(p=h-256,1);if(h<128)return n(h,1);throw new Error("not implemented yet")}function a(e,t,r,o){var a,s=[],c=0;for(t+=o,a=0;ai)&&((n=r.allocUnsafe(9))[0]=203,n.writeDoubleBE(e,1)),n}e.exports=function(e,t,n,i){function s(c,u){var l,f,h;if(void 0===c)throw new Error("undefined is not encodable in msgpack!");if(null===c)(l=r.allocUnsafe(1))[0]=192;else if(!0===c)(l=r.allocUnsafe(1))[0]=195;else if(!1===c)(l=r.allocUnsafe(1))[0]=194;else if("string"==typeof c)(f=r.byteLength(c))<32?((l=r.allocUnsafe(1+f))[0]=160|f,f>0&&l.write(c,1)):f<=255&&!n?((l=r.allocUnsafe(2+f))[0]=217,l[1]=f,l.write(c,2)):f<=65535?((l=r.allocUnsafe(3+f))[0]=218,l.writeUInt16BE(f,1),l.write(c,3)):((l=r.allocUnsafe(5+f))[0]=219,l.writeUInt32BE(f,1),l.write(c,5));else if(c&&(c.readUInt32LE||c instanceof Uint8Array))c instanceof Uint8Array&&(c=r.from(c)),c.length<=255?((l=r.allocUnsafe(2))[0]=196,l[1]=c.length):c.length<=65535?((l=r.allocUnsafe(3))[0]=197,l.writeUInt16BE(c.length,1)):((l=r.allocUnsafe(5))[0]=198,l.writeUInt32BE(c.length,1)),l=o([l,c]);else if(Array.isArray(c))c.length<16?(l=r.allocUnsafe(1))[0]=144|c.length:c.length<65536?((l=r.allocUnsafe(3))[0]=220,l.writeUInt16BE(c.length,1)):((l=r.allocUnsafe(5))[0]=221,l.writeUInt32BE(c.length,1)),l=c.reduce(function(e,t){return e.append(s(t,!0)),e},o().append(l));else{if(!i&&"function"==typeof c.getDate)return function(e){var t,n=1*e,i=Math.floor(n/1e3),a=1e6*(n-1e3*i);if(a||i>4294967295){(t=new r(10))[0]=215,t[1]=-1;var s=4*a,c=i/Math.pow(2,32),u=s+c&4294967295,l=4294967295&i;t.writeInt32BE(u,2),t.writeInt32BE(l,6)}else(t=new r(6))[0]=214,t[1]=-1,t.writeUInt32BE(Math.floor(n/1e3),2);return o().append(t)}(c);if("object"==typeof c)l=function(t){var n,i,a=-1,s=[];for(n=0;n>8),s.push(255&a)):(s.push(201),s.push(a>>24),s.push(a>>16&255),s.push(a>>8&255),s.push(255&a));return o().append(r.from(s)).append(i)}(c)||function(e){var t,n,i=[],a=0;for(t in e)e.hasOwnProperty(t)&&void 0!==e[t]&&"function"!=typeof e[t]&&(++a,i.push(s(t,!0)),i.push(s(e[t],!0)));a<16?(n=r.allocUnsafe(1))[0]=128|a:((n=r.allocUnsafe(3))[0]=222,n.writeUInt16BE(a,1));return i.unshift(n),i.reduce(function(e,t){return e.append(t)},o())}(c);else if("number"==typeof c){if((h=c)!==Math.floor(h))return a(c,t);if(c>=0)if(c<128)(l=r.allocUnsafe(1))[0]=c;else if(c<256)(l=r.allocUnsafe(2))[0]=204,l[1]=c;else if(c<65536)(l=r.allocUnsafe(3))[0]=205,l.writeUInt16BE(c,1);else if(c<=4294967295)(l=r.allocUnsafe(5))[0]=206,l.writeUInt32BE(c,1);else{if(!(c<=9007199254740991))return a(c,!0);(l=r.allocUnsafe(9))[0]=207,function(e,t){for(var n=7;n>=0;n--)e[n+1]=255&t,t/=256}(l,c)}else if(c>=-32)(l=r.allocUnsafe(1))[0]=256+c;else if(c>=-128)(l=r.allocUnsafe(2))[0]=208,l.writeInt8(c,1);else if(c>=-32768)(l=r.allocUnsafe(3))[0]=209,l.writeInt16BE(c,1);else if(c>-214748365)(l=r.allocUnsafe(5))[0]=210,l.writeInt32BE(c,1);else{if(!(c>=-9007199254740991))return a(c,!0);(l=r.allocUnsafe(9))[0]=211,function(e,t,n){var r=n<0;r&&(n=Math.abs(n));var o=n%4294967296,i=n/4294967296;if(e.writeUInt32BE(Math.floor(i),t+0),e.writeUInt32BE(o,t+4),r)for(var a=1,s=t+7;s>=t;s--){var c=(255^e[s])+a;e[s]=255&c,a=c>>8}}(l,1,c)}}}if(!l)throw new Error("not implemented yet");return u?l:l.slice()}return s}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)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 a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]this.nextBatchId?this.fatalError?(this.logger.log(s.LogLevel.Debug,"Received a new batch "+e+" but errored out on a previous batch "+(this.nextBatchId-1)),[4,n.send("OnRenderCompleted",this.nextBatchId-1,this.fatalError.toString())]):[3,4]:[3,5];case 3:return o.sent(),[2];case 4:return this.logger.log(s.LogLevel.Debug,"Waiting for batch "+this.nextBatchId+". Batch "+e+" not processed."),[2];case 5:return o.trys.push([5,7,,8]),this.nextBatchId++,this.logger.log(s.LogLevel.Debug,"Applying batch "+e+"."),i.renderBatch(this.browserRendererId,new a.OutOfProcessRenderBatch(t)),[4,this.completeBatch(n,e)];case 6:return o.sent(),[3,8];case 7:throw r=o.sent(),this.fatalError=r.toString(),this.logger.log(s.LogLevel.Error,"There was an error applying batch "+e+"."),n.send("OnRenderCompleted",e,r.toString()),r;case 8:return[2]}})})},e.prototype.getLastBatchid=function(){return this.nextBatchId-1},e.prototype.completeBatch=function(e,t){return r(this,void 0,void 0,function(){return o(this,function(n){switch(n.label){case 0:return n.trys.push([0,2,,3]),[4,e.send("OnRenderCompleted",t,null)];case 1:return n.sent(),[3,3];case 2:return n.sent(),this.logger.log(s.LogLevel.Warning,"Failed to deliver completion notification for render '"+t+"'."),[3,3];case 3:return[2]}})})},e}();t.RenderQueue=c},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(72),o=Math.pow(2,32),i=Math.pow(2,21)-1,a=function(){function e(e){this.batchData=e;var t=new l(e);this.arrayRangeReader=new f(e),this.arrayBuilderSegmentReader=new h(e),this.diffReader=new s(e),this.editReader=new c(e,t),this.frameReader=new u(e,t)}return e.prototype.updatedComponents=function(){return p(this.batchData,this.batchData.length-20)},e.prototype.referenceFrames=function(){return p(this.batchData,this.batchData.length-16)},e.prototype.disposedComponentIds=function(){return p(this.batchData,this.batchData.length-12)},e.prototype.disposedEventHandlerIds=function(){return p(this.batchData,this.batchData.length-8)},e.prototype.updatedComponentsEntry=function(e,t){var n=e+4*t;return p(this.batchData,n)},e.prototype.referenceFramesEntry=function(e,t){return e+20*t},e.prototype.disposedComponentIdsEntry=function(e,t){var n=e+4*t;return p(this.batchData,n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=e+8*t;return g(this.batchData,n)},e}();t.OutOfProcessRenderBatch=a;var s=function(){function e(e){this.batchDataUint8=e}return e.prototype.componentId=function(e){return p(this.batchDataUint8,e)},e.prototype.edits=function(e){return e+4},e.prototype.editsEntry=function(e,t){return e+16*t},e}(),c=function(){function e(e,t){this.batchDataUint8=e,this.stringReader=t}return e.prototype.editType=function(e){return p(this.batchDataUint8,e)},e.prototype.siblingIndex=function(e){return p(this.batchDataUint8,e+4)},e.prototype.newTreeIndex=function(e){return p(this.batchDataUint8,e+8)},e.prototype.moveToSiblingIndex=function(e){return p(this.batchDataUint8,e+8)},e.prototype.removedAttributeName=function(e){var t=p(this.batchDataUint8,e+12);return this.stringReader.readString(t)},e}(),u=function(){function e(e,t){this.batchDataUint8=e,this.stringReader=t}return e.prototype.frameType=function(e){return p(this.batchDataUint8,e)},e.prototype.subtreeLength=function(e){return p(this.batchDataUint8,e+4)},e.prototype.elementReferenceCaptureId=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.componentId=function(e){return p(this.batchDataUint8,e+8)},e.prototype.elementName=function(e){var t=p(this.batchDataUint8,e+8);return this.stringReader.readString(t)},e.prototype.textContent=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.markupContent=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.attributeName=function(e){var t=p(this.batchDataUint8,e+4);return this.stringReader.readString(t)},e.prototype.attributeValue=function(e){var t=p(this.batchDataUint8,e+8);return this.stringReader.readString(t)},e.prototype.attributeEventHandlerId=function(e){return g(this.batchDataUint8,e+12)},e}(),l=function(){function e(e){this.batchDataUint8=e,this.stringTableStartIndex=p(e,e.length-4)}return e.prototype.readString=function(e){if(-1===e)return null;var t,n=p(this.batchDataUint8,this.stringTableStartIndex+4*e),o=function(e,t){for(var n=0,r=0,o=0;o<4;o++){var i=e[t+o];if(n|=(127&i)<>>0)}function g(e,t){var n=d(e,t+4);if(n>i)throw new Error("Cannot read uint64 with high order part "+n+", because the result would exceed Number.MAX_SAFE_INTEGER.");return n*o+d(e,t)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r="function"==typeof TextDecoder?new TextDecoder("utf-8"):null;t.decodeUtf8=r?r.decode.bind(r):function(e){var t=0,n=e.length,r=[],o=[];for(;t65535&&(u-=65536,r.push(u>>>10&1023|55296),u=56320|1023&u),r.push(u)}r.length>1024&&(o.push(String.fromCharCode.apply(null,r)),r.length=0)}return o.push(String.fromCharCode.apply(null,r)),o.join("")}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(20),o=function(){function e(){}return e.prototype.log=function(e,t){},e.instance=new e,e}();t.NullLogger=o;var i=function(){function e(e){this.minimumLogLevel=e}return e.prototype.log=function(e,t){if(e>=this.minimumLogLevel)switch(e){case r.LogLevel.Critical:case r.LogLevel.Error:console.error("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;case r.LogLevel.Warning:console.warn("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;case r.LogLevel.Information:console.info("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t);break;default:console.log("["+(new Date).toISOString()+"] "+r.LogLevel[e]+": "+t)}},e}();t.ConsoleLogger=i},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{c(r.next(e))}catch(e){i(e)}}function s(e){try{c(r.throw(e))}catch(e){i(e)}}function c(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,s)}c((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function s(i){return function(s){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)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 a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]>=7)>0&&(r|=128),n.push(r)}while(t>0);t=e.byteLength||e.length;var o=new Uint8Array(n.length+t);return o.set(n,0),o.set(e,n.length),o.buffer},e.parse=function(e){for(var t=[],n=new Uint8Array(e),r=[0,7,14,21,28],o=0;o7)throw new Error("Messages bigger than 2GB are not supported.");if(!(n.byteLength>=o+i+a))throw new Error("Incomplete message.");t.push(n.slice?n.slice(o+i,o+i+a):n.subarray(o+i,o+i+a)),o=o+i+a}return t},e}();var s=new Uint8Array([145,i.MessageType.Ping]),c=function(){function e(){this.name="messagepack",this.version=1,this.transferFormat=i.TransferFormat.Binary,this.errorResult=1,this.voidResult=2,this.nonVoidResult=3}return e.prototype.parseMessages=function(e,t){if(!(e instanceof r.Buffer||(n=e,n&&"undefined"!=typeof ArrayBuffer&&(n instanceof ArrayBuffer||n.constructor&&"ArrayBuffer"===n.constructor.name))))throw new Error("Invalid input for MessagePack hub protocol. Expected an ArrayBuffer or Buffer.");var n;null===t&&(t=i.NullLogger.instance);for(var o=[],s=0,c=a.parse(e);s0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function u(e,t,n){var a=e;if(e instanceof Comment&&(s(a)&&s(a).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(l(a))throw new Error("Not implemented: moving existing logical children");var i=s(t);if(n0;)e(r,0);var a=r;a.parentNode.removeChild(a)},t.getLogicalParent=l,t.getLogicalSiblingEnd=function(e){return e[a]||null},t.getLogicalChild=function(e,t){return s(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===c(e).namespaceURI},t.getLogicalChildrenArray=s,t.permuteLogicalChildren=function(e,t){var n=s(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=l(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):d(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,a=r;a;){var i=a.nextSibling;if(n.insertBefore(a,t),a===o)break;a=i}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=c},function(e,t,n){"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),t.dispatchEvent=function(e,t){if(!r)throw new Error("eventDispatcher not initialized. Call 'setEventDispatcher' to configure it.");return r(e,t)},t.setEventDispatcher=function(e){r=e}},,,,function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,a){function i(e){try{l(r.next(e))}catch(e){a(e)}}function u(e){try{l(r.throw(e))}catch(e){a(e)}}function l(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,u)}l((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(a){return function(u){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,r=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]-1?a.substring(0,u):"",s=u>-1?a.substring(u+1):a,c=t.monoPlatform.findMethod(e,l,s,i);t.monoPlatform.callMethod(c,null,r)},callMethod:function(e,n,r){if(r.length>4)throw new Error("Currently, MonoPlatform supports passing a maximum of 4 arguments from JS to .NET. You tried to pass "+r.length+".");var o=Module.stackSave();try{for(var a=Module.stackAlloc(r.length),u=Module.stackAlloc(4),l=0;l>2,r=Module.HEAPU32[n+1];if(r>v)throw new Error("Cannot read uint64 with high order part "+r+", because the result would exceed Number.MAX_SAFE_INTEGER.");return r*h+Module.HEAPU32[n]},readFloatField:function(e,t){return Module.getValue(e+(t||0),"float")},readObjectField:function(e,t){return Module.getValue(e+(t||0),"i32")},readStringField:function(e,n){var r=Module.getValue(e+(n||0),"i32");return 0===r?null:t.monoPlatform.toJavaScriptString(r)},readStructField:function(e,t){return e+(t||0)}};var b=document.createElement("a");function w(e){return e+12}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(33),o=window.chrome&&navigator.userAgent.indexOf("Edge")<0,a=!1;function i(){return a&&o}t.hasDebuggingEnabled=i,t.attachDebuggerHotkey=function(e){a=e.some(function(e){return/\.pdb$/.test(r.getFileNameFromUrl(e))});var t=navigator.platform.match(/^Mac/i)?"Cmd":"Alt";i()&&console.info("Debugging hotkey: Shift+"+t+"+D (when application has focus)"),document.addEventListener("keydown",function(e){var t;e.shiftKey&&(e.metaKey||e.altKey)&&"KeyD"===e.code&&(a?o?((t=document.createElement("a")).href="_framework/debug?url="+encodeURIComponent(location.href),t.target="_blank",t.rel="noopener noreferrer",t.click()):console.error("Currently, only Chrome is supported for debugging."):console.error("Cannot start debugging, because the application was not compiled with debugging enabled."))})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(9),o=function(){function e(e){this.batchAddress=e,this.arrayRangeReader=a,this.arrayBuilderSegmentReader=i,this.diffReader=u,this.editReader=l,this.frameReader=s}return e.prototype.updatedComponents=function(){return r.platform.readStructField(this.batchAddress,0)},e.prototype.referenceFrames=function(){return r.platform.readStructField(this.batchAddress,a.structLength)},e.prototype.disposedComponentIds=function(){return r.platform.readStructField(this.batchAddress,2*a.structLength)},e.prototype.disposedEventHandlerIds=function(){return r.platform.readStructField(this.batchAddress,3*a.structLength)},e.prototype.updatedComponentsEntry=function(e,t){return c(e,t,u.structLength)},e.prototype.referenceFramesEntry=function(e,t){return c(e,t,s.structLength)},e.prototype.disposedComponentIdsEntry=function(e,t){var n=c(e,t,4);return r.platform.readInt32Field(n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=c(e,t,8);return r.platform.readUint64Field(n)},e}();t.SharedMemoryRenderBatch=o;var a={structLength:8,values:function(e){return r.platform.readObjectField(e,0)},count:function(e){return r.platform.readInt32Field(e,4)}},i={structLength:12,values:function(e){var t=r.platform.readObjectField(e,0),n=r.platform.getObjectFieldsBaseAddress(t);return r.platform.readObjectField(n,0)},offset:function(e){return r.platform.readInt32Field(e,4)},count:function(e){return r.platform.readInt32Field(e,8)}},u={structLength:4+i.structLength,componentId:function(e){return r.platform.readInt32Field(e,0)},edits:function(e){return r.platform.readStructField(e,4)},editsEntry:function(e,t){return c(e,t,l.structLength)}},l={structLength:20,editType:function(e){return r.platform.readInt32Field(e,0)},siblingIndex:function(e){return r.platform.readInt32Field(e,4)},newTreeIndex:function(e){return r.platform.readInt32Field(e,8)},moveToSiblingIndex:function(e){return r.platform.readInt32Field(e,8)},removedAttributeName:function(e){return r.platform.readStringField(e,16)}},s={structLength:36,frameType:function(e){return r.platform.readInt16Field(e,4)},subtreeLength:function(e){return r.platform.readInt32Field(e,8)},elementReferenceCaptureId:function(e){return r.platform.readStringField(e,16)},componentId:function(e){return r.platform.readInt32Field(e,12)},elementName:function(e){return r.platform.readStringField(e,16)},textContent:function(e){return r.platform.readStringField(e,16)},markupContent:function(e){return r.platform.readStringField(e,16)},attributeName:function(e){return r.platform.readStringField(e,16)},attributeValue:function(e){return r.platform.readStringField(e,24)},attributeEventHandlerId:function(e){return r.platform.readUint64Field(e,8)}};function c(e,t,n){return r.platform.getArrayEntryPtr(e,t,n)}}]); \ No newline at end of file +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=45)}([,,,,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(25),n(9);var r=n(26),o=n(16),a={},i=!1;function u(e,t,n){var o=a[e];o||(o=a[e]=new r.BrowserRenderer(e)),o.attachRootComponentToLogicalElement(n,t)}t.attachRootComponentToLogicalElement=u,t.attachRootComponentToElement=function(e,t,n){var r=document.querySelector(e);if(!r)throw new Error("Could not find any element matching selector '"+e+"'.");u(n||0,o.toLogicalElement(r,!0),t)},t.renderBatch=function(e,t){var n=a[e];if(!n)throw new Error("There is no browser renderer with ID "+e+".");for(var r=t.arrayRangeReader,o=t.updatedComponents(),u=r.values(o),l=r.count(o),s=t.referenceFrames(),c=r.values(s),f=t.diffReader,d=0;d0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function u(e,t,n){var a=e;if(e instanceof Comment&&(s(a)&&s(a).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(l(a))throw new Error("Not implemented: moving existing logical children");var i=s(t);if(n0;)e(r,0);var a=r;a.parentNode.removeChild(a)},t.getLogicalParent=l,t.getLogicalSiblingEnd=function(e){return e[a]||null},t.getLogicalChild=function(e,t){return s(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===c(e).namespaceURI},t.getLogicalChildrenArray=s,t.permuteLogicalChildren=function(e,t){var n=s(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=l(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):d(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,a=r;a;){var i=a.nextSibling;if(n.insertBefore(a,t),a===o)break;a=i}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=c},function(e,t,n){"use strict";var r;Object.defineProperty(t,"__esModule",{value:!0}),t.dispatchEvent=function(e,t){if(!r)throw new Error("eventDispatcher not initialized. Call 'setEventDispatcher' to configure it.");return r(e,t)},t.setEventDispatcher=function(e){r=e}},,,,function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,a){function i(e){try{l(r.next(e))}catch(e){a(e)}}function u(e){try{l(r.throw(e))}catch(e){a(e)}}function l(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,u)}l((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(a){return function(u){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,r=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]-1?a.substring(0,u):"",s=u>-1?a.substring(u+1):a,c=t.monoPlatform.findMethod(e,l,s,i);t.monoPlatform.callMethod(c,null,r)},callMethod:function(e,n,r){if(r.length>4)throw new Error("Currently, MonoPlatform supports passing a maximum of 4 arguments from JS to .NET. You tried to pass "+r.length+".");var o=Module.stackSave();try{for(var a=Module.stackAlloc(r.length),u=Module.stackAlloc(4),l=0;l>2,r=Module.HEAPU32[n+1];if(r>v)throw new Error("Cannot read uint64 with high order part "+r+", because the result would exceed Number.MAX_SAFE_INTEGER.");return r*h+Module.HEAPU32[n]},readFloatField:function(e,t){return Module.getValue(e+(t||0),"float")},readObjectField:function(e,t){return Module.getValue(e+(t||0),"i32")},readStringField:function(e,n){var r=Module.getValue(e+(n||0),"i32");return 0===r?null:t.monoPlatform.toJavaScriptString(r)},readStructField:function(e,t){return e+(t||0)}};var b=document.createElement("a");function w(e){return e+12}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(33),o=window.chrome&&navigator.userAgent.indexOf("Edge")<0,a=!1;function i(){return a&&o}t.hasDebuggingEnabled=i,t.attachDebuggerHotkey=function(e){a=e.some(function(e){return/\.pdb$/.test(r.getFileNameFromUrl(e))});var t=navigator.platform.match(/^Mac/i)?"Cmd":"Alt";i()&&console.info("Debugging hotkey: Shift+"+t+"+D (when application has focus)"),document.addEventListener("keydown",function(e){var t;e.shiftKey&&(e.metaKey||e.altKey)&&"KeyD"===e.code&&(a?o?((t=document.createElement("a")).href="_framework/debug?url="+encodeURIComponent(location.href),t.target="_blank",t.rel="noopener noreferrer",t.click()):console.error("Currently, only Chrome is supported for debugging."):console.error("Cannot start debugging, because the application was not compiled with debugging enabled."))})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(9),o=function(){function e(e){this.batchAddress=e,this.arrayRangeReader=a,this.arrayBuilderSegmentReader=i,this.diffReader=u,this.editReader=l,this.frameReader=s}return e.prototype.updatedComponents=function(){return r.platform.readStructField(this.batchAddress,0)},e.prototype.referenceFrames=function(){return r.platform.readStructField(this.batchAddress,a.structLength)},e.prototype.disposedComponentIds=function(){return r.platform.readStructField(this.batchAddress,2*a.structLength)},e.prototype.disposedEventHandlerIds=function(){return r.platform.readStructField(this.batchAddress,3*a.structLength)},e.prototype.updatedComponentsEntry=function(e,t){return c(e,t,u.structLength)},e.prototype.referenceFramesEntry=function(e,t){return c(e,t,s.structLength)},e.prototype.disposedComponentIdsEntry=function(e,t){var n=c(e,t,4);return r.platform.readInt32Field(n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=c(e,t,8);return r.platform.readUint64Field(n)},e}();t.SharedMemoryRenderBatch=o;var a={structLength:8,values:function(e){return r.platform.readObjectField(e,0)},count:function(e){return r.platform.readInt32Field(e,4)}},i={structLength:12,values:function(e){var t=r.platform.readObjectField(e,0),n=r.platform.getObjectFieldsBaseAddress(t);return r.platform.readObjectField(n,0)},offset:function(e){return r.platform.readInt32Field(e,4)},count:function(e){return r.platform.readInt32Field(e,8)}},u={structLength:4+i.structLength,componentId:function(e){return r.platform.readInt32Field(e,0)},edits:function(e){return r.platform.readStructField(e,4)},editsEntry:function(e,t){return c(e,t,l.structLength)}},l={structLength:20,editType:function(e){return r.platform.readInt32Field(e,0)},siblingIndex:function(e){return r.platform.readInt32Field(e,4)},newTreeIndex:function(e){return r.platform.readInt32Field(e,8)},moveToSiblingIndex:function(e){return r.platform.readInt32Field(e,8)},removedAttributeName:function(e){return r.platform.readStringField(e,16)}},s={structLength:36,frameType:function(e){return r.platform.readInt16Field(e,4)},subtreeLength:function(e){return r.platform.readInt32Field(e,8)},elementReferenceCaptureId:function(e){return r.platform.readStringField(e,16)},componentId:function(e){return r.platform.readInt32Field(e,12)},elementName:function(e){return r.platform.readStringField(e,16)},textContent:function(e){return r.platform.readStringField(e,16)},markupContent:function(e){return r.platform.readStringField(e,16)},attributeName:function(e){return r.platform.readStringField(e,16)},attributeValue:function(e){return r.platform.readStringField(e,24)},attributeEventHandlerId:function(e){return r.platform.readUint64Field(e,8)}};function c(e,t,n){return r.platform.getArrayEntryPtr(e,t,n)}}]); \ No newline at end of file diff --git a/src/Components/Web.JS/package.json b/src/Components/Web.JS/package.json index ea59361494..327aaaafea 100644 --- a/src/Components/Web.JS/package.json +++ b/src/Components/Web.JS/package.json @@ -5,9 +5,12 @@ "description": "", "main": "index.js", "scripts": { + "preclean": "yarn install --mutex network", + "clean": "node node_modules/rimraf/bin.js ./dist/Debug ./dist/Release", + "prebuild": "yarn run clean && yarn install --mutex network", "build": "yarn run build:debug && yarn run build:production", - "build:debug": "cd src && webpack --mode development --config ./webpack.config.js", - "build:production": "cd src && webpack --mode production --config ./webpack.config.js", + "build:debug": "cd src && node ../node_modules/webpack-cli/bin/cli.js --mode development --config ./webpack.config.js", + "build:production": "cd src && node ../node_modules/webpack-cli/bin/cli.js --mode production --config ./webpack.config.js", "test": "jest" }, "devDependencies": { @@ -21,6 +24,7 @@ "@typescript-eslint/parser": "^1.5.0", "eslint": "^5.16.0", "jest": "^24.8.0", + "rimraf": "^2.6.2", "ts-jest": "^24.0.0", "ts-loader": "^4.4.1", "typescript": "^3.5.3", diff --git a/src/Components/Web.JS/src/Boot.Server.ts b/src/Components/Web.JS/src/Boot.Server.ts index df96f1031f..b4135fc503 100644 --- a/src/Components/Web.JS/src/Boot.Server.ts +++ b/src/Components/Web.JS/src/Boot.Server.ts @@ -47,6 +47,16 @@ async function boot(userOptions?: Partial): Promise { return true; }; + window.addEventListener( + 'unload', + () => { + const data = new FormData(); + data.set('circuitId', circuit.circuitId); + navigator.sendBeacon('_blazor/disconnect', data); + }, + false + ); + window['Blazor'].reconnect = reconnect; logger.log(LogLevel.Information, 'Blazor server-side application started.'); @@ -75,12 +85,11 @@ async function initializeConnection(options: BlazorOptions, logger: Logger): Pro connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet); connection.on('JS.EndInvokeDotNet', (args: string) => DotNet.jsCallDispatcher.endInvokeDotNetFromJS(...(JSON.parse(args) as [string, boolean, unknown]))); - connection.on('JS.RenderBatch', (browserRendererId: number, batchId: number, batchData: Uint8Array) => { - logger.log(LogLevel.Debug, `Received render batch for ${browserRendererId} with id ${batchId} and ${batchData.byteLength} bytes.`); - const queue = RenderQueue.getOrCreateQueue(browserRendererId, logger); - - queue.processBatch(batchId, batchData, connection); + const renderQueue = new RenderQueue(/* renderer ID unused with remote renderer */ 0, logger); + connection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => { + logger.log(LogLevel.Debug, `Received render batch with id ${batchId} and ${batchData.byteLength} bytes.`); + renderQueue.processBatch(batchId, batchData, connection); }); connection.onclose(error => !renderingFailed && options.reconnectionHandler!.onConnectionDown(options.reconnectionOptions, error)); diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.ts b/src/Components/Web.JS/src/Boot.WebAssembly.ts index ee990a10c2..849721b20b 100644 --- a/src/Components/Web.JS/src/Boot.WebAssembly.ts +++ b/src/Components/Web.JS/src/Boot.WebAssembly.ts @@ -18,7 +18,7 @@ async function boot(options?: any): Promise { } started = true; - setEventDispatcher((eventDescriptor, eventArgs) => DotNet.invokeMethodAsync('Microsoft.AspNetCore.Components.Web', 'DispatchEvent', eventDescriptor, JSON.stringify(eventArgs))); + setEventDispatcher((eventDescriptor, eventArgs) => DotNet.invokeMethodAsync('Microsoft.AspNetCore.Blazor', 'DispatchEvent', eventDescriptor, JSON.stringify(eventArgs))); // Configure environment for execution under Mono WebAssembly with shared-memory rendering const platform = Environment.setPlatform(monoPlatform); diff --git a/src/Components/Web.JS/src/Platform/Circuits/RenderQueue.ts b/src/Components/Web.JS/src/Platform/Circuits/RenderQueue.ts index 78860d111c..24c22a09ee 100644 --- a/src/Components/Web.JS/src/Platform/Circuits/RenderQueue.ts +++ b/src/Components/Web.JS/src/Platform/Circuits/RenderQueue.ts @@ -4,8 +4,6 @@ import { Logger, LogLevel } from '../Logging/Logger'; import { HubConnection } from '@aspnet/signalr'; export class RenderQueue { - private static renderQueues = new Map(); - private nextBatchId = 2; private fatalError?: string; @@ -19,17 +17,6 @@ export class RenderQueue { this.logger = logger; } - public static getOrCreateQueue(browserRendererId: number, logger: Logger): RenderQueue { - const queue = this.renderQueues.get(browserRendererId); - if (queue) { - return queue; - } - - const newQueue = new RenderQueue(browserRendererId, logger); - this.renderQueues.set(browserRendererId, newQueue); - return newQueue; - } - public async processBatch(receivedBatchId: number, batchData: Uint8Array, connection: HubConnection): Promise { if (receivedBatchId < this.nextBatchId) { // SignalR delivers messages in order, but it does not guarantee that the message gets delivered. diff --git a/src/Components/Web.JS/src/Rendering/Renderer.ts b/src/Components/Web.JS/src/Rendering/Renderer.ts index 88a21d16a6..06b8ba3985 100644 --- a/src/Components/Web.JS/src/Rendering/Renderer.ts +++ b/src/Components/Web.JS/src/Rendering/Renderer.ts @@ -12,7 +12,6 @@ const browserRenderers: BrowserRendererRegistry = {}; let shouldResetScrollAfterNextBatch = false; export function attachRootComponentToLogicalElement(browserRendererId: number, logicalElement: LogicalElement, componentId: number): void { - let browserRenderer = browserRenderers[browserRendererId]; if (!browserRenderer) { browserRenderer = browserRenderers[browserRendererId] = new BrowserRenderer(browserRendererId); @@ -21,15 +20,15 @@ export function attachRootComponentToLogicalElement(browserRendererId: number, l browserRenderer.attachRootComponentToLogicalElement(componentId, logicalElement); } -export function attachRootComponentToElement(browserRendererId: number, elementSelector: string, componentId: number): void { - +export function attachRootComponentToElement(elementSelector: string, componentId: number, browserRendererId?: number): void { const element = document.querySelector(elementSelector); if (!element) { throw new Error(`Could not find any element matching selector '${elementSelector}'.`); } // 'allowExistingContents' to keep any prerendered content until we do the first client-side render - attachRootComponentToLogicalElement(browserRendererId, toLogicalElement(element, /* allow existing contents */ true), componentId); + // Only client-side Blazor supplies a browser renderer ID + attachRootComponentToLogicalElement(browserRendererId || 0, toLogicalElement(element, /* allow existing contents */ true), componentId); } export function renderBatch(browserRendererId: number, batch: RenderBatch): void { diff --git a/src/Components/Web.JS/tests/DefaultReconnectionHandler.test.ts b/src/Components/Web.JS/tests/DefaultReconnectionHandler.test.ts index 62f207415e..d59e0fecfe 100644 --- a/src/Components/Web.JS/tests/DefaultReconnectionHandler.test.ts +++ b/src/Components/Web.JS/tests/DefaultReconnectionHandler.test.ts @@ -58,23 +58,24 @@ describe('DefaultReconnectionHandler', () => { expect(reconnect).toHaveBeenCalledTimes(1); }); - it('invokes failed if reconnect fails', async () => { - const testDisplay = createTestDisplay(); - const reconnect = jest.fn().mockRejectedValue(null); - const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect); - window.console.error = jest.fn(); + // Skipped while under investigation: https://github.com/aspnet/AspNetCore/issues/12578 + // it('invokes failed if reconnect fails', async () => { + // const testDisplay = createTestDisplay(); + // const reconnect = jest.fn().mockRejectedValue(null); + // const handler = new DefaultReconnectionHandler(NullLogger.instance, testDisplay, reconnect); + // window.console.error = jest.fn(); - handler.onConnectionDown({ - maxRetries: 3, - retryIntervalMilliseconds: 20, - dialogId: 'ignored' - }); + // handler.onConnectionDown({ + // maxRetries: 3, + // retryIntervalMilliseconds: 20, + // dialogId: 'ignored' + // }); - await delay(100); - expect(testDisplay.show).toHaveBeenCalled(); - expect(testDisplay.failed).toHaveBeenCalled(); - expect(reconnect).toHaveBeenCalledTimes(3); - }); + // await delay(500); + // expect(testDisplay.show).toHaveBeenCalled(); + // expect(testDisplay.failed).toHaveBeenCalled(); + // expect(reconnect).toHaveBeenCalledTimes(3); + // }); }); function attachUserSpecifiedUI(options: ReconnectionOptions): Element { diff --git a/src/Components/Web.JS/tests/RenderQueue.test.ts b/src/Components/Web.JS/tests/RenderQueue.test.ts index df22da3a8a..81e283fc0d 100644 --- a/src/Components/Web.JS/tests/RenderQueue.test.ts +++ b/src/Components/Web.JS/tests/RenderQueue.test.ts @@ -10,23 +10,8 @@ jest.mock('../src/Rendering/Renderer', () => ({ describe('RenderQueue', () => { - it('getOrCreateRenderQueue returns a new queue if one does not exist for a renderer', () => { - const queue = RenderQueue.getOrCreateQueue(1, NullLogger.instance); - - expect(queue).toBeDefined(); - - }); - - it('getOrCreateRenderQueue returns an existing queue if one exists for a renderer', () => { - const queue = RenderQueue.getOrCreateQueue(2, NullLogger.instance); - const secondQueue = RenderQueue.getOrCreateQueue(2, NullLogger.instance); - - expect(secondQueue).toBe(queue); - - }); - it('processBatch acknowledges previously rendered batches', () => { - const queue = RenderQueue.getOrCreateQueue(3, NullLogger.instance); + const queue = new RenderQueue(0, NullLogger.instance); const sendMock = jest.fn(); const connection = { send: sendMock } as any as signalR.HubConnection; @@ -37,7 +22,7 @@ describe('RenderQueue', () => { }); it('processBatch does not render out of order batches', () => { - const queue = RenderQueue.getOrCreateQueue(4, NullLogger.instance); + const queue = new RenderQueue(0, NullLogger.instance); const sendMock = jest.fn(); const connection = { send: sendMock } as any as signalR.HubConnection; @@ -47,7 +32,7 @@ describe('RenderQueue', () => { }); it('processBatch renders pending batches', () => { - const queue = RenderQueue.getOrCreateQueue(5, NullLogger.instance); + const queue = new RenderQueue(0, NullLogger.instance); const sendMock = jest.fn(); const connection = { send: sendMock } as any as signalR.HubConnection; diff --git a/src/Components/Web.JS/yarn.lock b/src/Components/Web.JS/yarn.lock index f49df8df7d..48b8058558 100644 --- a/src/Components/Web.JS/yarn.lock +++ b/src/Components/Web.JS/yarn.lock @@ -2141,7 +2141,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: +glob@^7.1.1, glob@^7.1.2: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== @@ -2153,6 +2153,18 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.3: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-modules@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -2402,16 +2414,21 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inherits@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" diff --git a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs index 034b500218..f6a30257c2 100644 --- a/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs +++ b/src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netstandard2.0.cs @@ -305,81 +305,81 @@ namespace Microsoft.AspNetCore.Components.Forms public Microsoft.AspNetCore.Components.EventCallback OnSubmit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.EventCallback OnValidSubmit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } protected override void OnParametersSet() { } } - public abstract partial class InputBase : Microsoft.AspNetCore.Components.ComponentBase + public abstract partial class InputBase : Microsoft.AspNetCore.Components.ComponentBase { protected InputBase() { } [Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)] public System.Collections.Generic.IReadOnlyDictionary AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } protected string CssClass { get { throw null; } } - protected T CurrentValue { get { throw null; } set { } } + protected TValue CurrentValue { get { throw null; } set { } } protected string CurrentValueAsString { get { throw null; } set { } } protected Microsoft.AspNetCore.Components.Forms.EditContext EditContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } protected Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] - public T Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.EventCallback ValueChanged { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public Microsoft.AspNetCore.Components.EventCallback ValueChanged { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] - public System.Linq.Expressions.Expression> ValueExpression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - protected virtual string FormatValueAsString(T value) { throw null; } + public System.Linq.Expressions.Expression> ValueExpression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + protected virtual string FormatValueAsString(TValue value) { throw null; } public override System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } - protected abstract bool TryParseValueFromString(string value, out T result, out string validationErrorMessage); + protected abstract bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage); } public partial class InputCheckbox : Microsoft.AspNetCore.Components.Forms.InputBase { public InputCheckbox() { } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } protected override bool TryParseValueFromString(string value, out bool result, out string validationErrorMessage) { throw null; } } - public partial class InputDate : Microsoft.AspNetCore.Components.Forms.InputBase + public partial class InputDate : Microsoft.AspNetCore.Components.Forms.InputBase { public InputDate() { } [Microsoft.AspNetCore.Components.ParameterAttribute] public string ParsingErrorMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } - protected override string FormatValueAsString(T value) { throw null; } - protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage) { throw null; } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } + protected override string FormatValueAsString(TValue value) { throw null; } + protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage) { throw null; } } - public partial class InputNumber : Microsoft.AspNetCore.Components.Forms.InputBase + public partial class InputNumber : Microsoft.AspNetCore.Components.Forms.InputBase { public InputNumber() { } [Microsoft.AspNetCore.Components.ParameterAttribute] public string ParsingErrorMessage { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } - protected override string FormatValueAsString(T value) { throw null; } - protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage) { throw null; } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } + protected override string FormatValueAsString(TValue value) { throw null; } + protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage) { throw null; } } - public partial class InputSelect : Microsoft.AspNetCore.Components.Forms.InputBase + public partial class InputSelect : Microsoft.AspNetCore.Components.Forms.InputBase { public InputSelect() { } [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } - protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage) { throw null; } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } + protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage) { throw null; } } public partial class InputText : Microsoft.AspNetCore.Components.Forms.InputBase { public InputText() { } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage) { throw null; } } public partial class InputTextArea : Microsoft.AspNetCore.Components.Forms.InputBase { public InputTextArea() { } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage) { throw null; } } - public partial class ValidationMessage : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable + public partial class ValidationMessage : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable { public ValidationMessage() { } [Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)] public System.Collections.Generic.IReadOnlyDictionary AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] - public System.Linq.Expressions.Expression> For { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + public System.Linq.Expressions.Expression> For { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } protected virtual void Dispose(bool disposing) { } protected override void OnParametersSet() { } void System.IDisposable.Dispose() { } @@ -389,7 +389,7 @@ namespace Microsoft.AspNetCore.Components.Forms public ValidationSummary() { } [Microsoft.AspNetCore.Components.ParameterAttribute(CaptureUnmatchedValues=true)] public System.Collections.Generic.IReadOnlyDictionary AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } protected virtual void Dispose(bool disposing) { } protected override void OnParametersSet() { } void System.IDisposable.Dispose() { } @@ -409,7 +409,7 @@ namespace Microsoft.AspNetCore.Components.Routing protected string CssClass { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.Routing.NavLinkMatch Match { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { } public void Dispose() { } protected override void OnInitialized() { } protected override void OnParametersSet() { } @@ -422,17 +422,12 @@ namespace Microsoft.AspNetCore.Components.Routing } namespace Microsoft.AspNetCore.Components.Web { - public static partial class RendererRegistryEventDispatcher + public sealed partial class WebEventDescriptor { - [Microsoft.JSInterop.JSInvokableAttribute("DispatchEvent")] - public static System.Threading.Tasks.Task DispatchEvent(Microsoft.AspNetCore.Components.Web.RendererRegistryEventDispatcher.BrowserEventDescriptor eventDescriptor, string eventArgsJson) { throw null; } - public partial class BrowserEventDescriptor - { - public BrowserEventDescriptor() { } - public int BrowserRendererId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public string EventArgsType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public Microsoft.AspNetCore.Components.Rendering.EventFieldInfo EventFieldInfo { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public ulong EventHandlerId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - } + public WebEventDescriptor() { } + public int BrowserRendererId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public string EventArgsType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public Microsoft.AspNetCore.Components.Rendering.EventFieldInfo EventFieldInfo { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public ulong EventHandlerId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } } diff --git a/src/Components/Web/src/Forms/EditForm.cs b/src/Components/Web/src/Forms/EditForm.cs index 4a0e490dcf..d47a6377a6 100644 --- a/src/Components/Web/src/Forms/EditForm.cs +++ b/src/Components/Web/src/Forms/EditForm.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms { diff --git a/src/Components/Web/src/Forms/InputBase.cs b/src/Components/Web/src/Forms/InputBase.cs index 4202aec396..f60c330326 100644 --- a/src/Components/Web/src/Forms/InputBase.cs +++ b/src/Components/Web/src/Forms/InputBase.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Components.Forms /// integrates with an , which must be supplied /// as a cascading parameter. /// - public abstract class InputBase : ComponentBase + public abstract class InputBase : ComponentBase { private bool _previousParsingAttemptFailed; private ValidationMessageStore _parsingValidationMessages; @@ -32,20 +32,20 @@ namespace Microsoft.AspNetCore.Components.Forms /// /// @bind-Value="model.PropertyName" /// - [Parameter] public T Value { get; set; } + [Parameter] public TValue Value { get; set; } /// /// Gets or sets a callback that updates the bound value. /// - [Parameter] public EventCallback ValueChanged { get; set; } + [Parameter] public EventCallback ValueChanged { get; set; } /// /// Gets or sets an expression that identifies the bound value. /// - [Parameter] public Expression> ValueExpression { get; set; } + [Parameter] public Expression> ValueExpression { get; set; } /// - /// Gets the associated . + /// Gets the associated . /// protected EditContext EditContext { get; set; } @@ -57,12 +57,12 @@ namespace Microsoft.AspNetCore.Components.Forms /// /// Gets or sets the current value of the input. /// - protected T CurrentValue + protected TValue CurrentValue { get => Value; set { - var hasChanged = !EqualityComparer.Default.Equals(value, Value); + var hasChanged = !EqualityComparer.Default.Equals(value, Value); if (hasChanged) { Value = value; @@ -126,18 +126,18 @@ namespace Microsoft.AspNetCore.Components.Forms /// /// The value to format. /// A string representation of the value. - protected virtual string FormatValueAsString(T value) + protected virtual string FormatValueAsString(TValue value) => value?.ToString(); /// - /// Parses a string to create an instance of . Derived classes can override this to change how + /// Parses a string to create an instance of . Derived classes can override this to change how /// interprets incoming values. /// /// The string value to be parsed. - /// An instance of . + /// An instance of . /// If the value could not be parsed, provides a validation error message. /// True if the value could be parsed; otherwise false. - protected abstract bool TryParseValueFromString(string value, out T result, out string validationErrorMessage); + protected abstract bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage); /// /// Gets a string that indicates the status of the field being edited. This will include @@ -192,7 +192,7 @@ namespace Microsoft.AspNetCore.Components.Forms EditContext = CascadedEditContext; FieldIdentifier = FieldIdentifier.Create(ValueExpression); - _nullableUnderlyingType = Nullable.GetUnderlyingType(typeof(T)); + _nullableUnderlyingType = Nullable.GetUnderlyingType(typeof(TValue)); } else if (CascadedEditContext != EditContext) { diff --git a/src/Components/Web/src/Forms/InputCheckbox.cs b/src/Components/Web/src/Forms/InputCheckbox.cs index 981287ee8d..bb564dc13c 100644 --- a/src/Components/Web/src/Forms/InputCheckbox.cs +++ b/src/Components/Web/src/Forms/InputCheckbox.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms { diff --git a/src/Components/Web/src/Forms/InputDate.cs b/src/Components/Web/src/Forms/InputDate.cs index 416fd38587..7192523471 100644 --- a/src/Components/Web/src/Forms/InputDate.cs +++ b/src/Components/Web/src/Forms/InputDate.cs @@ -3,7 +3,7 @@ using System; using System.Globalization; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms { @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Components.Forms /// An input component for editing date values. /// Supported types are and . /// - public class InputDate : InputBase + public class InputDate : InputBase { private const string DateFormat = "yyyy-MM-dd"; // Compatible with HTML date inputs @@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Components.Forms } /// - protected override string FormatValueAsString(T value) + protected override string FormatValueAsString(TValue value) { switch (value) { @@ -47,11 +47,11 @@ namespace Microsoft.AspNetCore.Components.Forms } /// - protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage) + protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage) { // Unwrap nullable types. We don't have to deal with receiving empty values for nullable // types here, because the underlying InputBase already covers that. - var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + var targetType = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue); bool success; if (targetType == typeof(DateTime)) @@ -79,12 +79,12 @@ namespace Microsoft.AspNetCore.Components.Forms } } - static bool TryParseDateTime(string value, out T result) + static bool TryParseDateTime(string value, out TValue result) { var success = BindConverter.TryConvertToDateTime(value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue); if (success) { - result = (T)(object)parsedValue; + result = (TValue)(object)parsedValue; return true; } else @@ -94,12 +94,12 @@ namespace Microsoft.AspNetCore.Components.Forms } } - static bool TryParseDateTimeOffset(string value, out T result) + static bool TryParseDateTimeOffset(string value, out TValue result) { var success = BindConverter.TryConvertToDateTimeOffset(value, CultureInfo.InvariantCulture, DateFormat, out var parsedValue); if (success) { - result = (T)(object)parsedValue; + result = (TValue)(object)parsedValue; return true; } else diff --git a/src/Components/Web/src/Forms/InputNumber.cs b/src/Components/Web/src/Forms/InputNumber.cs index a685644c8d..4f0377ceed 100644 --- a/src/Components/Web/src/Forms/InputNumber.cs +++ b/src/Components/Web/src/Forms/InputNumber.cs @@ -3,7 +3,7 @@ using System; using System.Globalization; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms { @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Components.Forms /// An input component for editing numeric values. /// Supported numeric types are , , , , . /// - public class InputNumber : InputBase + public class InputNumber : InputBase { private static string _stepAttributeValue; // Null by default, so only allows whole numbers as per HTML spec @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Forms { // Unwrap Nullable, because InputBase already deals with the Nullable aspect // of it for us. We will only get asked to parse the T for nonempty inputs. - var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + var targetType = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue); if (targetType == typeof(int) || targetType == typeof(float) || targetType == typeof(double) || @@ -52,9 +52,9 @@ namespace Microsoft.AspNetCore.Components.Forms } /// - protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage) + protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage) { - if (BindConverter.TryConvertTo(value, CultureInfo.InvariantCulture, out result)) + if (BindConverter.TryConvertTo(value, CultureInfo.InvariantCulture, out result)) { validationErrorMessage = null; return true; @@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Components.Forms /// /// The value to format. /// A string representation of the value. - protected override string FormatValueAsString(T value) + protected override string FormatValueAsString(TValue value) { // Avoiding a cast to IFormattable to avoid boxing. switch (value) diff --git a/src/Components/Web/src/Forms/InputSelect.cs b/src/Components/Web/src/Forms/InputSelect.cs index 5b9cb0ca66..b8cdbeab1f 100644 --- a/src/Components/Web/src/Forms/InputSelect.cs +++ b/src/Components/Web/src/Forms/InputSelect.cs @@ -3,14 +3,14 @@ using System; using System.Globalization; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms { /// /// A dropdown selection component. /// - public class InputSelect : InputBase + public class InputSelect : InputBase { /// /// Gets or sets the child content to be rendering inside the select element. @@ -30,17 +30,17 @@ namespace Microsoft.AspNetCore.Components.Forms } /// - protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage) + protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage) { - if (typeof(T) == typeof(string)) + if (typeof(TValue) == typeof(string)) { - result = (T)(object)value; + result = (TValue)(object)value; validationErrorMessage = null; return true; } - else if (typeof(T).IsEnum) + else if (typeof(TValue).IsEnum) { - var success = BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out var parsedValue); + var success = BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out var parsedValue); if (success) { result = parsedValue; @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Components.Forms } } - throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(T)}'."); + throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(TValue)}'."); } } } diff --git a/src/Components/Web/src/Forms/InputText.cs b/src/Components/Web/src/Forms/InputText.cs index 94c09c4694..c7a0a319d7 100644 --- a/src/Components/Web/src/Forms/InputText.cs +++ b/src/Components/Web/src/Forms/InputText.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms { diff --git a/src/Components/Web/src/Forms/InputTextArea.cs b/src/Components/Web/src/Forms/InputTextArea.cs index ba61d95896..9f82b71759 100644 --- a/src/Components/Web/src/Forms/InputTextArea.cs +++ b/src/Components/Web/src/Forms/InputTextArea.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms { diff --git a/src/Components/Web/src/Forms/ValidationMessage.cs b/src/Components/Web/src/Forms/ValidationMessage.cs index f4676d7921..d033fdba20 100644 --- a/src/Components/Web/src/Forms/ValidationMessage.cs +++ b/src/Components/Web/src/Forms/ValidationMessage.cs @@ -4,17 +4,17 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms { /// /// Displays a list of validation messages for a specified field within a cascaded . /// - public class ValidationMessage : ComponentBase, IDisposable + public class ValidationMessage : ComponentBase, IDisposable { private EditContext _previousEditContext; - private Expression> _previousFieldAccessor; + private Expression> _previousFieldAccessor; private readonly EventHandler _validationStateChangedHandler; private FieldIdentifier _fieldIdentifier; @@ -28,10 +28,10 @@ namespace Microsoft.AspNetCore.Components.Forms /// /// Specifies the field for which validation messages should be displayed. /// - [Parameter] public Expression> For { get; set; } + [Parameter] public Expression> For { get; set; } /// ` - /// Constructs an instance of . + /// Constructs an instance of . /// public ValidationMessage() { diff --git a/src/Components/Web/src/Forms/ValidationSummary.cs b/src/Components/Web/src/Forms/ValidationSummary.cs index b0ed6596cd..8e151b1e63 100644 --- a/src/Components/Web/src/Forms/ValidationSummary.cs +++ b/src/Components/Web/src/Forms/ValidationSummary.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Forms { diff --git a/src/Components/Web/src/Properties/AssemblyInfo.cs b/src/Components/Web/src/Properties/AssemblyInfo.cs index c773cb61e9..2741560028 100644 --- a/src/Components/Web/src/Properties/AssemblyInfo.cs +++ b/src/Components/Web/src/Properties/AssemblyInfo.cs @@ -1,6 +1,3 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] -[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] - [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Components/Web/src/RendererRegistry.cs b/src/Components/Web/src/RendererRegistry.cs deleted file mode 100644 index 74926d8d0e..0000000000 --- a/src/Components/Web/src/RendererRegistry.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Components.Rendering; -using System.Collections.Generic; -using System.Threading; - -namespace Microsoft.AspNetCore.Components.Web -{ - // Provides mechanisms for locating instances - // by ID. This is used when receiving incoming events. It also implicitly - // roots instances and their associated component instances - // so they cannot be GCed while they are still registered for events. - - /// - /// Framework infrastructure, not intended to be used by application code. - /// - internal class RendererRegistry - { - private static AsyncLocal _current; - private static readonly RendererRegistry _globalRegistry; - - // By default the registry will be set to a default value. This means that - // things will 'just work when running in the browser. - // - // Running in Server-Side Components - any call into the Circuit will set this value via - // the async local. This will ensure that the incoming call can resolve the correct - // renderer associated with the user context. - static RendererRegistry() - { - _current = new AsyncLocal(); - _globalRegistry = new RendererRegistry(); - } - - /// - /// Framework infrastructure, not intended to be used by application code. - /// - public static RendererRegistry Current => _current.Value ?? _globalRegistry; - - /// - /// Framework infrastructure, not intended by used by application code. - /// - public static void SetCurrentRendererRegistry(RendererRegistry registry) - { - _current.Value = registry; - } - - private int _nextId; - private IDictionary _renderers = new Dictionary(); - - - /// - /// Framework infrastructure, not intended by used by application code. - /// - public int Add(Renderer renderer) - { - lock (_renderers) - { - var id = _nextId++; - _renderers.Add(id, renderer); - return id; - } - } - - /// - /// Framework infrastructure, not intended by used by application code. - /// - public Renderer Find(int rendererId) - { - lock (_renderers) - { - return _renderers[rendererId]; - } - } - - /// - /// Framework infrastructure, not intended by used by application code. - /// - public bool TryRemove(int rendererId) - { - lock (_renderers) - { - if (_renderers.ContainsKey(rendererId)) - { - _renderers.Remove(rendererId); - return true; - } - else - { - return false; - } - } - } - } -} diff --git a/src/Components/Web/src/Routing/NavLink.cs b/src/Components/Web/src/Routing/NavLink.cs index 949904e4d4..516f81d8aa 100644 --- a/src/Components/Web/src/Routing/NavLink.cs +++ b/src/Components/Web/src/Routing/NavLink.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; namespace Microsoft.AspNetCore.Components.Routing { diff --git a/src/Components/Web/src/WebEventDescriptor.cs b/src/Components/Web/src/WebEventDescriptor.cs new file mode 100644 index 0000000000..7b32c9b8a9 --- /dev/null +++ b/src/Components/Web/src/WebEventDescriptor.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Components.Rendering; + +namespace Microsoft.AspNetCore.Components.Web +{ + /// + /// For framework use only. + /// + public sealed class WebEventDescriptor + { + // We split the incoming event data in two, because we don't know what type + // to use when deserializing the args until we've deserialized the descriptor. + // This class represents the first half of the parsing process. + // It's public only because it's part of the signature of a [JSInvokable] method. + + /// + /// For framework use only. + /// + public int BrowserRendererId { get; set; } + + /// + /// For framework use only. + /// + public ulong EventHandlerId { get; set; } + + /// + /// For framework use only. + /// + public string EventArgsType { get; set; } + + /// + /// For framework use only. + /// + public EventFieldInfo EventFieldInfo { get; set; } + } +} diff --git a/src/Components/Web/test/Forms/InputBaseTest.cs b/src/Components/Web/test/Forms/InputBaseTest.cs index 8be8525365..570b0f9283 100644 --- a/src/Components/Web/test/Forms/InputBaseTest.cs +++ b/src/Components/Web/test/Forms/InputBaseTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; using Xunit; diff --git a/src/Components/test/E2ETest/ServerExecutionTests/CircuitGracefulTerminationTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/CircuitGracefulTerminationTests.cs index f383818386..188fae57d0 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/CircuitGracefulTerminationTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/CircuitGracefulTerminationTests.cs @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests public async Task ReloadingThePage_GracefullyDisconnects_TheCurrentCircuit() { // Arrange & Act - _ = ((IJavaScriptExecutor)Browser).ExecuteScript("location.reload()"); + Browser.Navigate().Refresh(); await Task.WhenAny(Task.Delay(10000), GracefulDisconnectCompletionSource.Task); // Assert @@ -70,7 +70,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Browser.Close(); // Set to null so that other tests in this class can create a new browser if necessary so // that tests don't fail when running together. - Browser = null; await Task.WhenAny(Task.Delay(10000), GracefulDisconnectCompletionSource.Task); diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubReliabilityTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubReliabilityTest.cs index deb353dd9c..3984f32d43 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubReliabilityTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ComponentHubReliabilityTest.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests private void CreateDefaultConfiguration() { Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout }; - Client.RenderBatchReceived += (id, rendererId, data) => Batches.Add(new Batch(id, rendererId, data)); + Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data)); Client.OnCircuitError += (error) => Errors.Add(error); _ = _serverFixture.RootUri; // this is needed for the side-effects of getting the URI. @@ -267,15 +267,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests private class Batch { - public Batch(int id, int rendererId, byte [] data) + public Batch(int id, byte [] data) { Id = id; - RendererId = rendererId; Data = data; } public int Id { get; } - public int RendererId { get; } public byte[] Data { get; } } } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs index 8b46ee7e0a..ee4f4a00ed 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs @@ -10,6 +10,8 @@ using Ignitor; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -17,9 +19,10 @@ using Xunit; namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { + [Flaky("https://github.com/aspnet/AspNetCore/issues/12940", FlakyOn.All)] public class InteropReliabilityTests : IClassFixture { - private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromMilliseconds(500); + private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(5); private readonly AspNetSiteServerFixture _serverFixture; public InteropReliabilityTests(AspNetSiteServerFixture serverFixture) @@ -77,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests await ValidateClientKeepsWorking(Client, batches); } - [Fact] + [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12940")] public async Task CannotInvokeJSInvokableMethodsWithWrongNumberOfArguments() { // Arrange @@ -256,7 +259,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests await ValidateClientKeepsWorking(Client, batches); } - [Fact] + [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12940")] public async Task LogsJSInteropCompletionsCallbacksAndContinuesWorkingInAllSituations() { // Arrange @@ -383,7 +386,30 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests null); Assert.Contains( - (LogLevel.Debug, "DispatchEventFailedToParseEventDescriptor"), + (LogLevel.Debug, "DispatchEventFailedToParseEventData"), + logEvents); + + await ValidateClientKeepsWorking(Client, batches); + } + + [Fact] + public async Task DispatchingEventsWithInvalidEventDescriptor() + { + // Arrange + var (interopCalls, dotNetCompletions, batches) = ConfigureClient(); + await GoToTestComponent(batches); + var sink = _serverFixture.Host.Services.GetRequiredService(); + var logEvents = new List<(LogLevel logLevel, string)>(); + sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name)); + + // Act + await Client.HubConnection.InvokeAsync( + "DispatchBrowserEvent", + "{Invalid:{\"payload}", + "{}"); + + Assert.Contains( + (LogLevel.Debug, "DispatchEventFailedToParseEventData"), logEvents); await ValidateClientKeepsWorking(Client, batches); @@ -400,7 +426,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name)); // Act - var browserDescriptor = new RendererRegistryEventDispatcher.BrowserEventDescriptor() + var browserDescriptor = new WebEventDescriptor() { BrowserRendererId = 0, EventHandlerId = 6, @@ -413,13 +439,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests "{Invalid:{\"payload}"); Assert.Contains( - (LogLevel.Debug, "DispatchEventFailedToDispatchEvent"), + (LogLevel.Debug, "DispatchEventFailedToParseEventData"), logEvents); await ValidateClientKeepsWorking(Client, batches); } - [Fact] + [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12940")] public async Task DispatchingEventsWithInvalidEventHandlerId() { // Arrange @@ -435,7 +461,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Type = "click", Detail = 1 }; - var browserDescriptor = new RendererRegistryEventDispatcher.BrowserEventDescriptor() + var browserDescriptor = new WebEventDescriptor() { BrowserRendererId = 0, EventHandlerId = 1, @@ -455,47 +481,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests await ValidateClientKeepsWorking(Client, batches); } - [Fact] - public async Task DispatchingEventThroughJSInterop() - { - // Arrange - var (interopCalls, dotNetCompletions, batches) = ConfigureClient(); - await GoToTestComponent(batches); - var sink = _serverFixture.Host.Services.GetRequiredService(); - var logEvents = new List<(LogLevel logLevel, string eventIdName)>(); - sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name)); - - // Act - var mouseEventArgs = new UIMouseEventArgs() - { - Type = "click", - Detail = 1 - }; - var browserDescriptor = new RendererRegistryEventDispatcher.BrowserEventDescriptor() - { - BrowserRendererId = 0, - EventHandlerId = 1, - EventArgsType = "mouse", - }; - - var serializerOptions = TestJsonSerializerOptionsProvider.Options; - var uiArgs = JsonSerializer.Serialize(mouseEventArgs, serializerOptions); - - await Assert.ThrowsAsync(() => Client.InvokeDotNetMethod( - 0, - "Microsoft.AspNetCore.Components.Web", - "DispatchEvent", - null, - JsonSerializer.Serialize(new object[] { browserDescriptor, uiArgs }, serializerOptions))); - - Assert.Contains( - (LogLevel.Debug, "DispatchEventThroughJSInterop"), - logEvents); - - await ValidateClientKeepsWorking(Client, batches); - } - - [Fact] public async Task EventHandlerThrowsSyncExceptionTerminatesTheCircuit() { @@ -507,18 +492,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception)); // Act - await Client.ClickAsync("event-handler-throw-sync"); + await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false); Assert.Contains( logEvents, e => LogLevel.Warning == e.logLevel && "UnhandledExceptionInCircuit" == e.eventIdName && "Handler threw an exception" == e.exception.Message); - - await ValidateClientKeepsWorking(Client, batches); } - private Task ValidateClientKeepsWorking(BlazorClient Client, List<(int, int, byte[])> batches) => + private Task ValidateClientKeepsWorking(BlazorClient Client, List<(int, byte[])> batches) => ValidateClientKeepsWorking(Client, () => batches.Count); private async Task ValidateClientKeepsWorking(BlazorClient Client, Func countAccessor) @@ -529,7 +512,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Assert.Equal(currentBatches + 1, countAccessor()); } - private async Task GoToTestComponent(List<(int, int, byte[])> batches) + private async Task GoToTestComponent(List<(int, byte[])> batches) { var rootUri = _serverFixture.RootUri; Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir"), prerendered: false), "Couldn't connect to the app"); @@ -539,12 +522,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Assert.Equal(2, batches.Count); } - private (List<(int id, string identifier, string args)>, List, List<(int, int, byte[])>) ConfigureClient() + private (List<(int id, string identifier, string args)>, List, List<(int, byte[])>) ConfigureClient() { var interopCalls = new List<(int, string, string)>(); Client.JSInterop += (int arg1, string arg2, string arg3) => interopCalls.Add((arg1, arg2, arg3)); - var batches = new List<(int, int, byte[])>(); - Client.RenderBatchReceived += (id, renderer, data) => batches.Add((id, renderer, data)); + var batches = new List<(int, byte[])>(); + Client.RenderBatchReceived += (renderer, data) => batches.Add((renderer, data)); var endInvokeDotNetCompletions = new List(); Client.DotNetInteropCompletion += (completion) => endInvokeDotNetCompletions.Add(completion); return (interopCalls, endInvokeDotNetCompletions, batches); diff --git a/src/Components/test/E2ETest/ServerExecutionTests/RemoteRendererBufferLimitTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/RemoteRendererBufferLimitTest.cs new file mode 100644 index 0000000000..3ff526ce2d --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/RemoteRendererBufferLimitTest.cs @@ -0,0 +1,122 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Ignitor; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests +{ + public class RemoteRendererBufferLimitTest : IClassFixture, IDisposable + { + private static readonly TimeSpan DefaultLatencyTimeout = Debugger.IsAttached ? TimeSpan.FromSeconds(60) : TimeSpan.FromMilliseconds(500); + + private AspNetSiteServerFixture _serverFixture; + + public RemoteRendererBufferLimitTest(AspNetSiteServerFixture serverFixture) + { + serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; + _serverFixture = serverFixture; + + // Needed here for side-effects + _ = _serverFixture.RootUri; + + Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout }; + Client.RenderBatchReceived += (id, data) => Batches.Add(new Batch(id, data)); + + Sink = _serverFixture.Host.Services.GetRequiredService(); + Sink.MessageLogged += LogMessages; + } + + public BlazorClient Client { get; set; } + + private IList Batches { get; set; } = new List(); + + // We use a stack so that we can search the logs in reverse order + private ConcurrentStack Logs { get; set; } = new ConcurrentStack(); + + public TestSink Sink { get; private set; } + + [Fact] + public async Task DispatchedEventsWillKeepBeingProcessed_ButUpdatedWillBeDelayedUntilARenderIsAcknowledged() + { + // Arrange + var baseUri = new Uri(_serverFixture.RootUri, "/subdir"); + Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app"); + Assert.Single(Batches); + + await Client.SelectAsync("test-selector-select", "BasicTestApp.LimitCounterComponent"); + Client.ConfirmRenderBatch = false; + + for (int i = 0; i < 10; i++) + { + await Client.ClickAsync("increment"); + } + await Client.ClickAsync("increment", expectRenderBatch: false); + + Assert.Single(Logs, l => (LogLevel.Debug, "The queue of unacknowledged render batches is full.") == (l.LogLevel, l.Message)); + Assert.Equal("10", ((TextNode)Client.FindElementById("the-count").Children.Single()).TextContent); + var fullCount = Batches.Count; + + // Act + await Client.ClickAsync("increment", expectRenderBatch: false); + + Assert.Contains(Logs, l => (LogLevel.Debug, "The queue of unacknowledged render batches is full.") == (l.LogLevel, l.Message)); + Assert.Equal(fullCount, Batches.Count); + Client.ConfirmRenderBatch = true; + + // This will resume the render batches. + await Client.ExpectRenderBatch(() => Client.ConfirmBatch(Batches[^1].Id)); + + // Assert + Assert.Equal("12", ((TextNode)Client.FindElementById("the-count").Children.Single()).TextContent); + Assert.Equal(fullCount + 1, Batches.Count); + } + + private void LogMessages(WriteContext context) => Logs.Push(new LogMessage(context.LogLevel, context.Message, context.Exception)); + + [DebuggerDisplay("{Message,nq}")] + private class LogMessage + { + public LogMessage(LogLevel logLevel, string message, Exception exception) + { + LogLevel = logLevel; + Message = message; + Exception = exception; + } + + public LogLevel LogLevel { get; set; } + public string Message { get; set; } + public Exception Exception { get; set; } + } + + private class Batch + { + public Batch(int id, byte[] data) + { + Id = id; + Data = data; + } + + public int Id { get; } + public byte[] Data { get; } + } + + public void Dispose() + { + if (Sink != null) + { + Sink.MessageLogged -= LogMessages; + } + } + } +} diff --git a/src/Components/test/E2ETest/Tests/AuthTest.cs b/src/Components/test/E2ETest/Tests/AuthTest.cs index deaa6bfb64..655505078a 100644 --- a/src/Components/test/E2ETest/Tests/AuthTest.cs +++ b/src/Components/test/E2ETest/Tests/AuthTest.cs @@ -44,6 +44,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests Browser.Equal("False", () => appElement.FindElement(By.Id("identity-authenticated")).Text); Browser.Equal(string.Empty, () => appElement.FindElement(By.Id("identity-name")).Text); Browser.Equal("(none)", () => appElement.FindElement(By.Id("test-claim")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -56,6 +57,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests Browser.Equal("True", () => appElement.FindElement(By.Id("identity-authenticated")).Text); Browser.Equal("someone cool", () => appElement.FindElement(By.Id("identity-name")).Text); Browser.Equal("Test claim value", () => appElement.FindElement(By.Id("test-claim")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -66,6 +68,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests WaitUntilExists(By.CssSelector("#no-authorization-rule .not-authorized")); Browser.Equal("You're not authorized, anonymous", () => appElement.FindElement(By.CssSelector("#no-authorization-rule .not-authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -75,6 +78,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("Welcome, Some User!", () => appElement.FindElement(By.CssSelector("#no-authorization-rule .authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -84,6 +88,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("Welcome, Some User!", () => appElement.FindElement(By.CssSelector("#authorize-role .authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -93,6 +98,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("You're not authorized, Some User", () => appElement.FindElement(By.CssSelector("#authorize-role .not-authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -102,6 +108,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("Welcome, Bert!", () => appElement.FindElement(By.CssSelector("#authorize-policy .authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -111,6 +118,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("You're not authorized, Mallory", () => appElement.FindElement(By.CssSelector("#authorize-policy .not-authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -120,6 +128,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous); Browser.Equal("Welcome to PageAllowingAnonymous!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -129,6 +138,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous); Browser.Equal("Welcome to PageAllowingAnonymous!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -138,6 +148,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization); Browser.Equal("Welcome to PageRequiringAuthorization!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -147,6 +158,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization); Browser.Equal("Sorry, anonymous, you're not authorized.", () => appElement.FindElement(By.CssSelector("#auth-failure")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -156,6 +168,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy); Browser.Equal("Welcome to PageRequiringPolicy!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -165,6 +178,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy); Browser.Equal("Sorry, Mallory, you're not authorized.", () => appElement.FindElement(By.CssSelector("#auth-failure")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -174,6 +188,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringRole); Browser.Equal("Welcome to PageRequiringRole!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -183,6 +198,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests var appElement = MountAndNavigateToAuthTest(PageRequiringRole); Browser.Equal("Sorry, Bert, you're not authorized.", () => appElement.FindElement(By.CssSelector("#auth-failure")).Text); + AssertExpectedLayoutUsed(); + } + + private void AssertExpectedLayoutUsed() + { + WaitUntilExists(By.Id("auth-links")); } protected IWebElement MountAndNavigateToAuthTest(string authLinkText) diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor index 5ec7bbe529..917111085a 100644 --- a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor @@ -8,19 +8,21 @@ and @page authorization rules. *@ - - - Authorizing... - -
- Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized. -
-
-
-
- -
- + + + + Authorizing... + +
+ Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized. +
+
+
+
+ +

There's nothing here

+
+
@code { protected override void OnInitialized() diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouterLayout.razor similarity index 93% rename from src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor rename to src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouterLayout.razor index 35ff333ed7..71c040058f 100644 --- a/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouterLayout.razor @@ -1,3 +1,8 @@ +@inherits LayoutComponentBase + +@Body + +
+ [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] internal class DefaultSpaPrerenderer : ISpaPrerenderer { private readonly string _applicationBasePath; diff --git a/src/Middleware/SpaServices/src/Prerendering/ISpaPrerenderer.cs b/src/Middleware/SpaServices/src/Prerendering/ISpaPrerenderer.cs index 183d4ae632..dcf986673e 100644 --- a/src/Middleware/SpaServices/src/Prerendering/ISpaPrerenderer.cs +++ b/src/Middleware/SpaServices/src/Prerendering/ISpaPrerenderer.cs @@ -1,4 +1,8 @@ -using System.Threading.Tasks; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.SpaServices.Prerendering { @@ -7,6 +11,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering /// JavaScript-based Single Page Applications. This is an alternative /// to using the 'asp-prerender-module' tag helper. /// + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public interface ISpaPrerenderer { /// diff --git a/src/Middleware/SpaServices/src/Prerendering/JavaScriptModuleExport.cs b/src/Middleware/SpaServices/src/Prerendering/JavaScriptModuleExport.cs index 97456b653d..13fd2177dd 100644 --- a/src/Middleware/SpaServices/src/Prerendering/JavaScriptModuleExport.cs +++ b/src/Middleware/SpaServices/src/Prerendering/JavaScriptModuleExport.cs @@ -1,3 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; namespace Microsoft.AspNetCore.SpaServices.Prerendering @@ -5,6 +8,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering /// /// Describes how to find the JavaScript code that performs prerendering. /// + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public class JavaScriptModuleExport { /// @@ -27,4 +31,4 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering /// public string ExportName { get; set; } } -} \ No newline at end of file +} diff --git a/src/Middleware/SpaServices/src/Prerendering/PrerenderTagHelper.cs b/src/Middleware/SpaServices/src/Prerendering/PrerenderTagHelper.cs index 665fbff8ef..3aaed1445a 100644 --- a/src/Middleware/SpaServices/src/Prerendering/PrerenderTagHelper.cs +++ b/src/Middleware/SpaServices/src/Prerendering/PrerenderTagHelper.cs @@ -1,3 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using System.Threading; using System.Threading.Tasks; @@ -14,6 +17,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering /// A tag helper for prerendering JavaScript applications on the server. /// [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public class PrerenderTagHelper : TagHelper { private const string PrerenderModuleAttributeName = "asp-prerender-module"; diff --git a/src/Middleware/SpaServices/src/Prerendering/Prerenderer.cs b/src/Middleware/SpaServices/src/Prerendering/Prerenderer.cs index 4e2fb16a42..26316320c6 100644 --- a/src/Middleware/SpaServices/src/Prerendering/Prerenderer.cs +++ b/src/Middleware/SpaServices/src/Prerendering/Prerenderer.cs @@ -1,3 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using System.Threading; using System.Threading.Tasks; @@ -10,12 +13,14 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering /// /// Performs server-side prerendering by invoking code in Node.js. /// + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public static class Prerenderer { private static readonly object CreateNodeScriptLock = new object(); private static StringAsTempFile NodeScript; + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] internal static Task RenderToString( string applicationBasePath, INodeServices nodeServices, @@ -63,6 +68,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering /// The maximum duration to wait for prerendering to complete. /// The PathBase for the currently-executing HTTP request. /// + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public static Task RenderToString( string applicationBasePath, INodeServices nodeServices, @@ -88,7 +94,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering private static string GetNodeScriptFilename(CancellationToken applicationStoppingToken) { - lock(CreateNodeScriptLock) + lock (CreateNodeScriptLock) { if (NodeScript == null) { @@ -100,4 +106,4 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering return NodeScript.FileName; } } -} \ No newline at end of file +} diff --git a/src/Middleware/SpaServices/src/Prerendering/PrerenderingServiceCollectionExtensions.cs b/src/Middleware/SpaServices/src/Prerendering/PrerenderingServiceCollectionExtensions.cs index d6a674396f..cabc57adcf 100644 --- a/src/Middleware/SpaServices/src/Prerendering/PrerenderingServiceCollectionExtensions.cs +++ b/src/Middleware/SpaServices/src/Prerendering/PrerenderingServiceCollectionExtensions.cs @@ -1,10 +1,15 @@ -using Microsoft.AspNetCore.SpaServices.Prerendering; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.SpaServices.Prerendering; namespace Microsoft.Extensions.DependencyInjection { /// /// Extension methods for setting up prerendering features in an . /// + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public static class PrerenderingServiceCollectionExtensions { /// @@ -12,6 +17,7 @@ namespace Microsoft.Extensions.DependencyInjection /// of . /// /// The . + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public static void AddSpaPrerenderer(this IServiceCollection serviceCollection) { serviceCollection.AddHttpContextAccessor(); diff --git a/src/Middleware/SpaServices/src/Prerendering/RenderToStringResult.cs b/src/Middleware/SpaServices/src/Prerendering/RenderToStringResult.cs index 1a2e156354..f6f5d77911 100644 --- a/src/Middleware/SpaServices/src/Prerendering/RenderToStringResult.cs +++ b/src/Middleware/SpaServices/src/Prerendering/RenderToStringResult.cs @@ -1,3 +1,7 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Text; @@ -7,6 +11,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering /// /// Describes the prerendering result returned by JavaScript code. /// + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public class RenderToStringResult { /// @@ -57,4 +62,4 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering return stringBuilder.ToString(); } } -} \ No newline at end of file +} diff --git a/src/Middleware/SpaServices/src/Routing/SpaRouteConstraint.cs b/src/Middleware/SpaServices/src/Routing/SpaRouteConstraint.cs index d6a1d5b227..6f25a25379 100644 --- a/src/Middleware/SpaServices/src/Routing/SpaRouteConstraint.cs +++ b/src/Middleware/SpaServices/src/Routing/SpaRouteConstraint.cs @@ -1,9 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.SpaServices { + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] internal class SpaRouteConstraint : IRouteConstraint { private readonly string _clientRouteTokenName; @@ -34,4 +38,4 @@ namespace Microsoft.AspNetCore.SpaServices return uri.IndexOf('.', lastSegmentStartPos + 1) >= 0; } } -} \ No newline at end of file +} diff --git a/src/Middleware/SpaServices/src/Routing/SpaRouteExtensions.cs b/src/Middleware/SpaServices/src/Routing/SpaRouteExtensions.cs index 7838aa13b9..547cf7c8ad 100644 --- a/src/Middleware/SpaServices/src/Routing/SpaRouteExtensions.cs +++ b/src/Middleware/SpaServices/src/Routing/SpaRouteExtensions.cs @@ -1,3 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using System.Collections.Generic; using Microsoft.AspNetCore.Routing; @@ -10,6 +13,7 @@ namespace Microsoft.AspNetCore.Builder /// /// Extension methods useful for configuring routing in a single-page application (SPA). /// + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public static class SpaRouteExtensions { private const string ClientRouteTokenName = "clientRoute"; @@ -91,4 +95,4 @@ namespace Microsoft.AspNetCore.Builder private static IDictionary ObjectToDictionary(object value) => value as IDictionary ?? new RouteValueDictionary(value); } -} \ No newline at end of file +} diff --git a/src/Middleware/SpaServices/src/Webpack/ConditionalProxyMiddleware.cs b/src/Middleware/SpaServices/src/Webpack/ConditionalProxyMiddleware.cs index 8cfbd07a18..59623ad879 100644 --- a/src/Middleware/SpaServices/src/Webpack/ConditionalProxyMiddleware.cs +++ b/src/Middleware/SpaServices/src/Webpack/ConditionalProxyMiddleware.cs @@ -1,9 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.SpaServices.Webpack @@ -14,6 +16,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack /// This is useful for Webpack middleware, because it lets you fall back on prebuilt files on disk for /// chunks not exposed by the current Webpack config (e.g., DLL/vendor chunks). /// + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] internal class ConditionalProxyMiddleware { private const int DefaultHttpBufferSize = 4096; @@ -89,7 +92,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack } // We can handle this - context.Response.StatusCode = (int) responseMessage.StatusCode; + context.Response.StatusCode = (int)responseMessage.StatusCode; foreach (var header in responseMessage.Headers) { context.Response.Headers[header.Key] = header.Value.ToArray(); @@ -120,4 +123,4 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack } } } -} \ No newline at end of file +} diff --git a/src/Middleware/SpaServices/src/Webpack/ConditionalProxyMiddlewareOptions.cs b/src/Middleware/SpaServices/src/Webpack/ConditionalProxyMiddlewareOptions.cs index 2c3311aabd..7f6f80fd77 100644 --- a/src/Middleware/SpaServices/src/Webpack/ConditionalProxyMiddlewareOptions.cs +++ b/src/Middleware/SpaServices/src/Webpack/ConditionalProxyMiddlewareOptions.cs @@ -1,7 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; namespace Microsoft.AspNetCore.SpaServices.Webpack { + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] internal class ConditionalProxyMiddlewareOptions { public ConditionalProxyMiddlewareOptions(string scheme, string host, string port, TimeSpan requestTimeout) @@ -17,4 +21,4 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack public string Port { get; } public TimeSpan RequestTimeout { get; } } -} \ No newline at end of file +} diff --git a/src/Middleware/SpaServices/src/Webpack/WebpackDevMiddleware.cs b/src/Middleware/SpaServices/src/Webpack/WebpackDevMiddleware.cs index 2e8f92ea3c..1a39d2de20 100644 --- a/src/Middleware/SpaServices/src/Webpack/WebpackDevMiddleware.cs +++ b/src/Middleware/SpaServices/src/Webpack/WebpackDevMiddleware.cs @@ -1,3 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System; using System.IO; using System.Threading; @@ -11,6 +14,7 @@ namespace Microsoft.AspNetCore.Builder /// /// Extension methods that can be used to enable Webpack dev middleware support. /// + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public static class WebpackDevMiddleware { private const string DefaultConfigFile = "webpack.config.js"; @@ -36,6 +40,7 @@ namespace Microsoft.AspNetCore.Builder /// /// The . /// Options for configuring the Webpack compiler instance. + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public static void UseWebpackDevMiddleware( this IApplicationBuilder appBuilder, WebpackDevMiddlewareOptions options = null) @@ -145,4 +150,4 @@ namespace Microsoft.AspNetCore.Builder } } #pragma warning restore CS0649 -} \ No newline at end of file +} diff --git a/src/Middleware/SpaServices/src/Webpack/WebpackDevMiddlewareOptions.cs b/src/Middleware/SpaServices/src/Webpack/WebpackDevMiddlewareOptions.cs index df50100dc7..28685e0d90 100644 --- a/src/Middleware/SpaServices/src/Webpack/WebpackDevMiddlewareOptions.cs +++ b/src/Middleware/SpaServices/src/Webpack/WebpackDevMiddlewareOptions.cs @@ -1,3 +1,7 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; using System.Collections.Generic; namespace Microsoft.AspNetCore.SpaServices.Webpack @@ -5,6 +9,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack /// /// Options for configuring a Webpack dev middleware compiler. /// + [Obsolete("Use Microsoft.AspNetCore.SpaServices.Extensions")] public class WebpackDevMiddlewareOptions { /// @@ -30,10 +35,10 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack /// public bool ReactHotModuleReplacement { get; set; } - /// + /// /// Specifies additional options to be passed to the Webpack Hot Middleware client, if used. - /// - public IDictionary HotModuleReplacementClientOptions { get; set; } + /// + public IDictionary HotModuleReplacementClientOptions { get; set; } /// /// Specifies the Webpack configuration file to be used. If not set, defaults to 'webpack.config.js'. @@ -58,4 +63,4 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack /// public object EnvParam { get; set; } } -} \ No newline at end of file +} diff --git a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs index f44b184654..edcfcd4271 100644 --- a/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs +++ b/src/Mvc/Mvc.Core/ref/Microsoft.AspNetCore.Mvc.Core.netcoreapp3.0.cs @@ -271,6 +271,7 @@ namespace Microsoft.AspNetCore.Mvc public Microsoft.AspNetCore.Mvc.ModelBinding.IModelBinderFactory ModelBinderFactory { get { throw null; } set { } } public Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary ModelState { get { throw null; } } public Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IObjectModelValidator ObjectValidator { get { throw null; } set { } } + public Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory ProblemDetailsFactory { get { throw null; } set { } } public Microsoft.AspNetCore.Http.HttpRequest Request { get { throw null; } } public Microsoft.AspNetCore.Http.HttpResponse Response { get { throw null; } } public Microsoft.AspNetCore.Routing.RouteData RouteData { get { throw null; } } @@ -445,6 +446,8 @@ namespace Microsoft.AspNetCore.Mvc [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.PhysicalFileResult PhysicalFile(string physicalPath, string contentType, string fileDownloadName, System.DateTimeOffset? lastModified, Microsoft.Net.Http.Headers.EntityTagHeaderValue entityTag, bool enableRangeProcessing) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] + public virtual Microsoft.AspNetCore.Mvc.ObjectResult Problem(string detail = null, string instance = null, int? statusCode = default(int?), string title = null, string type = null) { throw null; } + [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.RedirectResult Redirect(string url) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.RedirectResult RedirectPermanent(string url) { throw null; } @@ -586,6 +589,8 @@ namespace Microsoft.AspNetCore.Mvc public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary) { throw null; } [Microsoft.AspNetCore.Mvc.NonActionAttribute] public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem([Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ValidationProblemDetails descriptor) { throw null; } + [Microsoft.AspNetCore.Mvc.NonActionAttribute] + public virtual Microsoft.AspNetCore.Mvc.ActionResult ValidationProblem(string detail = null, string instance = null, int? statusCode = default(int?), string title = null, string type = null, [Microsoft.AspNetCore.Mvc.Infrastructure.ActionResultObjectValueAttribute]Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary = null) { throw null; } } public partial class ControllerContext : Microsoft.AspNetCore.Mvc.ActionContext { @@ -2205,7 +2210,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } public partial class SystemTextJsonOutputFormatter : Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter { - public SystemTextJsonOutputFormatter(Microsoft.AspNetCore.Mvc.JsonOptions options) { } + public SystemTextJsonOutputFormatter(System.Text.Json.JsonSerializerOptions jsonSerializerOptions) { } public System.Text.Json.JsonSerializerOptions SerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } [System.Diagnostics.DebuggerStepThroughAttribute] public sealed override System.Threading.Tasks.Task WriteResponseBodyAsync(Microsoft.AspNetCore.Mvc.Formatters.OutputFormatterWriteContext context, System.Text.Encoding selectedEncoding) { throw null; } @@ -2439,6 +2444,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure public long Length { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } } + public abstract partial class ProblemDetailsFactory + { + protected ProblemDetailsFactory() { } + public abstract Microsoft.AspNetCore.Mvc.ProblemDetails CreateProblemDetails(Microsoft.AspNetCore.Http.HttpContext httpContext, int? statusCode = default(int?), string title = null, string type = null, string detail = null, string instance = null); + public abstract Microsoft.AspNetCore.Mvc.ValidationProblemDetails CreateValidationProblemDetails(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary modelStateDictionary, int? statusCode = default(int?), string title = null, string type = null, string detail = null, string instance = null); + } public partial class RedirectResultExecutor : Microsoft.AspNetCore.Mvc.Infrastructure.IActionResultExecutor { public RedirectResultExecutor(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Mvc.Routing.IUrlHelperFactory urlHelperFactory) { } @@ -2549,6 +2560,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { public EmptyModelMetadataProvider() : base (default(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ICompositeMetadataDetailsProvider)) { } } + public sealed partial class FormFileValueProvider : Microsoft.AspNetCore.Mvc.ModelBinding.IValueProvider + { + public FormFileValueProvider(Microsoft.AspNetCore.Http.IFormFileCollection files) { } + public bool ContainsPrefix(string prefix) { throw null; } + public Microsoft.AspNetCore.Mvc.ModelBinding.ValueProviderResult GetValue(string key) { throw null; } + } + public sealed partial class FormFileValueProviderFactory : Microsoft.AspNetCore.Mvc.ModelBinding.IValueProviderFactory + { + public FormFileValueProviderFactory() { } + public System.Threading.Tasks.Task CreateValueProviderAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ValueProviderFactoryContext context) { throw null; } + } public partial class FormValueProvider : Microsoft.AspNetCore.Mvc.ModelBinding.BindingSourceValueProvider, Microsoft.AspNetCore.Mvc.ModelBinding.IEnumerableValueProvider, Microsoft.AspNetCore.Mvc.ModelBinding.IValueProvider { public FormValueProvider(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource bindingSource, Microsoft.AspNetCore.Http.IFormCollection values, System.Globalization.CultureInfo culture) : base (default(Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource)) { } diff --git a/src/Mvc/Mvc.Core/src/ControllerBase.cs b/src/Mvc/Mvc.Core/src/ControllerBase.cs index 1bca7a09f3..e502ca144a 100644 --- a/src/Mvc/Mvc.Core/src/ControllerBase.cs +++ b/src/Mvc/Mvc.Core/src/ControllerBase.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc @@ -31,6 +32,7 @@ namespace Microsoft.AspNetCore.Mvc private IModelBinderFactory _modelBinderFactory; private IObjectModelValidator _objectValidator; private IUrlHelper _url; + private ProblemDetailsFactory _problemDetailsFactory; /// /// Gets the for the executing action. @@ -189,6 +191,28 @@ namespace Microsoft.AspNetCore.Mvc } } + public ProblemDetailsFactory ProblemDetailsFactory + { + get + { + if (_problemDetailsFactory == null) + { + _problemDetailsFactory = HttpContext?.RequestServices?.GetRequiredService(); + } + + return _problemDetailsFactory; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _problemDetailsFactory = value; + } + } + /// /// Gets the for user associated with the executing action. /// @@ -1821,6 +1845,34 @@ namespace Microsoft.AspNetCore.Mvc public virtual ConflictObjectResult Conflict([ActionResultObjectValue] ModelStateDictionary modelState) => new ConflictObjectResult(modelState); + /// + /// Creates an that produces a response. + /// + /// The value for .. + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The created for the response. + [NonAction] + public virtual ObjectResult Problem( + string detail = null, + string instance = null, + int? statusCode = null, + string title = null, + string type = null) + { + var problemDetails = ProblemDetailsFactory.CreateProblemDetails( + HttpContext, + statusCode: statusCode ?? 500, + title: title, + type: type, + detail: detail, + instance: instance); + + return new ObjectResult(problemDetails); + } + /// /// Creates an that produces a response. /// @@ -1837,31 +1889,64 @@ namespace Microsoft.AspNetCore.Mvc } /// - /// Creates an that produces a response. + /// Creates an that produces a response + /// with validation errors from . /// + /// The . /// The created for the response. [NonAction] public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelStateDictionary modelStateDictionary) - { - if (modelStateDictionary == null) - { - throw new ArgumentNullException(nameof(modelStateDictionary)); - } + => ValidationProblem(detail: null, modelStateDictionary: modelStateDictionary); - var validationProblem = new ValidationProblemDetails(modelStateDictionary); - return new BadRequestObjectResult(validationProblem); - } /// - /// Creates an that produces a response + /// Creates an that produces a response /// with validation errors from . /// - /// The created for the response. + /// The created for the response. [NonAction] public virtual ActionResult ValidationProblem() + => ValidationProblem(ModelState); + + /// + /// Creates an that produces a response + /// with a value. + /// + /// The value for . + /// The value for . + /// The status code. + /// The value for . + /// The value for . + /// The . + /// When uses . + /// The created for the response. + [NonAction] + public virtual ActionResult ValidationProblem( + string detail = null, + string instance = null, + int? statusCode = null, + string title = null, + string type = null, + [ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null) { - var validationProblem = new ValidationProblemDetails(ModelState); - return new BadRequestObjectResult(validationProblem); + modelStateDictionary ??= ModelState; + + var validationProblem = ProblemDetailsFactory.CreateValidationProblemDetails( + HttpContext, + modelStateDictionary, + statusCode: statusCode, + title: title, + type: type, + detail: detail, + instance: instance); + + if (validationProblem.Status == 400) + { + // For compatibility with 2.x, continue producing BadRequestObjectResult instances if the status code is 400. + return new BadRequestObjectResult(validationProblem); + } + + return new ObjectResult(validationProblem); } /// diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs index 3ed6a3eeef..4cbd1a2dba 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -10,13 +9,9 @@ using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection { - internal class ApiBehaviorOptionsSetup : - IConfigureOptions, - IPostConfigureOptions + internal class ApiBehaviorOptionsSetup : IConfigureOptions { - internal static readonly Func DefaultFactory = DefaultInvalidModelStateResponse; - internal static readonly Func ProblemDetailsFactory = - ProblemDetailsInvalidModelStateResponse; + private ProblemDetailsFactory _problemDetailsFactory; public void Configure(ApiBehaviorOptions options) { @@ -25,20 +20,34 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(options)); } - options.InvalidModelStateResponseFactory = DefaultFactory; + options.InvalidModelStateResponseFactory = context => + { + // ProblemDetailsFactory depends on the ApiBehaviorOptions instance. We intentionally avoid constructor injecting + // it in this options setup to to avoid a DI cycle. + _problemDetailsFactory ??= context.HttpContext.RequestServices.GetRequiredService(); + return ProblemDetailsInvalidModelStateResponse(_problemDetailsFactory, context); + }; + ConfigureClientErrorMapping(options); } - public void PostConfigure(string name, ApiBehaviorOptions options) + internal static IActionResult ProblemDetailsInvalidModelStateResponse(ProblemDetailsFactory problemDetailsFactory, ActionContext context) { - // We want to use problem details factory only if - // (a) it has not been opted out of (SuppressMapClientErrors = true) - // (b) a different factory was configured - if (!options.SuppressMapClientErrors && - object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory)) + var problemDetails = problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState); + ObjectResult result; + if (problemDetails.Status == 400) { - options.InvalidModelStateResponseFactory = ProblemDetailsFactory; + // For compatibility with 2.x, continue producing BadRequestObjectResult instances if the status code is 400. + result = new BadRequestObjectResult(problemDetails); } + else + { + result = new ObjectResult(problemDetails); + } + result.ContentTypes.Add("application/problem+json"); + result.ContentTypes.Add("application/problem+xml"); + + return result; } // Internal for unit testing @@ -91,33 +100,12 @@ namespace Microsoft.Extensions.DependencyInjection Link = "https://tools.ietf.org/html/rfc4918#section-11.2", Title = Resources.ApiConventions_Title_422, }; - } - private static IActionResult DefaultInvalidModelStateResponse(ActionContext context) - { - var result = new BadRequestObjectResult(context.ModelState); - - result.ContentTypes.Add("application/json"); - result.ContentTypes.Add("application/xml"); - - return result; - } - - internal static IActionResult ProblemDetailsInvalidModelStateResponse(ActionContext context) - { - var problemDetails = new ValidationProblemDetails(context.ModelState) + options.ClientErrorMapping[500] = new ClientErrorData { - Status = StatusCodes.Status400BadRequest, + Link = "https://tools.ietf.org/html/rfc7231#section-6.6.1", + Title = Resources.ApiConventions_Title_500, }; - - ProblemDetailsClientErrorFactory.SetTraceId(context, problemDetails); - - var result = new BadRequestObjectResult(problemDetails); - - result.ContentTypes.Add("application/problem+json"); - result.ContentTypes.Add("application/problem+xml"); - - return result; } } } diff --git a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index e34f74c221..82181cd034 100644 --- a/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -149,8 +149,6 @@ namespace Microsoft.Extensions.DependencyInjection ServiceDescriptor.Transient, MvcCoreMvcOptionsSetup>()); services.TryAddEnumerable( ServiceDescriptor.Transient, ApiBehaviorOptionsSetup>()); - services.TryAddEnumerable( - ServiceDescriptor.Transient, ApiBehaviorOptionsSetup>()); services.TryAddEnumerable( ServiceDescriptor.Transient, MvcCoreRouteOptionsSetup>()); @@ -260,6 +258,7 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddSingleton, ContentResultExecutor>(); services.TryAddSingleton, SystemTextJsonResultExecutor>(); services.TryAddSingleton(); + services.TryAddSingleton(); // // Route Handlers diff --git a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs index 861f277e48..664615fa8d 100644 --- a/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs +++ b/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -17,14 +18,13 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// public class SystemTextJsonOutputFormatter : TextOutputFormatter { - /// /// Initializes a new instance. /// - /// The . - public SystemTextJsonOutputFormatter(JsonOptions options) + /// The . + public SystemTextJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions) { - SerializerOptions = options.JsonSerializerOptions; + SerializerOptions = jsonSerializerOptions; SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode); @@ -33,6 +33,19 @@ namespace Microsoft.AspNetCore.Mvc.Formatters SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax); } + internal static SystemTextJsonOutputFormatter CreateFormatter(JsonOptions jsonOptions) + { + var jsonSerializerOptions = jsonOptions.JsonSerializerOptions; + + if (jsonSerializerOptions.Encoder is null) + { + // If the user hasn't explicitly configured the encoder, use the less strict encoder that does not encode all non-ASCII characters. + jsonSerializerOptions = jsonSerializerOptions.Copy(JavaScriptEncoder.UnsafeRelaxedJsonEscaping); + } + + return new SystemTextJsonOutputFormatter(jsonSerializerOptions); + } + /// /// Gets the used to configure the . /// diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ActionSelectionTable.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ActionSelectionTable.cs index c028fcf6ed..4521e310ad 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ActionSelectionTable.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ActionSelectionTable.cs @@ -66,10 +66,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure // For action selection, ignore attribute routed actions items: actions.Items.Where(a => a.AttributeRouteInfo == null), - getRouteKeys: a => a.RouteValues.Keys, + getRouteKeys: a => a.RouteValues?.Keys, getRouteValue: (a, key) => { - a.RouteValues.TryGetValue(key, out var value); + string value = null; + a.RouteValues?.TryGetValue(key, out value); return value ?? string.Empty; }); } @@ -87,10 +88,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure return e.GetType() == typeof(Endpoint); }), - getRouteKeys: e => e.Metadata.GetMetadata().RouteValues.Keys, + getRouteKeys: e => e.Metadata.GetMetadata()?.RouteValues?.Keys, getRouteValue: (e, key) => { - e.Metadata.GetMetadata().RouteValues.TryGetValue(key, out var value); + string value = null; + e.Metadata.GetMetadata()?.RouteValues?.TryGetValue(key, out value); return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty; }); } @@ -112,9 +114,13 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure foreach (var item in items) { - foreach (var key in getRouteKeys(item)) + var keys = getRouteKeys(item); + if (keys != null) { - routeKeys.Add(key); + foreach (var key in keys) + { + routeKeys.Add(key); + } } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs new file mode 100644 index 0000000000..cbd1a2aad4 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs @@ -0,0 +1,97 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + internal sealed class DefaultProblemDetailsFactory : ProblemDetailsFactory + { + private readonly ApiBehaviorOptions _options; + + public DefaultProblemDetailsFactory(IOptions options) + { + _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + } + + public override ProblemDetails CreateProblemDetails( + HttpContext httpContext, + int? statusCode = null, + string title = null, + string type = null, + string detail = null, + string instance = null) + { + statusCode ??= 500; + + var problemDetails = new ProblemDetails + { + Status = statusCode, + Title = title, + Type = type, + Detail = detail, + Instance = instance, + }; + + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + + return problemDetails; + } + + public override ValidationProblemDetails CreateValidationProblemDetails( + HttpContext httpContext, + ModelStateDictionary modelStateDictionary, + int? statusCode = null, + string title = null, + string type = null, + string detail = null, + string instance = null) + { + if (modelStateDictionary == null) + { + throw new ArgumentNullException(nameof(modelStateDictionary)); + } + + statusCode ??= 400; + + var problemDetails = new ValidationProblemDetails(modelStateDictionary) + { + Status = statusCode, + Type = type, + Detail = detail, + Instance = instance, + }; + + if (title != null) + { + // For validation problem details, don't overwrite the default title with null. + problemDetails.Title = title; + } + + ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value); + + return problemDetails; + } + + private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode) + { + problemDetails.Status ??= statusCode; + + if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData)) + { + problemDetails.Title ??= clientErrorData.Title; + problemDetails.Type ??= clientErrorData.Link; + } + + var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; + if (traceId != null) + { + problemDetails.Extensions["traceId"] = traceId; + } + } + } +} diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/JsonSerializerOptionsCopyConstructor.cs b/src/Mvc/Mvc.Core/src/Infrastructure/JsonSerializerOptionsCopyConstructor.cs new file mode 100644 index 0000000000..f378aec55e --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Infrastructure/JsonSerializerOptionsCopyConstructor.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text.Encodings.Web; + +namespace System.Text.Json +{ + internal static class JsonSerializerOptionsCopyConstructor + { + public static JsonSerializerOptions Copy(this JsonSerializerOptions serializerOptions, JavaScriptEncoder encoder) + { + var copiedOptions = new JsonSerializerOptions + { + AllowTrailingCommas = serializerOptions.AllowTrailingCommas, + DefaultBufferSize = serializerOptions.DefaultBufferSize, + DictionaryKeyPolicy = serializerOptions.DictionaryKeyPolicy, + IgnoreNullValues = serializerOptions.IgnoreNullValues, + IgnoreReadOnlyProperties = serializerOptions.IgnoreReadOnlyProperties, + MaxDepth = serializerOptions.MaxDepth, + PropertyNameCaseInsensitive = serializerOptions.PropertyNameCaseInsensitive, + PropertyNamingPolicy = serializerOptions.PropertyNamingPolicy, + ReadCommentHandling = serializerOptions.ReadCommentHandling, + WriteIndented = serializerOptions.WriteIndented + }; + + for (var i = 0; i < serializerOptions.Converters.Count; i++) + { + copiedOptions.Converters.Add(serializerOptions.Converters[i]); + } + + copiedOptions.Encoder = encoder; + + return copiedOptions; + } + } +} diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs index cbc141fe76..a2c0dbdcae 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/MvcCoreMvcOptionsSetup.cs @@ -87,13 +87,16 @@ namespace Microsoft.AspNetCore.Mvc options.OutputFormatters.Add(new HttpNoContentOutputFormatter()); options.OutputFormatters.Add(new StringOutputFormatter()); options.OutputFormatters.Add(new StreamOutputFormatter()); - options.OutputFormatters.Add(new SystemTextJsonOutputFormatter(_jsonOptions.Value)); + + var jsonOutputFormatter = SystemTextJsonOutputFormatter.CreateFormatter(_jsonOptions.Value); + options.OutputFormatters.Add(jsonOutputFormatter); // Set up ValueProviders options.ValueProviderFactories.Add(new FormValueProviderFactory()); options.ValueProviderFactories.Add(new RouteValueProviderFactory()); options.ValueProviderFactories.Add(new QueryStringValueProviderFactory()); options.ValueProviderFactories.Add(new JQueryFormValueProviderFactory()); + options.ValueProviderFactories.Add(new FormFileValueProviderFactory()); // Set up metadata providers ConfigureAdditionalModelMetadataDetailsProviders(options.ModelMetadataDetailsProviders); diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs index ff47a18fa6..f0fe9eadaf 100644 --- a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsClientErrorFactory.cs @@ -2,37 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Diagnostics; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.Infrastructure { internal class ProblemDetailsClientErrorFactory : IClientErrorFactory { - private static readonly string TraceIdentifierKey = "traceId"; - private readonly ApiBehaviorOptions _options; + private readonly ProblemDetailsFactory _problemDetailsFactory; - public ProblemDetailsClientErrorFactory(IOptions options) + public ProblemDetailsClientErrorFactory(ProblemDetailsFactory problemDetailsFactory) { - _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); + _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory)); } public IActionResult GetClientError(ActionContext actionContext, IClientErrorActionResult clientError) { - var problemDetails = new ProblemDetails - { - Status = clientError.StatusCode, - Type = "about:blank", - }; - - if (clientError.StatusCode is int statusCode && - _options.ClientErrorMapping.TryGetValue(statusCode, out var errorData)) - { - problemDetails.Title = errorData.Title; - problemDetails.Type = errorData.Link; - - SetTraceId(actionContext, problemDetails); - } + var problemDetails = _problemDetailsFactory.CreateProblemDetails(actionContext.HttpContext, clientError.StatusCode); return new ObjectResult(problemDetails) { @@ -44,11 +28,5 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure }, }; } - - internal static void SetTraceId(ActionContext actionContext, ProblemDetails problemDetails) - { - var traceId = Activity.Current?.Id ?? actionContext.HttpContext.TraceIdentifier; - problemDetails.Extensions[TraceIdentifierKey] = traceId; - } } } diff --git a/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsFactory.cs b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsFactory.cs new file mode 100644 index 0000000000..aeb930dfb5 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Infrastructure/ProblemDetailsFactory.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + /// + /// Factory to produce and . + /// + public abstract class ProblemDetailsFactory + { + /// + /// Creates a instance that configures defaults based on values specified in . + /// + /// The . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The instance. + public abstract ProblemDetails CreateProblemDetails( + HttpContext httpContext, + int? statusCode = null, + string title = null, + string type = null, + string detail = null, + string instance = null); + + /// + /// Creates a instance that configures defaults based on values specified in . + /// + /// The . + /// The . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The instance. + public abstract ValidationProblemDetails CreateValidationProblemDetails( + HttpContext httpContext, + ModelStateDictionary modelStateDictionary, + int? statusCode = null, + string title = null, + string type = null, + string detail = null, + string instance = null); + } +} diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProvider.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProvider.cs new file mode 100644 index 0000000000..bf976f5b03 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProvider.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + /// + /// An adapter for data stored in an . + /// + /// + /// Unlike most instances, does not provide any values, but + /// specifically responds to queries. This allows the model binding system to + /// recurse in to deeply nested object graphs with only values for form files. + /// + public sealed class FormFileValueProvider : IValueProvider + { + private readonly IFormFileCollection _files; + private PrefixContainer _prefixContainer; + + /// + /// Creates a value provider for . + /// + /// The . + public FormFileValueProvider(IFormFileCollection files) + { + _files = files ?? throw new ArgumentNullException(nameof(files)); + } + + private PrefixContainer PrefixContainer + { + get + { + _prefixContainer ??= CreatePrefixContainer(_files); + return _prefixContainer; + } + } + + private static PrefixContainer CreatePrefixContainer(IFormFileCollection formFiles) + { + var fileNames = new List(); + var count = formFiles.Count; + for (var i = 0; i < count; i++) + { + var file = formFiles[i]; + + // If there is an in the form and is left blank. + // This matches the filtering behavior from FormFileModelBinder + if (file.Length == 0 && string.IsNullOrEmpty(file.FileName)) + { + continue; + } + + fileNames.Add(file.Name); + } + + return new PrefixContainer(fileNames); + } + + /// + public bool ContainsPrefix(string prefix) => PrefixContainer.ContainsPrefix(prefix); + + /// + public ValueProviderResult GetValue(string key) => ValueProviderResult.None; + } +} diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs new file mode 100644 index 0000000000..0f3ae8b8ff --- /dev/null +++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + /// + /// A for . + /// + public sealed class FormFileValueProviderFactory : IValueProviderFactory + { + /// + public Task CreateValueProviderAsync(ValueProviderFactoryContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var request = context.ActionContext.HttpContext.Request; + if (request.HasFormContentType) + { + // Allocating a Task only when the body is multipart form. + return AddValueProviderAsync(context, request); + } + + return Task.CompletedTask; + } + + private static async Task AddValueProviderAsync(ValueProviderFactoryContext context, HttpRequest request) + { + var formCollection = await request.ReadFormAsync(); + if (formCollection.Files.Count > 0) + { + var valueProvider = new FormFileValueProvider(formCollection.Files); + context.ValueProviders.Add(valueProvider); + } + } + } +} diff --git a/src/Mvc/Mvc.Core/src/Resources.resx b/src/Mvc/Mvc.Core/src/Resources.resx index a255bcf725..03647f8e23 100644 --- a/src/Mvc/Mvc.Core/src/Resources.resx +++ b/src/Mvc/Mvc.Core/src/Resources.resx @@ -510,4 +510,7 @@ Unexcepted end when reading JSON. + + An error occured while processing your request. + \ No newline at end of file diff --git a/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs index 369502d633..118a5b8e84 100644 --- a/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs +++ b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointMatcherPolicy.cs @@ -138,9 +138,12 @@ namespace Microsoft.AspNetCore.Mvc.Routing var values = new RouteValueDictionary(dynamicValues); // Include values that were matched by the fallback route. - foreach (var kvp in originalValues) + if (originalValues != null) { - values.TryAdd(kvp.Key, kvp.Value); + foreach (var kvp in originalValues) + { + values.TryAdd(kvp.Key, kvp.Value); + } } // Update the route values diff --git a/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointSelector.cs b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointSelector.cs index fbe0337420..f07b22b94b 100644 --- a/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointSelector.cs +++ b/src/Mvc/Mvc.Core/src/Routing/DynamicControllerEndpointSelector.cs @@ -11,10 +11,17 @@ namespace Microsoft.AspNetCore.Mvc.Routing { internal class DynamicControllerEndpointSelector : IDisposable { - private readonly ControllerActionEndpointDataSource _dataSource; + private readonly EndpointDataSource _dataSource; private readonly DataSourceDependentCache> _cache; public DynamicControllerEndpointSelector(ControllerActionEndpointDataSource dataSource) + : this((EndpointDataSource)dataSource) + { + } + + // Exposed for tests. We need to accept a more specific type in the constructor for DI + // to work. + protected DynamicControllerEndpointSelector(EndpointDataSource dataSource) { if (dataSource == null) { diff --git a/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs b/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs index 89a38e587a..8c3a4b17c6 100644 --- a/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs +++ b/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs @@ -9,8 +9,11 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Routing; @@ -2187,7 +2190,6 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test { // Arrange var contentController = new ContentController(); - var expected = MediaTypeHeaderValue.Parse("text/plain; charset=utf-8"); // Act var contentResult = (ContentResult)contentController.Content_WithNoEncoding(); @@ -2290,6 +2292,163 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test Assert.Equal(statusCode, result.StatusCode); } + [Fact] + public void ValidationProblemDetails_Works() + { + // Arrange + var context = new ControllerContext(new ActionContext( + new DefaultHttpContext { TraceIdentifier = "some-trace" }, + new RouteData(), + new ControllerActionDescriptor())); + + context.ModelState.AddModelError("key1", "error1"); + + var options = GetApiBehaviorOptions(); + var controller = new TestableController + { + ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)), + ControllerContext = context, + }; + + // Act + var actionResult = controller.ValidationProblem(); + + // Assert + var badRequestResult = Assert.IsType(actionResult); + var problemDetails = Assert.IsType(badRequestResult.Value); + Assert.Equal(400, problemDetails.Status); + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + Assert.Equal("some-trace", problemDetails.Extensions["traceId"]); + Assert.Equal(new[] { "error1" }, problemDetails.Errors["key1"]); + } + + [Fact] + public void ValidationProblemDetails_UsesSpecifiedTitle() + { + // Arrange + var detail = "My detail"; + var title = "Custom title"; + var type = "http://custom-link"; + var options = GetApiBehaviorOptions(); + + var controller = new TestableController + { + ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)), + }; + + // Act + var actionResult = controller.ValidationProblem(detail: detail, title: title, type: type); + + // Assert + var badRequestResult = Assert.IsType(actionResult); + var problemDetails = Assert.IsType(badRequestResult.Value); + Assert.Equal(title, problemDetails.Title); + Assert.Equal(type, problemDetails.Type); + Assert.Equal(detail, problemDetails.Detail); + } + + [Fact] + public void ProblemDetails_Works() + { + // Arrange + var context = new ControllerContext(new ActionContext( + new DefaultHttpContext { TraceIdentifier = "some-trace" }, + new RouteData(), + new ControllerActionDescriptor())); + + var options = GetApiBehaviorOptions(); + + var controller = new TestableController + { + ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)), + ControllerContext = context, + }; + + // Act + var actionResult = controller.Problem(); + + // Assert + var badRequestResult = Assert.IsType(actionResult); + var problemDetails = Assert.IsType(badRequestResult.Value); + Assert.Equal(500, problemDetails.Status); + Assert.Equal("An error occured while processing your request.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type); + Assert.Equal("some-trace", problemDetails.Extensions["traceId"]); + } + + [Fact] + public void ProblemDetails_UsesPassedInValues() + { + // Arrange + var title = "The website is down."; + var detail = "Try again in a few minutes."; + var options = GetApiBehaviorOptions(); + + var controller = new TestableController + { + ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)), + }; + + // Act + var actionResult = controller.Problem(detail, title: title); + + // Assert + var badRequestResult = Assert.IsType(actionResult); + var problemDetails = Assert.IsType(badRequestResult.Value); + Assert.Equal(500, problemDetails.Status); + Assert.Equal(title, problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type); + Assert.Equal(detail, problemDetails.Detail); + } + + [Fact] + public void ProblemDetails_UsesPassedInStatusCode() + { + // Arrange + var options = GetApiBehaviorOptions(); + + var controller = new TestableController + { + ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)), + }; + + // Act + var actionResult = controller.Problem(statusCode: 422); + + // Assert + var badRequestResult = Assert.IsType(actionResult); + var problemDetails = Assert.IsType(badRequestResult.Value); + Assert.Equal(422, problemDetails.Status); + Assert.Equal("Unprocessable entity.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc4918#section-11.2", problemDetails.Type); + } + + private static ApiBehaviorOptions GetApiBehaviorOptions() + { + return new ApiBehaviorOptions + { + ClientErrorMapping = + { + [400] = new ClientErrorData + { + Title = "One or more validation errors occurred.", + Link = "https://tools.ietf.org/html/rfc7231#section-6.5.1" + }, + [422] = new ClientErrorData + { + Title = "Unprocessable entity.", + Link = "https://tools.ietf.org/html/rfc4918#section-11.2" + }, + [500] = new ClientErrorData + { + Title = "An error occured while processing your request.", + Link = "https://tools.ietf.org/html/rfc7231#section-6.6.1" + } + } + }; + } + public static IEnumerable RedirectTestData { get diff --git a/src/Mvc/Mvc.Core/test/CreatedAtActionResultTests.cs b/src/Mvc/Mvc.Core/test/CreatedAtActionResultTests.cs index be493c9eda..3b559992e5 100644 --- a/src/Mvc/Mvc.Core/test/CreatedAtActionResultTests.cs +++ b/src/Mvc/Mvc.Core/test/CreatedAtActionResultTests.cs @@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Mvc { var options = Options.Create(new MvcOptions()); options.Value.OutputFormatters.Add(new StringOutputFormatter()); - options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + options.Value.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( diff --git a/src/Mvc/Mvc.Core/test/CreatedAtRouteResultTests.cs b/src/Mvc/Mvc.Core/test/CreatedAtRouteResultTests.cs index 50eb7ba12b..f6cf7d583a 100644 --- a/src/Mvc/Mvc.Core/test/CreatedAtRouteResultTests.cs +++ b/src/Mvc/Mvc.Core/test/CreatedAtRouteResultTests.cs @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc { var options = Options.Create(new MvcOptions()); options.Value.OutputFormatters.Add(new StringOutputFormatter()); - options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + options.Value.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( diff --git a/src/Mvc/Mvc.Core/test/CreatedResultTests.cs b/src/Mvc/Mvc.Core/test/CreatedResultTests.cs index 22e6b13637..56db1771c7 100644 --- a/src/Mvc/Mvc.Core/test/CreatedResultTests.cs +++ b/src/Mvc/Mvc.Core/test/CreatedResultTests.cs @@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.Mvc { var options = Options.Create(new MvcOptions()); options.Value.OutputFormatters.Add(new StringOutputFormatter()); - options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + options.Value.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( diff --git a/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs b/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs index 82a137bf87..ebca639ee9 100644 --- a/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs +++ b/src/Mvc/Mvc.Core/test/DependencyInjection/ApiBehaviorOptionsSetupTest.cs @@ -6,31 +6,18 @@ using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Xunit; namespace Microsoft.Extensions.DependencyInjection { public class ApiBehaviorOptionsSetupTest { - [Fact] - public void Configure_AssignsInvalidModelStateResponseFactory() - { - // Arrange - var optionsSetup = new ApiBehaviorOptionsSetup(); - var options = new ApiBehaviorOptions(); - - // Act - optionsSetup.Configure(options); - - // Assert - Assert.Same(ApiBehaviorOptionsSetup.DefaultFactory, options.InvalidModelStateResponseFactory); - } - [Fact] public void Configure_AddsClientErrorMappings() { // Arrange - var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, }; + var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, 500, }; var optionsSetup = new ApiBehaviorOptionsSetup(); var options = new ApiBehaviorOptions(); @@ -41,50 +28,15 @@ namespace Microsoft.Extensions.DependencyInjection Assert.Equal(expected, options.ClientErrorMapping.Keys); } - [Fact] - public void PostConfigure_SetProblemDetailsModelStateResponseFactory() - { - // Arrange - var optionsSetup = new ApiBehaviorOptionsSetup(); - var options = new ApiBehaviorOptions(); - - // Act - optionsSetup.Configure(options); - optionsSetup.PostConfigure(string.Empty, options); - - // Assert - Assert.Same(ApiBehaviorOptionsSetup.ProblemDetailsFactory, options.InvalidModelStateResponseFactory); - } - - [Fact] - public void PostConfigure_DoesNotSetProblemDetailsFactory_IfValueWasModified() - { - // Arrange - var optionsSetup = new ApiBehaviorOptionsSetup(); - var options = new ApiBehaviorOptions(); - Func expected = _ => null; - - // Act - optionsSetup.Configure(options); - // This is equivalent to user code updating the value via ConfigureOptions - options.InvalidModelStateResponseFactory = expected; - optionsSetup.PostConfigure(string.Empty, options); - - // Assert - Assert.Same(expected, options.InvalidModelStateResponseFactory); - } - [Fact] public void ProblemDetailsInvalidModelStateResponse_ReturnsBadRequestWithProblemDetails() { // Arrange - var actionContext = new ActionContext - { - HttpContext = new DefaultHttpContext { TraceIdentifier = "42" }, - }; + var actionContext = GetActionContext(); + var factory = GetProblemDetailsFactory(); // Act - var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext); + var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext); // Assert var badRequest = Assert.IsType(result); @@ -92,6 +44,30 @@ namespace Microsoft.Extensions.DependencyInjection var problemDetails = Assert.IsType(badRequest.Value); Assert.Equal(400, problemDetails.Status); + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + } + + [Fact] + public void ProblemDetailsInvalidModelStateResponse_UsesUserConfiguredLink() + { + // Arrange + var link = "http://mylink"; + var actionContext = GetActionContext(); + + var factory = GetProblemDetailsFactory(options => options.ClientErrorMapping[400].Link = link); + + // Act + var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext); + + // Assert + var badRequest = Assert.IsType(result); + Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, badRequest.ContentTypes.OrderBy(c => c)); + + var problemDetails = Assert.IsType(badRequest.Value); + Assert.Equal(400, problemDetails.Status); + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal(link, problemDetails.Type); } [Fact] @@ -100,13 +76,11 @@ namespace Microsoft.Extensions.DependencyInjection // Arrange using (new ActivityReplacer()) { - var actionContext = new ActionContext - { - HttpContext = new DefaultHttpContext { TraceIdentifier = "42" }, - }; + var actionContext = GetActionContext(); + var factory = GetProblemDetailsFactory(); // Act - var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext); + var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext); // Assert var badRequest = Assert.IsType(result); @@ -119,18 +93,38 @@ namespace Microsoft.Extensions.DependencyInjection public void ProblemDetailsInvalidModelStateResponse_SetsTraceIdFromRequest_IfActivityIsNull() { // Arrange - var actionContext = new ActionContext - { - HttpContext = new DefaultHttpContext { TraceIdentifier = "42" }, - }; + var actionContext = GetActionContext(); + var factory = GetProblemDetailsFactory(); // Act - var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext); + var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(factory, actionContext); // Assert var badRequest = Assert.IsType(result); var problemDetails = Assert.IsType(badRequest.Value); Assert.Equal("42", problemDetails.Extensions["traceId"]); } + + private static ProblemDetailsFactory GetProblemDetailsFactory(Action configure = null) + { + var options = new ApiBehaviorOptions(); + var setup = new ApiBehaviorOptionsSetup(); + + setup.Configure(options); + if (configure != null) + { + configure(options); + } + + return new DefaultProblemDetailsFactory(Options.Options.Create(options)); + } + + private static ActionContext GetActionContext() + { + return new ActionContext + { + HttpContext = new DefaultHttpContext { TraceIdentifier = "42" }, + }; + } } } diff --git a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs index 1c9117ef6e..612129f8c2 100644 --- a/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs +++ b/src/Mvc/Mvc.Core/test/DependencyInjection/MvcCoreServiceCollectionExtensionsTest.cs @@ -264,13 +264,6 @@ namespace Microsoft.AspNetCore.Mvc typeof(ApiBehaviorOptionsSetup), } }, - { - typeof(IPostConfigureOptions), - new Type[] - { - typeof(ApiBehaviorOptionsSetup), - } - }, { typeof(IActionConstraintProvider), new Type[] diff --git a/src/Mvc/Mvc.Core/test/Formatters/FormatFilterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/FormatFilterTest.cs index fea2967990..3dd386d314 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/FormatFilterTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/FormatFilterTest.cs @@ -465,7 +465,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters // Set up default output formatters. MvcOptions.OutputFormatters.Add(new HttpNoContentOutputFormatter()); MvcOptions.OutputFormatters.Add(new StringOutputFormatter()); - MvcOptions.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + MvcOptions.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); // Set up default mapping for json extensions to content type MvcOptions.FormatterMappings.SetMediaTypeMappingForFormat( diff --git a/src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs b/src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs index 0467d0e51c..8990b1cb9c 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/JsonOutputFormatterTestBase.cs @@ -76,6 +76,71 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } } + [Theory] + [MemberData(nameof(WriteCorrectCharacterEncoding))] + public async Task WriteToStreamAsync_UsesCorrectCharacterEncoding( + string content, + string encodingAsString, + bool isDefaultEncoding) + { + // Arrange + var formatter = GetOutputFormatter(); + var expectedContent = "\"" + content + "\""; + var mediaType = MediaTypeHeaderValue.Parse(string.Format("application/json; charset={0}", encodingAsString)); + var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding); + + + var body = new MemoryStream(); + var actionContext = GetActionContext(mediaType, body); + + var outputFormatterContext = new OutputFormatterWriteContext( + actionContext.HttpContext, + new TestHttpResponseStreamWriterFactory().CreateWriter, + typeof(string), + content) + { + ContentType = new StringSegment(mediaType.ToString()), + }; + + // Act + await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding(encodingAsString)); + + // Assert + var actualContent = encoding.GetString(body.ToArray()); + Assert.Equal(expectedContent, actualContent, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public async Task WriteResponseBodyAsync_Encodes() + { + // Arrange + var formatter = GetOutputFormatter(); + var expectedContent = "{\"key\":\"Hello \\n Wörld\"}"; + var content = new { key = "Hello \n Wörld" }; + + var mediaType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); + var encoding = CreateOrGetSupportedEncoding(formatter, "utf-8", isDefaultEncoding: true); + + var body = new MemoryStream(); + var actionContext = GetActionContext(mediaType, body); + + var outputFormatterContext = new OutputFormatterWriteContext( + actionContext.HttpContext, + new TestHttpResponseStreamWriterFactory().CreateWriter, + typeof(string), + content) + { + ContentType = new StringSegment(mediaType.ToString()), + }; + + // Act + await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding("utf-8")); + + // Assert + var actualContent = encoding.GetString(body.ToArray()); + Assert.Equal(expectedContent, actualContent); + } + [Fact] public async Task ErrorDuringSerialization_DoesNotCloseTheBrackets() { diff --git a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs index 0a3d90e18b..cd245872ef 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonOutputFormatterTest.cs @@ -1,56 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.IO; -using System.Text; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using Xunit; - namespace Microsoft.AspNetCore.Mvc.Formatters { public class SystemTextJsonOutputFormatterTest : JsonOutputFormatterTestBase { protected override TextOutputFormatter GetOutputFormatter() { - return new SystemTextJsonOutputFormatter(new JsonOptions()); - } - - [Theory] - [MemberData(nameof(WriteCorrectCharacterEncoding))] - public async Task WriteToStreamAsync_UsesCorrectCharacterEncoding( - string content, - string encodingAsString, - bool isDefaultEncoding) - { - // Arrange - var formatter = GetOutputFormatter(); - var expectedContent = "\"" + JavaScriptEncoder.Default.Encode(content) + "\""; - var mediaType = MediaTypeHeaderValue.Parse(string.Format("application/json; charset={0}", encodingAsString)); - var encoding = CreateOrGetSupportedEncoding(formatter, encodingAsString, isDefaultEncoding); - - - var body = new MemoryStream(); - var actionContext = GetActionContext(mediaType, body); - - var outputFormatterContext = new OutputFormatterWriteContext( - actionContext.HttpContext, - new TestHttpResponseStreamWriterFactory().CreateWriter, - typeof(string), - content) - { - ContentType = new StringSegment(mediaType.ToString()), - }; - - // Act - await formatter.WriteResponseBodyAsync(outputFormatterContext, Encoding.GetEncoding(encodingAsString)); - - // Assert - var actualContent = encoding.GetString(body.ToArray()); - Assert.Equal(expectedContent, actualContent, StringComparer.OrdinalIgnoreCase); + return SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions()); } } } diff --git a/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs b/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs index 4f8948aa62..6206aeab62 100644 --- a/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs +++ b/src/Mvc/Mvc.Core/test/Formatters/TranscodingReadStreamTest.cs @@ -1,8 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Collections.Generic; using System.IO; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; diff --git a/src/Mvc/Mvc.Core/test/HttpNotFoundObjectResultTest.cs b/src/Mvc/Mvc.Core/test/HttpNotFoundObjectResultTest.cs index 42ef48b2f0..7956120d2a 100644 --- a/src/Mvc/Mvc.Core/test/HttpNotFoundObjectResultTest.cs +++ b/src/Mvc/Mvc.Core/test/HttpNotFoundObjectResultTest.cs @@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Mvc { var options = Options.Create(new MvcOptions()); options.Value.OutputFormatters.Add(new StringOutputFormatter()); - options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + options.Value.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( diff --git a/src/Mvc/Mvc.Core/test/HttpOkObjectResultTest.cs b/src/Mvc/Mvc.Core/test/HttpOkObjectResultTest.cs index 49fac14fff..8735a8c89f 100644 --- a/src/Mvc/Mvc.Core/test/HttpOkObjectResultTest.cs +++ b/src/Mvc/Mvc.Core/test/HttpOkObjectResultTest.cs @@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Mvc { var options = Options.Create(new MvcOptions()); options.Value.OutputFormatters.Add(new StringOutputFormatter()); - options.Value.OutputFormatters.Add(new SystemTextJsonOutputFormatter(new JsonOptions())); + options.Value.OutputFormatters.Add(SystemTextJsonOutputFormatter.CreateFormatter(new JsonOptions())); var services = new ServiceCollection(); services.AddSingleton>(new ObjectResultExecutor( diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetailsFactoryTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetailsFactoryTest.cs new file mode 100644 index 0000000000..0bf84da14f --- /dev/null +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetailsFactoryTest.cs @@ -0,0 +1,188 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + public class ProblemDetailsFactoryTest + { + private readonly ProblemDetailsFactory Factory = GetProblemDetails(); + + [Fact] + public void CreateProblemDetails_DefaultValues() + { + // Act + var problemDetails = Factory.CreateProblemDetails(GetHttpContext()); + + // Assert + Assert.Equal(500, problemDetails.Status); + Assert.Equal("An error occured while processing your request.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type); + Assert.Null(problemDetails.Instance); + Assert.Null(problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + } + + [Fact] + public void CreateProblemDetails_WithStatusCode() + { + // Act + var problemDetails = Factory.CreateProblemDetails(GetHttpContext(), statusCode: 406); + + // Assert + Assert.Equal(406, problemDetails.Status); + Assert.Equal("Not Acceptable", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.6", problemDetails.Type); + Assert.Null(problemDetails.Instance); + Assert.Null(problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + } + + [Fact] + public void CreateProblemDetails_WithDetailAndTitle() + { + // Act + var title = "Some title"; + var detail = "some detail"; + var problemDetails = Factory.CreateProblemDetails(GetHttpContext(), statusCode: 406, title: title, detail: detail); + + // Assert + Assert.Equal(406, problemDetails.Status); + Assert.Equal(title, problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.6", problemDetails.Type); + Assert.Null(problemDetails.Instance); + Assert.Equal(detail, problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + } + + [Fact] + public void CreateValidationProblemDetails_DefaultValues() + { + // Act + var modelState = new ModelStateDictionary(); + modelState.AddModelError("some-key", "some-value"); + var problemDetails = Factory.CreateValidationProblemDetails(GetHttpContext(), modelState); + + // Assert + Assert.Equal(400, problemDetails.Status); + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + Assert.Null(problemDetails.Instance); + Assert.Null(problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + Assert.Collection( + problemDetails.Errors, + kvp => + { + Assert.Equal("some-key", kvp.Key); + Assert.Equal(new[] { "some-value" }, kvp.Value); + }); + } + + [Fact] + public void CreateValidationProblemDetails_WithStatusCode() + { + // Act + var modelState = new ModelStateDictionary(); + modelState.AddModelError("some-key", "some-value"); + var problemDetails = Factory.CreateValidationProblemDetails(GetHttpContext(), modelState, 422); + + // Assert + Assert.Equal(422, problemDetails.Status); + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc4918#section-11.2", problemDetails.Type); + Assert.Null(problemDetails.Instance); + Assert.Null(problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + Assert.Collection( + problemDetails.Errors, + kvp => + { + Assert.Equal("some-key", kvp.Key); + Assert.Equal(new[] { "some-value" }, kvp.Value); + }); + } + + [Fact] + public void CreateValidationProblemDetails_WithTitleAndInstance() + { + // Act + var title = "Some title"; + var instance = "some instance"; + var modelState = new ModelStateDictionary(); + modelState.AddModelError("some-key", "some-value"); + var problemDetails = Factory.CreateValidationProblemDetails(GetHttpContext(), modelState, title: title, instance: instance); + + // Assert + Assert.Equal(400, problemDetails.Status); + Assert.Equal(title, problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + Assert.Equal(instance, problemDetails.Instance); + Assert.Null(problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal("some-trace", kvp.Value); + }); + Assert.Collection( + problemDetails.Errors, + kvp => + { + Assert.Equal("some-key", kvp.Key); + Assert.Equal(new[] { "some-value" }, kvp.Value); + }); + } + + private static DefaultHttpContext GetHttpContext() + { + return new DefaultHttpContext + { + TraceIdentifier = "some-trace", + }; + } + + private static ProblemDetailsFactory GetProblemDetails() + { + var options = new ApiBehaviorOptions(); + new ApiBehaviorOptionsSetup().Configure(options); + return new DefaultProblemDetailsFactory(Options.Create(options)); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs index 3a84deabe7..93ac451d37 100644 --- a/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs +++ b/src/Mvc/Mvc.Core/test/Infrastructure/ProblemDetalsClientErrorFactoryTest.cs @@ -15,13 +15,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure { // Arrange var clientError = new UnsupportedMediaTypeResult(); - var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions + var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions { ClientErrorMapping = { [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -31,7 +32,6 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes); var problemDetails = Assert.IsType(objectResult.Value); Assert.Equal(415, problemDetails.Status); - Assert.Equal("about:blank", problemDetails.Type); Assert.Null(problemDetails.Title); Assert.Null(problemDetails.Detail); Assert.Null(problemDetails.Instance); @@ -42,13 +42,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure { // Arrange var clientError = new UnsupportedMediaTypeResult(); - var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions + var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions { ClientErrorMapping = { [415] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -71,13 +72,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure using (new ActivityReplacer()) { var clientError = new UnsupportedMediaTypeResult(); - var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions + var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions { ClientErrorMapping = { - [415] = new ClientErrorData { Link = "Some link", Title = "Summary" }, + [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); @@ -96,13 +98,14 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure { // Arrange var clientError = new UnsupportedMediaTypeResult(); - var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions + var problemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(new ApiBehaviorOptions { ClientErrorMapping = { - [415] = new ClientErrorData { Link = "Some link", Title = "Summary" }, + [405] = new ClientErrorData { Link = "Some link", Title = "Summary" }, }, })); + var factory = new ProblemDetailsClientErrorFactory(problemDetailsFactory); // Act var result = factory.GetClientError(GetActionContext(), clientError); diff --git a/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj b/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj index 39df066d88..1552b9c6e4 100644 --- a/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj +++ b/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj @@ -2,6 +2,7 @@ netcoreapp3.0 + Microsoft.AspNetCore.Mvc diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs new file mode 100644 index 0000000000..c0030a0ba5 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + public class FormFileValueProviderFactoryTest + { + [Fact] + public async Task CreateValueProviderAsync_DoesNotAddValueProvider_IfRequestDoesNotHaveFormContent() + { + // Arrange + var factory = new FormFileValueProviderFactory(); + var context = CreateContext("application/json"); + + // Act + await factory.CreateValueProviderAsync(context); + + // Assert + Assert.Empty(context.ValueProviders); + } + + [Fact] + public async Task CreateValueProviderAsync_DoesNotAddValueProvider_IfFileCollectionIsEmpty() + { + // Arrange + var factory = new FormFileValueProviderFactory(); + var context = CreateContext("multipart/form-data"); + + // Act + await factory.CreateValueProviderAsync(context); + + // Assert + Assert.Empty(context.ValueProviders); + } + + [Fact] + public async Task CreateValueProviderAsync_AddsValueProvider() + { + // Arrange + var factory = new FormFileValueProviderFactory(); + var context = CreateContext("multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq"); + var files = (FormFileCollection)context.ActionContext.HttpContext.Request.Form.Files; + files.Add(new FormFile(Stream.Null, 0, 10, "some-name", "some-name")); + + // Act + await factory.CreateValueProviderAsync(context); + + // Assert + Assert.Collection( + context.ValueProviders, + v => Assert.IsType(v)); + } + + private static ValueProviderFactoryContext CreateContext(string contentType) + { + var context = new DefaultHttpContext(); + context.Request.ContentType = contentType; + context.Request.Form = new FormCollection(new Dictionary(), new FormFileCollection()); + var actionContext = new ActionContext(context, new RouteData(), new ActionDescriptor()); + + return new ValueProviderFactoryContext(actionContext); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderTest.cs new file mode 100644 index 0000000000..7be36b91ff --- /dev/null +++ b/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderTest.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ModelBinding +{ + public class FormFileValueProviderTest + { + [Fact] + public void ContainsPrefix_ReturnsFalse_IfFileIs0LengthAndFileNameIsEmpty() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.Request.ContentType = "multipart/form-data"; + var formFiles = new FormFileCollection(); + formFiles.Add(new FormFile(Stream.Null, 0, 0, "file", fileName: null)); + httpContext.Request.Form = new FormCollection(new Dictionary(), formFiles); + + var valueProvider = new FormFileValueProvider(formFiles); + + // Act + var result = valueProvider.ContainsPrefix("file"); + + // Assert + Assert.False(result); + } + + [Fact] + public void ContainsPrefix_ReturnsTrue_IfFileExists() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.Request.ContentType = "multipart/form-data"; + var formFiles = new FormFileCollection(); + formFiles.Add(new FormFile(Stream.Null, 0, 10, "file", "file")); + httpContext.Request.Form = new FormCollection(new Dictionary(), formFiles); + + var valueProvider = new FormFileValueProvider(formFiles); + + // Act + var result = valueProvider.ContainsPrefix("file"); + + // Assert + Assert.True(result); + } + + [Fact] + public void GetValue_ReturnsNoneResult() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.Request.ContentType = "multipart/form-data"; + var formFiles = new FormFileCollection(); + formFiles.Add(new FormFile(Stream.Null, 0, 10, "file", "file")); + httpContext.Request.Form = new FormCollection(new Dictionary(), formFiles); + + var valueProvider = new FormFileValueProvider(formFiles); + + // Act + var result = valueProvider.GetValue("file"); + + // Assert + Assert.Equal(ValueProviderResult.None, result); + } + } +} diff --git a/src/Mvc/Mvc.Core/test/Routing/DynamicControllerEndpointMatcherPolicyTest.cs b/src/Mvc/Mvc.Core/test/Routing/DynamicControllerEndpointMatcherPolicyTest.cs new file mode 100644 index 0000000000..52e7dcece6 --- /dev/null +++ b/src/Mvc/Mvc.Core/test/Routing/DynamicControllerEndpointMatcherPolicyTest.cs @@ -0,0 +1,272 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + public class DynamicControllerEndpointMatcherPolicyTest + { + public DynamicControllerEndpointMatcherPolicyTest() + { + var actions = new ActionDescriptor[] + { + new ControllerActionDescriptor() + { + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["action"] = "Index", + ["controller"] = "Home", + }, + }, + new ControllerActionDescriptor() + { + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["action"] = "About", + ["controller"] = "Home", + }, + }, + new ControllerActionDescriptor() + { + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["action"] = "Index", + ["controller"] = "Blog", + }, + } + }; + + ControllerEndpoints = new[] + { + new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(actions[0]), "Test1"), + new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(actions[1]), "Test2"), + new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(actions[2]), "Test3"), + }; + + DynamicEndpoint = new Endpoint( + _ => Task.CompletedTask, + new EndpointMetadataCollection(new object[] + { + new DynamicControllerRouteValueTransformerMetadata(typeof(CustomTransformer)), + }), + "dynamic"); + + DataSource = new DefaultEndpointDataSource(ControllerEndpoints); + + Selector = new TestDynamicControllerEndpointSelector(DataSource); + + var services = new ServiceCollection(); + services.AddRouting(); + services.AddScoped(s => + { + var transformer = new CustomTransformer(); + transformer.Transform = (c, values) => Transform(c, values); + return transformer; + }); + Services = services.BuildServiceProvider(); + + Comparer = Services.GetRequiredService(); + } + + private EndpointMetadataComparer Comparer { get; } + + private DefaultEndpointDataSource DataSource { get; } + + private Endpoint[] ControllerEndpoints { get; } + + private Endpoint DynamicEndpoint { get; } + + private DynamicControllerEndpointSelector Selector { get; } + + private IServiceProvider Services { get; } + + private Func> Transform { get; set; } + + [Fact] + public async Task ApplyAsync_NoMatch() + { + // Arrange + var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer); + + var endpoints = new[] { DynamicEndpoint, }; + var values = new RouteValueDictionary[] { null, }; + var scores = new[] { 0, }; + + var candidates = new CandidateSet(endpoints, values, scores); + candidates.SetValidity(0, false); + + Transform = (c, values) => + { + throw new InvalidOperationException(); + }; + + var httpContext = new DefaultHttpContext() + { + RequestServices = Services, + }; + + // Act + await policy.ApplyAsync(httpContext, candidates); + + // Assert + Assert.False(candidates.IsValidCandidate(0)); + } + + [Fact] + public async Task ApplyAsync_HasMatchNoEndpointFound() + { + // Arrange + var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer); + + var endpoints = new[] { DynamicEndpoint, }; + var values = new RouteValueDictionary[] { null, }; + var scores = new[] { 0, }; + + var candidates = new CandidateSet(endpoints, values, scores); + + Transform = (c, values) => + { + return new ValueTask(new RouteValueDictionary()); + }; + + var httpContext = new DefaultHttpContext() + { + RequestServices = Services, + }; + + // Act + await policy.ApplyAsync(httpContext, candidates); + + // Assert + Assert.Null(candidates[0].Endpoint); + Assert.Null(candidates[0].Values); + Assert.False(candidates.IsValidCandidate(0)); + } + + [Fact] + public async Task ApplyAsync_HasMatchFindsEndpoint_WithoutRouteValues() + { + // Arrange + var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer); + + var endpoints = new[] { DynamicEndpoint, }; + var values = new RouteValueDictionary[] { null, }; + var scores = new[] { 0, }; + + var candidates = new CandidateSet(endpoints, values, scores); + + Transform = (c, values) => + { + return new ValueTask(new RouteValueDictionary(new + { + controller = "Home", + action = "Index", + })); + }; + + var httpContext = new DefaultHttpContext() + { + RequestServices = Services, + }; + + // Act + await policy.ApplyAsync(httpContext, candidates); + + // Assert + Assert.Same(ControllerEndpoints[0], candidates[0].Endpoint); + Assert.Collection( + candidates[0].Values.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("action", kvp.Key); + Assert.Equal("Index", kvp.Value); + }, + kvp => + { + Assert.Equal("controller", kvp.Key); + Assert.Equal("Home", kvp.Value); + }); + Assert.True(candidates.IsValidCandidate(0)); + } + + [Fact] + public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues() + { + // Arrange + var policy = new DynamicControllerEndpointMatcherPolicy(Selector, Comparer); + + var endpoints = new[] { DynamicEndpoint, }; + var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), }; + var scores = new[] { 0, }; + + var candidates = new CandidateSet(endpoints, values, scores); + + Transform = (c, values) => + { + return new ValueTask(new RouteValueDictionary(new + { + controller = "Home", + action = "Index", + })); + }; + + var httpContext = new DefaultHttpContext() + { + RequestServices = Services, + }; + + // Act + await policy.ApplyAsync(httpContext, candidates); + + // Assert + Assert.Same(ControllerEndpoints[0], candidates[0].Endpoint); + Assert.Collection( + candidates[0].Values.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("action", kvp.Key); + Assert.Equal("Index", kvp.Value); + }, + kvp => + { + Assert.Equal("controller", kvp.Key); + Assert.Equal("Home", kvp.Value); + }, + kvp => + { + Assert.Equal("slug", kvp.Key); + Assert.Equal("test", kvp.Value); + }); + Assert.True(candidates.IsValidCandidate(0)); + } + + private class TestDynamicControllerEndpointSelector : DynamicControllerEndpointSelector + { + public TestDynamicControllerEndpointSelector(EndpointDataSource dataSource) + : base(dataSource) + { + } + } + + private class CustomTransformer : DynamicRouteValueTransformer + { + public Func> Transform { get; set; } + + public override ValueTask TransformAsync(HttpContext httpContext, RouteValueDictionary values) + { + return Transform(httpContext, values); + } + } + } +} diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs index 6751d41658..ea68558d4b 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointMatcherPolicy.cs @@ -146,9 +146,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure var values = new RouteValueDictionary(dynamicValues); // Include values that were matched by the fallback route. - foreach (var kvp in originalValues) + if (originalValues != null) { - values.TryAdd(kvp.Key, kvp.Value); + foreach (var kvp in originalValues) + { + values.TryAdd(kvp.Key, kvp.Value); + } } // Update the route values diff --git a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointSelector.cs b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointSelector.cs index 0e143db00b..b333f2947b 100644 --- a/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointSelector.cs +++ b/src/Mvc/Mvc.RazorPages/src/Infrastructure/DynamicPageEndpointSelector.cs @@ -11,10 +11,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { internal class DynamicPageEndpointSelector : IDisposable { - private readonly PageActionEndpointDataSource _dataSource; + private readonly EndpointDataSource _dataSource; private readonly DataSourceDependentCache> _cache; public DynamicPageEndpointSelector(PageActionEndpointDataSource dataSource) + : this((EndpointDataSource)dataSource) + { + } + + // Exposed for tests. We need to accept a more specific type in the constructor for DI + // to work. + protected DynamicPageEndpointSelector(EndpointDataSource dataSource) { if (dataSource == null) { diff --git a/src/Mvc/Mvc.RazorPages/test/Infrastructure/DynamicPageEndpointMatcherPolicyTest.cs b/src/Mvc/Mvc.RazorPages/test/Infrastructure/DynamicPageEndpointMatcherPolicyTest.cs new file mode 100644 index 0000000000..1cea019f5c --- /dev/null +++ b/src/Mvc/Mvc.RazorPages/test/Infrastructure/DynamicPageEndpointMatcherPolicyTest.cs @@ -0,0 +1,265 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public class DynamicPageEndpointMatcherPolicyTest + { + public DynamicPageEndpointMatcherPolicyTest() + { + var actions = new ActionDescriptor[] + { + new PageActionDescriptor() + { + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["page"] = "/Index", + }, + }, + new PageActionDescriptor() + { + RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["page"] = "/About", + }, + }, + }; + + PageEndpoints = new[] + { + new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(actions[0]), "Test1"), + new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(actions[1]), "Test2"), + }; + + DynamicEndpoint = new Endpoint( + _ => Task.CompletedTask, + new EndpointMetadataCollection(new object[] + { + new DynamicPageRouteValueTransformerMetadata(typeof(CustomTransformer)), + }), + "dynamic"); + + DataSource = new DefaultEndpointDataSource(PageEndpoints); + + Selector = new TestDynamicPageEndpointSelector(DataSource); + + var services = new ServiceCollection(); + services.AddRouting(); + services.AddScoped(s => + { + var transformer = new CustomTransformer(); + transformer.Transform = (c, values) => Transform(c, values); + return transformer; + }); + Services = services.BuildServiceProvider(); + + Comparer = Services.GetRequiredService(); + + LoadedEndpoint = new Endpoint(_ => Task.CompletedTask, EndpointMetadataCollection.Empty, "Loaded"); + + var loader = new Mock(); + loader + .Setup(l => l.LoadAsync(It.IsAny())) + .Returns(Task.FromResult(new CompiledPageActionDescriptor() { Endpoint = LoadedEndpoint, })); + Loader = loader.Object; + + } + + private EndpointMetadataComparer Comparer { get; } + + private DefaultEndpointDataSource DataSource { get; } + + private Endpoint[] PageEndpoints { get; } + + private Endpoint DynamicEndpoint { get; } + + private Endpoint LoadedEndpoint { get; } + + private PageLoader Loader { get; } + + private DynamicPageEndpointSelector Selector { get; } + + private IServiceProvider Services { get; } + + private Func> Transform { get; set; } + + [Fact] + public async Task ApplyAsync_NoMatch() + { + // Arrange + var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer); + + var endpoints = new[] { DynamicEndpoint, }; + var values = new RouteValueDictionary[] { null, }; + var scores = new[] { 0, }; + + var candidates = new CandidateSet(endpoints, values, scores); + candidates.SetValidity(0, false); + + Transform = (c, values) => + { + throw new InvalidOperationException(); + }; + + var httpContext = new DefaultHttpContext() + { + RequestServices = Services, + }; + + // Act + await policy.ApplyAsync(httpContext, candidates); + + // Assert + Assert.False(candidates.IsValidCandidate(0)); + } + + [Fact] + public async Task ApplyAsync_HasMatchNoEndpointFound() + { + // Arrange + var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer); + + var endpoints = new[] { DynamicEndpoint, }; + var values = new RouteValueDictionary[] { null, }; + var scores = new[] { 0, }; + + var candidates = new CandidateSet(endpoints, values, scores); + + Transform = (c, values) => + { + return new ValueTask(new RouteValueDictionary()); + }; + + var httpContext = new DefaultHttpContext() + { + RequestServices = Services, + }; + + // Act + await policy.ApplyAsync(httpContext, candidates); + + // Assert + Assert.Null(candidates[0].Endpoint); + Assert.Null(candidates[0].Values); + Assert.False(candidates.IsValidCandidate(0)); + } + + [Fact] + public async Task ApplyAsync_HasMatchFindsEndpoint_WithoutRouteValues() + { + // Arrange + var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer); + + var endpoints = new[] { DynamicEndpoint, }; + var values = new RouteValueDictionary[] { null, }; + var scores = new[] { 0, }; + + var candidates = new CandidateSet(endpoints, values, scores); + + Transform = (c, values) => + { + return new ValueTask(new RouteValueDictionary(new + { + page = "/Index", + })); + }; + + var httpContext = new DefaultHttpContext() + { + RequestServices = Services, + }; + + // Act + await policy.ApplyAsync(httpContext, candidates); + + // Assert + Assert.Same(LoadedEndpoint, candidates[0].Endpoint); + Assert.Collection( + candidates[0].Values.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("page", kvp.Key); + Assert.Equal("/Index", kvp.Value); + }); + Assert.True(candidates.IsValidCandidate(0)); + } + + [Fact] + public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues() + { + // Arrange + var policy = new DynamicPageEndpointMatcherPolicy(Selector, Loader, Comparer); + + var endpoints = new[] { DynamicEndpoint, }; + var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), }; + var scores = new[] { 0, }; + + var candidates = new CandidateSet(endpoints, values, scores); + + Transform = (c, values) => + { + return new ValueTask(new RouteValueDictionary(new + { + page = "/Index", + })); + }; + + var httpContext = new DefaultHttpContext() + { + RequestServices = Services, + }; + + // Act + await policy.ApplyAsync(httpContext, candidates); + + // Assert + Assert.Same(LoadedEndpoint, candidates[0].Endpoint); + Assert.Collection( + candidates[0].Values.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("page", kvp.Key); + Assert.Equal("/Index", kvp.Value); + }, + kvp => + { + Assert.Equal("slug", kvp.Key); + Assert.Equal("test", kvp.Value); + }); + Assert.True(candidates.IsValidCandidate(0)); + } + + private class TestDynamicPageEndpointSelector : DynamicPageEndpointSelector + { + public TestDynamicPageEndpointSelector(EndpointDataSource dataSource) + : base(dataSource) + { + } + } + + private class CustomTransformer : DynamicRouteValueTransformer + { + public Func> Transform { get; set; } + + public override ValueTask TransformAsync(HttpContext httpContext, RouteValueDictionary values) + { + return Transform(httpContext, values); + } + } + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs index df59a73215..48f74b2113 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/SystemTextJsonHelper.cs @@ -1,7 +1,7 @@ - -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Text.Encodings.Web; using System.Text.Json; using Microsoft.AspNetCore.Html; using Microsoft.Extensions.Options; @@ -10,11 +10,11 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { internal class SystemTextJsonHelper : IJsonHelper { - private readonly JsonOptions _options; + private readonly JsonSerializerOptions _htmlSafeJsonSerializerOptions; public SystemTextJsonHelper(IOptions options) { - _options = options.Value; + _htmlSafeJsonSerializerOptions = GetHtmlSafeSerializerOptions(options.Value.JsonSerializerOptions); } /// @@ -22,8 +22,18 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { // JsonSerializer always encodes non-ASCII chars, so we do not need // to do anything special with the SerializerOptions - var json = JsonSerializer.Serialize(value, _options.JsonSerializerOptions); + var json = JsonSerializer.Serialize(value, _htmlSafeJsonSerializerOptions); return new HtmlString(json); } + + private static JsonSerializerOptions GetHtmlSafeSerializerOptions(JsonSerializerOptions serializerOptions) + { + if (serializerOptions.Encoder is null || serializerOptions.Encoder == JavaScriptEncoder.Default) + { + return serializerOptions; + } + + return serializerOptions.Copy(JavaScriptEncoder.Default); + } } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs b/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs index 665e050196..421fc49d37 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs @@ -6,7 +6,7 @@ using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Rendering; diff --git a/src/Mvc/Mvc.ViewFeatures/test/Rendering/JsonHelperTestBase.cs b/src/Mvc/Mvc.ViewFeatures/test/Rendering/JsonHelperTestBase.cs index cfbabe735b..205a3479b6 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Rendering/JsonHelperTestBase.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/Rendering/JsonHelperTestBase.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering // Assert var htmlString = Assert.IsType(result); - Assert.Equal(expectedOutput, htmlString.ToString()); + Assert.Equal(expectedOutput, htmlString.ToString(), ignoreCase: true); } [Fact] @@ -71,14 +71,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { HTML = $"Hello pingüino" }; - var expectedOutput = "{\"html\":\"Hello ping\\u00fcino\"}"; + var expectedOutput = "{\"html\":\"Hello pingüino\"}"; // Act var result = helper.Serialize(obj); // Assert var htmlString = Assert.IsType(result); - Assert.Equal(expectedOutput, htmlString.ToString()); + Assert.Equal(expectedOutput, htmlString.ToString(), ignoreCase: true); } [Fact] @@ -99,5 +99,25 @@ namespace Microsoft.AspNetCore.Mvc.Rendering var htmlString = Assert.IsType(result); Assert.Equal(expectedOutput, htmlString.ToString()); } + + + [Fact] + public virtual void Serialize_WithHTMLNonAsciiAndControlChars() + { + // Arrange + var helper = GetJsonHelper(); + var obj = new + { + HTML = "Hello \n pingüino" + }; + var expectedOutput = "{\"html\":\"\\u003cb\\u003eHello \\n pingüino\\u003c/b\\u003e\"}"; + + // Act + var result = helper.Serialize(obj); + + // Assert + var htmlString = Assert.IsType(result); + Assert.Equal(expectedOutput, htmlString.ToString()); + } } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/Rendering/SystemTextJsonHelperTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Rendering/SystemTextJsonHelperTest.cs index ce03e5f633..97f4c145b7 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/Rendering/SystemTextJsonHelperTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/Rendering/SystemTextJsonHelperTest.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Text.Json; +using System.Text.Encodings.Web; using Microsoft.AspNetCore.Html; using Microsoft.Extensions.Options; using Xunit; @@ -10,31 +10,13 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { public class SystemTextJsonHelperTest : JsonHelperTestBase { - protected override IJsonHelper GetJsonHelper() + protected override IJsonHelper GetJsonHelper() => GetJsonHelper(new JsonOptions()); + + private static IJsonHelper GetJsonHelper(JsonOptions options) { - var options = new JsonOptions() { JsonSerializerOptions = { PropertyNamingPolicy = JsonNamingPolicy.CamelCase } }; return new SystemTextJsonHelper(Options.Create(options)); } - [Fact] - public override void Serialize_EscapesHtmlByDefault() - { - // Arrange - var helper = GetJsonHelper(); - var obj = new - { - HTML = "John Doe" - }; - var expectedOutput = "{\"html\":\"\\u003Cb\\u003EJohn Doe\\u003C/b\\u003E\"}"; - - // Act - var result = helper.Serialize(obj); - - // Assert - var htmlString = Assert.IsType(result); - Assert.Equal(expectedOutput, htmlString.ToString()); - } - [Fact] public override void Serialize_WithNonAsciiChars() { @@ -53,5 +35,56 @@ namespace Microsoft.AspNetCore.Mvc.Rendering var htmlString = Assert.IsType(result); Assert.Equal(expectedOutput, htmlString.ToString()); } + + [Fact] + public override void Serialize_WithHTMLNonAsciiAndControlChars() + { + // Arrange + var helper = GetJsonHelper(); + var obj = new + { + HTML = "Hello \n pingüino" + }; + var expectedOutput = "{\"html\":\"\\u003Cb\\u003EHello \\n ping\\u00FCino\\u003C/b\\u003E\"}"; + + // Act + var result = helper.Serialize(obj); + + // Assert + var htmlString = Assert.IsType(result); + Assert.Equal(expectedOutput, htmlString.ToString()); + } + + [Fact] + public void Serialize_UsesOptionsConfiguredInTheProvider() + { + // Arrange + // This should use property-casing and indentation, but the result should be HTML-safe + var options = new JsonOptions + { + JsonSerializerOptions = + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + PropertyNamingPolicy = null, + WriteIndented = true, + } + }; + var helper = GetJsonHelper(options); + var obj = new + { + HTML = "John" + }; + var expectedOutput = +@"{ + ""HTML"": ""\u003Cb\u003EJohn\u003C/b\u003E"" +}"; + + // Act + var result = helper.Serialize(obj); + + // Assert + var htmlString = Assert.IsType(result); + Assert.Equal(expectedOutput, htmlString.ToString(), ignoreLineEndingDifferences: true); + } } } diff --git a/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs b/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs index 00eda811ea..6b68683118 100644 --- a/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs +++ b/src/Mvc/Mvc/test/MvcOptionsSetupTest.cs @@ -82,7 +82,8 @@ namespace Microsoft.AspNetCore.Mvc provider => Assert.IsType(provider), provider => Assert.IsType(provider), provider => Assert.IsType(provider), - provider => Assert.IsType(provider)); + provider => Assert.IsType(provider), + provider => Assert.IsType(provider)); } [Fact] diff --git a/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs b/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs index d7c6326ace..ca92aba1b6 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ApiBehaviorTest.cs @@ -56,6 +56,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { Converters = { new ValidationProblemDetailsConverter() } }); + + Assert.Equal("One or more validation errors occurred.", problemDetails.Title); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); + Assert.Collection( problemDetails.Errors.OrderBy(kvp => kvp.Key), kvp => diff --git a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs index c4f7ee9dcc..c5c4ae51fd 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/JsonOutputFormatterTestBase.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; @@ -11,6 +10,7 @@ using System.Text; using System.Threading.Tasks; using FormatterWebSite.Controllers; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; @@ -21,13 +21,14 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { protected JsonOutputFormatterTestBase(MvcTestFixture fixture) { - var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); - Client = factory.CreateDefaultClient(); + Factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); + Client = Factory.CreateDefaultClient(); } private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => builder.UseStartup(); + public WebApplicationFactory Factory { get; } public HttpClient Client { get; } [Fact] @@ -100,6 +101,17 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("\"Hello Mr. 🦊\"", await response.Content.ReadAsStringAsync()); } + [Fact] + public virtual async Task Formatting_StringValueWithNonAsciiCharacters() + { + // Act + var response = await Client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.StringWithNonAsciiContent)}"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + Assert.Equal("\"Une bête de cirque\"", await response.Content.ReadAsStringAsync()); + } + [Fact] public virtual async Task Formatting_SimpleModel() { diff --git a/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs index d508bfe731..d0af74ada1 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs @@ -2,8 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Net; +using System.Text.Encodings.Web; using System.Threading.Tasks; using FormatterWebSite.Controllers; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Microsoft.AspNetCore.Mvc.FunctionalTests @@ -15,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { } - [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/11459")] + [Fact] public override Task SerializableErrorIsReturnedInExpectedFormat() => base.SerializableErrorIsReturnedInExpectedFormat(); [Fact] @@ -29,6 +31,25 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal("\"Hello Mr. \\uD83E\\uDD8A\"", await response.Content.ReadAsStringAsync()); } + [Fact] + public async Task Formatting_WithCustomEncoder() + { + // Arrange + static void ConfigureServices(IServiceCollection serviceCollection) + { + serviceCollection.AddControllers() + .AddJsonOptions(o => o.JsonSerializerOptions.Encoder = JavaScriptEncoder.Default); + } + var client = Factory.WithWebHostBuilder(c => c.ConfigureServices(ConfigureServices)).CreateClient(); + + // Act + var response = await client.GetAsync($"/JsonOutputFormatter/{nameof(JsonOutputFormatterController.StringWithNonAsciiContent)}"); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + Assert.Equal("\"Une b\\u00EAte de cirque\"", await response.Content.ReadAsStringAsync()); + } + [Fact] public override Task Formatting_DictionaryType() => base.Formatting_DictionaryType(); diff --git a/src/Mvc/test/Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs b/src/Mvc/test/Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs index a7ff6eeba3..92ea35e319 100644 --- a/src/Mvc/test/Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs +++ b/src/Mvc/test/Mvc.IntegrationTests/FormFileModelBindingIntegrationTest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -75,6 +76,397 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.Equal(ModelValidationState.Valid, modelState[key].ValidationState); } + [Fact] + public async Task BindProperty_WithOnlyFormFile_WithEmptyPrefix() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Person) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + UpdateRequest(request, data, "Address.File"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson.Address); + var file = Assert.IsAssignableFrom(boundPerson.Address.File); + Assert.Equal("form-data; name=Address.File; filename=text.txt", file.ContentDisposition); + using var reader = new StreamReader(boundPerson.Address.File.OpenReadStream()); + Assert.Equal(data, reader.ReadToEnd()); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Collection( + modelState.OrderBy(kvp => kvp.Key), + kvp => + { + var (key, value) = kvp; + Assert.Equal("Address.File", kvp.Key); + Assert.Null(value.RawValue); + Assert.Empty(value.Errors); + Assert.Equal(ModelValidationState.Valid, value.ValidationState); + }); + } + + [Fact] + public async Task BindProperty_WithOnlyFormFile_WithPrefix() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Person) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + UpdateRequest(request, data, "Parameter1.Address.File"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var boundPerson = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(boundPerson.Address); + var file = Assert.IsAssignableFrom(boundPerson.Address.File); + Assert.Equal("form-data; name=Parameter1.Address.File; filename=text.txt", file.ContentDisposition); + using var reader = new StreamReader(boundPerson.Address.File.OpenReadStream()); + Assert.Equal(data, reader.ReadToEnd()); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Collection( + modelState.OrderBy(kvp => kvp.Key), + kvp => + { + var (key, value) = kvp; + Assert.Equal("Parameter1.Address.File", kvp.Key); + Assert.Null(value.RawValue); + Assert.Empty(value.Errors); + Assert.Equal(ModelValidationState.Valid, value.ValidationState); + }); + } + + private class Group + { + public string GroupName { get; set; } + + public Person Person { get; set; } + } + + [Fact] + public async Task BindProperty_OnFormFileInNestedSubClass_AtSecondLevel_WhenSiblingPropertyIsSpecified() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Group) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.QueryString = QueryString.Create("Person.Address.Zip", "98056"); + UpdateRequest(request, data, "Person.Address.File"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var group = Assert.IsType(modelBindingResult.Model); + Assert.Null(group.GroupName); + var boundPerson = group.Person; + Assert.NotNull(boundPerson); + Assert.NotNull(boundPerson.Address); + var file = Assert.IsAssignableFrom(boundPerson.Address.File); + Assert.Equal("form-data; name=Person.Address.File; filename=text.txt", file.ContentDisposition); + using var reader = new StreamReader(boundPerson.Address.File.OpenReadStream()); + Assert.Equal(data, reader.ReadToEnd()); + Assert.Equal(98056, boundPerson.Address.Zip); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Collection( + modelState.OrderBy(kvp => kvp.Key), + kvp => + { + var (key, value) = kvp; + Assert.Equal("Person.Address.File", kvp.Key); + Assert.Null(value.RawValue); + Assert.Empty(value.Errors); + Assert.Equal(ModelValidationState.Valid, value.ValidationState); + }, + kvp => + { + var (key, value) = kvp; + Assert.Equal("Person.Address.Zip", kvp.Key); + Assert.Equal("98056", value.RawValue); + Assert.Empty(value.Errors); + Assert.Equal(ModelValidationState.Valid, value.ValidationState); + }); + } + + private class Fleet + { + public int? Id { get; set; } + + public FleetGarage Garage { get; set; } + } + + public class FleetGarage + { + public string Name { get; set; } + + public FleetVehicle[] Vehicles { get; set; } + } + + public class FleetVehicle + { + public string Name { get; set; } + + public IFormFile Spec { get; set; } + + public FleetVehicle BackupVehicle { get; set; } + } + + [Fact] + public async Task BindProperty_OnFormFileInNestedSubClass_AtSecondLevel_RecursiveModel() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor() + { + Name = "fleet", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Fleet) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.QueryString = QueryString.Create("fleet.Garage.Name", "WestEnd"); + UpdateRequest(request, data, "fleet.Garage.Vehicles[0].Spec"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var fleet = Assert.IsType(modelBindingResult.Model); + Assert.Null(fleet.Id); + + Assert.NotNull(fleet.Garage); + Assert.NotNull(fleet.Garage.Vehicles); + + var vehicle = Assert.Single(fleet.Garage.Vehicles); + var file = Assert.IsAssignableFrom(vehicle.Spec); + + using var reader = new StreamReader(file.OpenReadStream()); + Assert.Equal(data, reader.ReadToEnd()); + Assert.Null(vehicle.Name); + Assert.Null(vehicle.BackupVehicle); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Collection( + modelState.OrderBy(kvp => kvp.Key), + kvp => + { + var (key, value) = kvp; + Assert.Equal("fleet.Garage.Name", kvp.Key); + Assert.Equal("WestEnd", value.RawValue); + Assert.Empty(value.Errors); + Assert.Equal(ModelValidationState.Valid, value.ValidationState); + }, + kvp => + { + var (key, value) = kvp; + Assert.Equal("fleet.Garage.Vehicles[0].Spec", kvp.Key); + Assert.Equal(ModelValidationState.Valid, value.ValidationState); + }); + } + + [Fact] + public async Task BindProperty_OnFormFileInNestedSubClass_AtThirdLevel_RecursiveModel() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor() + { + Name = "fleet", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Fleet) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.QueryString = QueryString.Create("fleet.Garage.Name", "WestEnd"); + UpdateRequest(request, data, "fleet.Garage.Vehicles[0].BackupVehicle.Spec"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var fleet = Assert.IsType(modelBindingResult.Model); + Assert.Null(fleet.Id); + + Assert.NotNull(fleet.Garage); + Assert.NotNull(fleet.Garage.Vehicles); + + var vehicle = Assert.Single(fleet.Garage.Vehicles); + Assert.Null(vehicle.Spec); + Assert.NotNull(vehicle.BackupVehicle); + var file = Assert.IsAssignableFrom(vehicle.BackupVehicle.Spec); + + using var reader = new StreamReader(file.OpenReadStream()); + Assert.Equal(data, reader.ReadToEnd()); + Assert.Null(vehicle.Name); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Collection( + modelState.OrderBy(kvp => kvp.Key), + kvp => + { + var (key, value) = kvp; + Assert.Equal("fleet.Garage.Name", kvp.Key); + Assert.Equal("WestEnd", value.RawValue); + Assert.Empty(value.Errors); + Assert.Equal(ModelValidationState.Valid, value.ValidationState); + }, + kvp => + { + var (key, value) = kvp; + Assert.Equal("fleet.Garage.Vehicles[0].BackupVehicle.Spec", kvp.Key); + Assert.Equal(ModelValidationState.Valid, value.ValidationState); + }); + } + + [Fact] + public async Task BindProperty_OnFormFileInNestedSubClass_AtSecondLevel_WhenSiblingPropertiesAreNotSpecified() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor() + { + Name = "Parameter1", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Group) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.QueryString = QueryString.Create("GroupName", "TestGroup"); + UpdateRequest(request, data, "Person.Address.File"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var group = Assert.IsType(modelBindingResult.Model); + Assert.Equal("TestGroup", group.GroupName); + var boundPerson = group.Person; + Assert.NotNull(boundPerson); + Assert.NotNull(boundPerson.Address); + var file = Assert.IsAssignableFrom(boundPerson.Address.File); + Assert.Equal("form-data; name=Person.Address.File; filename=text.txt", file.ContentDisposition); + using var reader = new StreamReader(boundPerson.Address.File.OpenReadStream()); + Assert.Equal(data, reader.ReadToEnd()); + Assert.Equal(0, boundPerson.Address.Zip); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Collection( + modelState.OrderBy(kvp => kvp.Key), + kvp => + { + var (key, value) = kvp; + Assert.Equal("GroupName", kvp.Key); + Assert.Equal("TestGroup", value.RawValue); + Assert.Empty(value.Errors); + Assert.Equal(ModelValidationState.Valid, value.ValidationState); + }, + kvp => + { + var (key, value) = kvp; + Assert.Equal("Person.Address.File", kvp.Key); + Assert.Null(value.RawValue); + Assert.Empty(value.Errors); + Assert.Equal(ModelValidationState.Valid, value.ValidationState); + }); + } + private class ListContainer1 { [ModelBinder(Name = "files")] @@ -354,15 +746,526 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests Assert.Single(modelState, e => e.Key == "p.Specs"); } + private class House + { + public Garage Garage { get; set; } + } + + private class Garage + { + public List Cars { get; set; } + } + + [Fact] + public async Task BindProperty_FormFileCollectionInCollection_WithPrefix() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor + { + Name = "house", + BindingInfo = new BindingInfo(), + ParameterType = typeof(House) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.QueryString = QueryString.Create("house.Garage.Cars[0].Name", "Accord"); + UpdateRequest(request, data + 1, "house.Garage.Cars[0].Specs"); + AddFormFile(request, data + 2, "house.Garage.Cars[1].Specs"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var house = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(house.Garage); + Assert.NotNull(house.Garage.Cars); + Assert.Collection( + house.Garage.Cars, + car => + { + Assert.Equal("Accord", car.Name); + + var file = Assert.Single(car.Specs); + using var reader = new StreamReader(file.OpenReadStream()); + Assert.Equal(data + 1, reader.ReadToEnd()); + }, + car => + { + Assert.Null(car.Name); + + var file = Assert.Single(car.Specs); + using var reader = new StreamReader(file.OpenReadStream()); + Assert.Equal(data + 2, reader.ReadToEnd()); + }); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Equal(3, modelState.Count); + + var entry = Assert.Single(modelState, e => e.Key == "house.Garage.Cars[0].Name").Value; + Assert.Equal("Accord", entry.AttemptedValue); + Assert.Equal("Accord", entry.RawValue); + + Assert.Single(modelState, e => e.Key == "house.Garage.Cars[0].Specs"); + Assert.Single(modelState, e => e.Key == "house.Garage.Cars[1].Specs"); + } + + [Fact] + public async Task BindProperty_FormFileCollectionInCollection_OnlyFiles() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor + { + Name = "house", + BindingInfo = new BindingInfo(), + ParameterType = typeof(House) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + UpdateRequest(request, data + 1, "house.Garage.Cars[0].Specs"); + AddFormFile(request, data + 2, "house.Garage.Cars[1].Specs"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var house = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(house.Garage); + Assert.NotNull(house.Garage.Cars); + Assert.Collection( + house.Garage.Cars, + car => + { + Assert.Null(car.Name); + + var file = Assert.Single(car.Specs); + using var reader = new StreamReader(file.OpenReadStream()); + Assert.Equal(data + 1, reader.ReadToEnd()); + }, + car => + { + Assert.Null(car.Name); + + var file = Assert.Single(car.Specs); + using var reader = new StreamReader(file.OpenReadStream()); + Assert.Equal(data + 2, reader.ReadToEnd()); + }); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Equal(2, modelState.Count); + + Assert.Single(modelState, e => e.Key == "house.Garage.Cars[0].Specs"); + Assert.Single(modelState, e => e.Key == "house.Garage.Cars[1].Specs"); + } + + [Fact] + public async Task BindProperty_FormFileCollectionInCollection_OutOfOrderFile() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor + { + Name = "house", + BindingInfo = new BindingInfo(), + ParameterType = typeof(House) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + UpdateRequest(request, data + 1, "house.Garage.Cars[800].Specs"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var house = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(house.Garage); + Assert.Empty(house.Garage.Cars); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Empty(modelState); + } + + [Fact] + public async Task BindProperty_FormFileCollectionInCollection_MultipleFiles() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor + { + Name = "house", + BindingInfo = new BindingInfo(), + ParameterType = typeof(House) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + UpdateRequest(request, data + 1, "house.Garage.Cars[0].Specs"); + AddFormFile(request, data + 2, "house.Garage.Cars[0].Specs"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var house = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(house.Garage); + Assert.NotNull(house.Garage.Cars); + Assert.Collection( + house.Garage.Cars, + car => + { + Assert.Null(car.Name); + Assert.Collection( + car.Specs, + file => + { + using var reader = new StreamReader(file.OpenReadStream()); + Assert.Equal(data + 1, reader.ReadToEnd()); + + }, + file => + { + using var reader = new StreamReader(file.OpenReadStream()); + Assert.Equal(data + 2, reader.ReadToEnd()); + + }); + }); + + // ModelState + Assert.True(modelState.IsValid); + var kvp = Assert.Single(modelState); + + Assert.Equal("house.Garage.Cars[0].Specs", kvp.Key); + } + + [Fact] + public async Task BindProperty_FormFile_AsAPropertyOnNestedColection() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor + { + Name = "p", + BindingInfo = new BindingInfo(), + ParameterType = typeof(Car1) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.QueryString = QueryString.Create("p.Name", "Accord"); + UpdateRequest(request, data, "p.Specs"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var car = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(car.Specs); + var file = Assert.Single(car.Specs); + Assert.Equal("form-data; name=p.Specs; filename=text.txt", file.ContentDisposition); + var reader = new StreamReader(file.OpenReadStream()); + Assert.Equal(data, reader.ReadToEnd()); + + // ModelState + Assert.True(modelState.IsValid); + Assert.Equal(2, modelState.Count); + + var entry = Assert.Single(modelState, e => e.Key == "p.Name").Value; + Assert.Equal("Accord", entry.AttemptedValue); + Assert.Equal("Accord", entry.RawValue); + + Assert.Single(modelState, e => e.Key == "p.Specs"); + } + + public class MultiDimensionalFormFileContainer + { + public IFormFile[][] FormFiles { get; set; } + } + + [Fact] + public async Task BindModelAsync_MultiDimensionalFormFile_Works() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor + { + Name = "p", + BindingInfo = new BindingInfo(), + ParameterType = typeof(MultiDimensionalFormFileContainer) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + UpdateRequest(request, data + 1, "FormFiles[0]"); + AddFormFile(request, data + 2, "FormFiles[1]"); + AddFormFile(request, data + 3, "FormFiles[1]"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var container = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(container.FormFiles); + Assert.Collection( + container.FormFiles, + item => + { + Assert.Collection( + item, + file => Assert.Equal(data + 1, ReadFormFile(file))); + }, + item => + { + Assert.Collection( + item, + file => Assert.Equal(data + 2, ReadFormFile(file)), + file => Assert.Equal(data + 3, ReadFormFile(file))); + }); + } + + [Fact] + public async Task BindModelAsync_MultiDimensionalFormFile_WithArrayNotation() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor + { + Name = "p", + BindingInfo = new BindingInfo(), + ParameterType = typeof(MultiDimensionalFormFileContainer) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + UpdateRequest(request, data + 1, "FormFiles[0][0]"); + AddFormFile(request, data + 2, "FormFiles[1][0]"); + AddFormFile(request, data + 3, "FormFiles[1][0]"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + var container = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(container.FormFiles); + Assert.Empty(container.FormFiles); + } + + public class MultiDimensionalFormFileContainerLevel2 + { + public MultiDimensionalFormFileContainerLevel1 Level1 { get; set; } + } + + public class MultiDimensionalFormFileContainerLevel1 + { + public MultiDimensionalFormFileContainer Container { get; set; } + } + + [Fact] + public async Task BindModelAsync_DeeplyNestedMultiDimensionalFormFile_Works() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor + { + Name = "p", + BindingInfo = new BindingInfo(), + ParameterType = typeof(MultiDimensionalFormFileContainerLevel2) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + UpdateRequest(request, data + 1, "p.Level1.Container.FormFiles[0]"); + AddFormFile(request, data + 2, "p.Level1.Container.FormFiles[1]"); + AddFormFile(request, data + 3, "p.Level1.Container.FormFiles[1]"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var level2 = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(level2.Level1); + var container = level2.Level1.Container; + Assert.NotNull(container); + Assert.NotNull(container.FormFiles); + Assert.Collection( + container.FormFiles, + item => + { + Assert.Collection( + item, + file => Assert.Equal(data + 1, ReadFormFile(file))); + }, + item => + { + Assert.Collection( + item, + file => Assert.Equal(data + 2, ReadFormFile(file)), + file => Assert.Equal(data + 3, ReadFormFile(file))); + }); + } + + public class DictionaryContainer + { + public Dictionary Dictionary { get; set; } + } + + [Fact] + public async Task BindModelAsync_DictionaryOfFormFiles() + { + // Arrange + var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); + var parameter = new ParameterDescriptor + { + Name = "p", + BindingInfo = new BindingInfo(), + ParameterType = typeof(DictionaryContainer) + }; + + var data = "Some Data Is Better Than No Data."; + var testContext = ModelBindingTestHelper.GetTestContext( + request => + { + request.QueryString = QueryString.Create(new Dictionary + { + { "p.Dictionary[0].Key", "key0" }, + { "p.Dictionary[1].Key", "key1" }, + { "p.Dictionary[4000].Key", "key1" }, + }); + UpdateRequest(request, data + 1, "p.Dictionary[0].Value"); + AddFormFile(request, data + 2, "p.Dictionary[1].Value"); + }); + + var modelState = testContext.ModelState; + + // Act + var modelBindingResult = await parameterBinder.BindModelAsync(parameter, testContext); + + // Assert + + // ModelBindingResult + Assert.True(modelBindingResult.IsModelSet); + + // Model + var container = Assert.IsType(modelBindingResult.Model); + Assert.NotNull(container.Dictionary); + Assert.Collection( + container.Dictionary.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("key0", kvp.Key); + Assert.Equal(data + 1, ReadFormFile(kvp.Value)); + }, + kvp => + { + Assert.Equal("key1", kvp.Key); + Assert.Equal(data + 2, ReadFormFile(kvp.Value)); + }); + } + + private static string ReadFormFile(IFormFile file) + { + using var reader = new StreamReader(file.OpenReadStream()); + return reader.ReadToEnd(); + } + private void UpdateRequest(HttpRequest request, string data, string name) { - const string fileName = "text.txt"; - var fileCollection = new FormFileCollection(); - var formCollection = new FormCollection(new Dictionary(), fileCollection); - + var formCollection = new FormCollection(new Dictionary(), new FormFileCollection()); request.Form = formCollection; + request.ContentType = "multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq"; + AddFormFile(request, data, name); + } + + private void AddFormFile(HttpRequest request, string data, string name) + { + const string fileName = "text.txt"; + if (string.IsNullOrEmpty(data) || string.IsNullOrEmpty(name)) { // Leave the submission empty. @@ -371,6 +1274,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests request.Headers["Content-Disposition"] = $"form-data; name={name}; filename={fileName}"; + var fileCollection = (FormFileCollection)request.Form.Files; var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(data)); fileCollection.Add(new FormFile(memoryStream, 0, data.Length, name, fileName) { @@ -378,4 +1282,4 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests }); } } -} \ No newline at end of file +} diff --git a/src/Mvc/test/WebSites/BasicWebSite/RazorComponents/RouterContainer.razor b/src/Mvc/test/WebSites/BasicWebSite/RazorComponents/RouterContainer.razor index 20d8ef2991..57e5ce1102 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/RazorComponents/RouterContainer.razor +++ b/src/Mvc/test/WebSites/BasicWebSite/RazorComponents/RouterContainer.razor @@ -1,6 +1,7 @@ @using Microsoft.AspNetCore.Components.Routing Router component +

Route not found

diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs index ff911cc981..66feb4d7cf 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs +++ b/src/Mvc/test/WebSites/BasicWebSite/StartupWithCustomInvalidModelStateFactory.cs @@ -24,15 +24,11 @@ namespace BasicWebSite services.Configure(options => { - var previous = options.InvalidModelStateResponseFactory; options.InvalidModelStateResponseFactory = context => { - var result = (BadRequestObjectResult)previous(context); - if (context.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is VndErrorAttribute)) - { - result.ContentTypes.Clear(); - result.ContentTypes.Add("application/vnd.error+json"); - } + var result = new BadRequestObjectResult(context.ModelState); + result.ContentTypes.Clear(); + result.ContentTypes.Add("application/vnd.error+json"); return result; }; diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs index bf8b004553..67ae87d482 100644 --- a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs +++ b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/JsonOutputFormatterController.cs @@ -20,6 +20,9 @@ namespace FormatterWebSite.Controllers [HttpGet] public ActionResult StringWithUnicodeResult() => "Hello Mr. 🦊"; + [HttpGet] + public ActionResult StringWithNonAsciiContent() => "Une bête de cirque"; + [HttpGet] public ActionResult SimpleModelResult() => new SimpleModel { Id = 10, Name = "Test", StreetName = "Some street" }; diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/App.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/App.razor index 52dc3c98d9..4280307834 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/App.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/App.razor @@ -1,7 +1,14 @@ - - - + + +@*#if (OrganizationalAuth || IndividualAuth) + +#else + +#endif*@ + + +

Sorry, there's nothing at this address.

-
-
-
+ + +
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Areas/Identity/RevalidatingAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Areas/Identity/RevalidatingAuthenticationStateProvider.cs deleted file mode 100644 index 3895c488a9..0000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Areas/Identity/RevalidatingAuthenticationStateProvider.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace BlazorServerWeb_CSharp.Areas.Identity -{ - /// - /// An service that revalidates the - /// authentication state at regular intervals. If a signed-in user's security - /// stamp changes, this revalidation mechanism will sign the user out. - /// - /// The type encapsulating a user. - public class RevalidatingAuthenticationStateProvider - : AuthenticationStateProvider, IDisposable where TUser : class - { - private readonly static TimeSpan RevalidationInterval = TimeSpan.FromMinutes(30); - - private readonly CancellationTokenSource _loopCancellationTokenSource = new CancellationTokenSource(); - private readonly IServiceScopeFactory _scopeFactory; - private readonly ILogger _logger; - private Task _currentAuthenticationStateTask; - - public RevalidatingAuthenticationStateProvider( - IServiceScopeFactory scopeFactory, - SignInManager circuitScopeSignInManager, - ILogger> logger) - { - var initialUser = circuitScopeSignInManager.Context.User; - _currentAuthenticationStateTask = Task.FromResult(new AuthenticationState(initialUser)); - _scopeFactory = scopeFactory; - _logger = logger; - - if (initialUser.Identity.IsAuthenticated) - { - _ = RevalidationLoop(); - } - } - - public override Task GetAuthenticationStateAsync() - => _currentAuthenticationStateTask; - - private async Task RevalidationLoop() - { - var cancellationToken = _loopCancellationTokenSource.Token; - - while (!cancellationToken.IsCancellationRequested) - { - try - { - await Task.Delay(RevalidationInterval, cancellationToken); - } - catch (TaskCanceledException) - { - break; - } - - var isValid = await CheckIfAuthenticationStateIsValidAsync(); - if (!isValid) - { - // Force sign-out. Also stop the revalidation loop, because the user can - // only sign back in by starting a new connection. - var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity()); - _currentAuthenticationStateTask = Task.FromResult(new AuthenticationState(anonymousUser)); - NotifyAuthenticationStateChanged(_currentAuthenticationStateTask); - _loopCancellationTokenSource.Cancel(); - } - } - } - - private async Task CheckIfAuthenticationStateIsValidAsync() - { - try - { - // Get the sign-in manager from a new scope to ensure it fetches fresh data - using (var scope = _scopeFactory.CreateScope()) - { - var signInManager = scope.ServiceProvider.GetRequiredService>(); - var authenticationState = await _currentAuthenticationStateTask; - var validatedUser = await signInManager.ValidateSecurityStampAsync(authenticationState.User); - return validatedUser != null; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred while revalidating authentication state"); - return false; - } - } - - void IDisposable.Dispose() - => _loopCancellationTokenSource.Cancel(); - } -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Areas/Identity/RevalidatingIdentityAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Areas/Identity/RevalidatingIdentityAuthenticationStateProvider.cs new file mode 100644 index 0000000000..dd4fb3e33e --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Areas/Identity/RevalidatingIdentityAuthenticationStateProvider.cs @@ -0,0 +1,74 @@ +using System; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace BlazorServerWeb_CSharp.Areas.Identity +{ + public class RevalidatingIdentityAuthenticationStateProvider + : RevalidatingServerAuthenticationStateProvider where TUser : class + { + private readonly IServiceScopeFactory _scopeFactory; + private readonly IdentityOptions _options; + + public RevalidatingIdentityAuthenticationStateProvider( + ILoggerFactory loggerFactory, + IServiceScopeFactory scopeFactory, + IOptions optionsAccessor) + : base(loggerFactory) + { + _scopeFactory = scopeFactory; + _options = optionsAccessor.Value; + } + + protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); + + protected override async Task ValidateAuthenticationStateAsync( + AuthenticationState authenticationState, CancellationToken cancellationToken) + { + // Get the user manager from a new scope to ensure it fetches fresh data + var scope = _scopeFactory.CreateScope(); + try + { + var userManager = scope.ServiceProvider.GetRequiredService>(); + return await ValidateSecurityStampAsync(userManager, authenticationState.User); + } + finally + { + if (scope is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + scope.Dispose(); + } + } + } + + private async Task ValidateSecurityStampAsync(UserManager userManager, ClaimsPrincipal principal) + { + var user = await userManager.GetUserAsync(principal); + if (user == null) + { + return false; + } + else if (!userManager.SupportsUserSecurityStamp) + { + return true; + } + else + { + var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType); + var userStamp = await userManager.GetSecurityStampAsync(user); + return principalStamp == userStamp; + } + } + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Imports.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Imports.razor deleted file mode 100644 index 0f24edaf1d..0000000000 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@layout MainLayout diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs index 7840c30734..7a6742000f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs @@ -127,7 +127,7 @@ namespace BlazorServerWeb_CSharp services.AddRazorPages(); services.AddServerSideBlazor(); #if (IndividualLocalAuth) - services.AddScoped>(); + services.AddScoped>(); #endif services.AddSingleton(); } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/cs-CZ/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/cs-CZ/strings.json index b87b21d59c..9a99015d9e 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/cs-CZ/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/cs-CZ/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Knihovna tříd Razor", - "description": "Projekt šablony pro vytvoření knihovny tříd Razor" + "description": "Projekt šablony pro vytvoření knihovny tříd Razor", + "parameter.SupportPagesAndViews.name": "_Stránky podpory a zobrazení" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/de-DE/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/de-DE/strings.json index 4ee471d2b7..19ad3ed98c 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/de-DE/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/de-DE/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Razor-Klassenbibliothek", - "description": "Eine Projektvorlage zum Erstellen einer Razor-Klassenbibliothek." + "description": "Eine Projektvorlage zum Erstellen einer Razor-Klassenbibliothek.", + "parameter.SupportPagesAndViews.name": "_Supportseiten und -ansichten" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/en/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/en/strings.json index a95f7f0ae4..cf1f213fd4 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/en/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/en/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Razor Class Library", - "description": "A project template for creating a Razor class library." + "description": "A project template for creating a Razor class library.", + "parameter.SupportPagesAndViews.name": "_Support pages and views" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/es-ES/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/es-ES/strings.json index fd48afa6e8..63645de5ae 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/es-ES/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/es-ES/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Biblioteca de clases de Razor", - "description": "Plantilla de proyecto para crear una biblioteca de clases de Razor." + "description": "Plantilla de proyecto para crear una biblioteca de clases de Razor.", + "parameter.SupportPagesAndViews.name": "_Páginas y vistas de soporte técnico" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/fr-FR/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/fr-FR/strings.json index 66b937b0a1..70b03f2fa8 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/fr-FR/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/fr-FR/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Bibliothèque de classes Razor", - "description": "Modèle de projet pour créer une bibliothèque de classes Razor." + "description": "Modèle de projet pour créer une bibliothèque de classes Razor.", + "parameter.SupportPagesAndViews.name": "_Prendre en charge les pages et les vues" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/it-IT/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/it-IT/strings.json index da09949234..dc75280f9f 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/it-IT/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/it-IT/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Libreria di classi Razor", - "description": "Modello di progetto per la creazione di una libreria di classi Razor." + "description": "Modello di progetto per la creazione di una libreria di classi Razor.", + "parameter.SupportPagesAndViews.name": "_Supporta pagine e visualizzazioni" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ja-JP/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ja-JP/strings.json index 858f722396..c89d555ec7 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ja-JP/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ja-JP/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Razor クラス ライブラリ", - "description": "Razor クラス ライブラリを作成するためのプロジェクト テンプレートです。" + "description": "Razor クラス ライブラリを作成するためのプロジェクト テンプレートです。", + "parameter.SupportPagesAndViews.name": "ページとビューのサポート(_S)" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ko-KR/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ko-KR/strings.json index a0a11cd02d..f5d37428dd 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ko-KR/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ko-KR/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Razor 클래스 라이브러리", - "description": "Razor 클래스 라이브러리를 만들기 위한 프로젝트 템플릿입니다." + "description": "Razor 클래스 라이브러리를 만들기 위한 프로젝트 템플릿입니다.", + "parameter.SupportPagesAndViews.name": "지원 페이지 및 보기(_S)" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/pl-PL/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/pl-PL/strings.json index da8e0fe500..a0fd9fd297 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/pl-PL/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/pl-PL/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Biblioteka klas Razor", - "description": "Szablon projektu do tworzenia biblioteki klas Razor." + "description": "Szablon projektu do tworzenia biblioteki klas Razor.", + "parameter.SupportPagesAndViews.name": "_Obsługa stron i widoków" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/pt-BR/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/pt-BR/strings.json index 90f9fc4b8c..7183895cf2 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/pt-BR/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/pt-BR/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Biblioteca de Classes Razor", - "description": "Um modelo de projeto para criar uma biblioteca de classes Razor." + "description": "Um modelo de projeto para criar uma biblioteca de classes Razor.", + "parameter.SupportPagesAndViews.name": "Páginas e exibições do _suporte" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ru-RU/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ru-RU/strings.json index b025f1e0ec..efe043fde6 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ru-RU/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/ru-RU/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Библиотека классов Razor", - "description": "Шаблон проекта для создания библиотеки классов Razor." + "description": "Шаблон проекта для создания библиотеки классов Razor.", + "parameter.SupportPagesAndViews.name": "_Представления и страницы поддержки" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/tr-TR/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/tr-TR/strings.json index aef6961ad5..4ef1c24edc 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/tr-TR/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/tr-TR/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Razor Sınıf Kitaplığı", - "description": "Razor sınıf kütüphanesi yaratma projesi şablonu." + "description": "Razor sınıf kütüphanesi yaratma projesi şablonu.", + "parameter.SupportPagesAndViews.name": "_Destek sayfaları ve görünümler" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/zh-CN/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/zh-CN/strings.json index 1c511b7f65..961e5ca496 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/zh-CN/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/zh-CN/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Razor 类库", - "description": "用于创建 Razor 类库的项目模板。" + "description": "用于创建 Razor 类库的项目模板。", + "parameter.SupportPagesAndViews.name": "支持页面和视图(_S)" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/zh-TW/strings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/zh-TW/strings.json index 5abee44995..ed86c524aa 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/zh-TW/strings.json +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorClassLibrary-CSharp/.template.config/zh-TW/strings.json @@ -2,6 +2,7 @@ "version": "1.0.0.0", "strings": { "name": "Razor 類別庫", - "description": "用於建立 Razor 類別庫的專案範本。" + "description": "用於建立 Razor 類別庫的專案範本。", + "parameter.SupportPagesAndViews.name": "_Support 頁面與檢視" } } \ No newline at end of file diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.html b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.html index d37b212c4c..89b9c80ff8 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.html +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/counter/counter.component.html @@ -2,6 +2,6 @@

This is a simple example of an Angular component.

-

Current count: {{ currentCount }}

+

Current count: {{ currentCount }}

diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.html b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.html index cd33b58a5c..19b3835dbf 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.html +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/ClientApp/src/app/fetch-data/fetch-data.component.html @@ -1,10 +1,10 @@ -

Weather forecast

+

Weather forecast

This component demonstrates fetching data from the server.

Loading...

- +
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Startup.cs b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Startup.cs index d6e4a775cc..053345327c 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Startup.cs +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/Angular-CSharp/Startup.cs @@ -88,7 +88,10 @@ namespace Company.WebApplication1 #endif app.UseStaticFiles(); - app.UseSpaStaticFiles(); + if (!env.IsDevelopment()) + { + app.UseSpaStaticFiles(); + } app.UseRouting(); diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Counter.js b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Counter.js index 734887d18f..9a4ec746ba 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Counter.js +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/Counter.js @@ -3,26 +3,26 @@ import React, { Component } from 'react'; export class Counter extends Component { static displayName = Counter.name; - constructor (props) { + constructor(props) { super(props); this.state = { currentCount: 0 }; this.incrementCounter = this.incrementCounter.bind(this); } - incrementCounter () { + incrementCounter() { this.setState({ currentCount: this.state.currentCount + 1 }); } - render () { + render() { return (

Counter

This is a simple example of a React component.

-

Current count: {this.state.currentCount}

+

Current count: {this.state.currentCount}

diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/FetchData.js b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/FetchData.js index 4782540e3b..d1a9f910d0 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/FetchData.js +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/ClientApp/src/components/FetchData.js @@ -17,7 +17,7 @@ export class FetchData extends Component { static renderForecastsTable(forecasts) { return ( -
Date
+
@@ -47,7 +47,7 @@ export class FetchData extends Component { return (
-

Weather forecast

+

Weather forecast

This component demonstrates fetching data from the server.

{contents}
diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Counter.tsx b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Counter.tsx index a31720c44b..82fde952e6 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Counter.tsx +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/Counter.tsx @@ -17,11 +17,11 @@ class Counter extends React.PureComponent {

This is a simple example of a React component.

-

Current count: {this.props.count}

+

Current count: {this.props.count}

diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/FetchData.tsx b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/FetchData.tsx index c177f0bc67..9fed830288 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/FetchData.tsx +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/ClientApp/src/components/FetchData.tsx @@ -7,78 +7,78 @@ import * as WeatherForecastsStore from '../store/WeatherForecasts'; // At runtime, Redux will merge together... type WeatherForecastProps = - WeatherForecastsStore.WeatherForecastsState // ... state we've requested from the Redux store - & typeof WeatherForecastsStore.actionCreators // ... plus action creators we've requested - & RouteComponentProps<{ startDateIndex: string }>; // ... plus incoming routing parameters + WeatherForecastsStore.WeatherForecastsState // ... state we've requested from the Redux store + & typeof WeatherForecastsStore.actionCreators // ... plus action creators we've requested + & RouteComponentProps<{ startDateIndex: string }>; // ... plus incoming routing parameters class FetchData extends React.PureComponent { - // This method is called when the component is first added to the document - public componentDidMount() { - this.ensureDataFetched(); - } + // This method is called when the component is first added to the document + public componentDidMount() { + this.ensureDataFetched(); + } - // This method is called when the route parameters change - public componentDidUpdate() { - this.ensureDataFetched(); - } + // This method is called when the route parameters change + public componentDidUpdate() { + this.ensureDataFetched(); + } - public render() { - return ( - -

Weather forecast

-

This component demonstrates fetching data from the server and working with URL parameters.

- { this.renderForecastsTable() } - { this.renderPagination() } -
- ); - } + public render() { + return ( + +

Weather forecast

+

This component demonstrates fetching data from the server and working with URL parameters.

+ {this.renderForecastsTable()} + {this.renderPagination()} +
+ ); + } - private ensureDataFetched() { - const startDateIndex = parseInt(this.props.match.params.startDateIndex, 10) || 0; - this.props.requestWeatherForecasts(startDateIndex); - } + private ensureDataFetched() { + const startDateIndex = parseInt(this.props.match.params.startDateIndex, 10) || 0; + this.props.requestWeatherForecasts(startDateIndex); + } - private renderForecastsTable() { - return ( -
Date
- - - - - - - - - - {this.props.forecasts.map((forecast: WeatherForecastsStore.WeatherForecast) => - - - - - - - )} - -
DateTemp. (C)Temp. (F)Summary
{forecast.date}{forecast.temperatureC}{forecast.temperatureF}{forecast.summary}
- ); - } + private renderForecastsTable() { + return ( + + + + + + + + + + + {this.props.forecasts.map((forecast: WeatherForecastsStore.WeatherForecast) => + + + + + + + )} + +
DateTemp. (C)Temp. (F)Summary
{forecast.date}{forecast.temperatureC}{forecast.temperatureF}{forecast.summary}
+ ); + } - private renderPagination() { - const prevStartDateIndex = (this.props.startDateIndex || 0) - 5; - const nextStartDateIndex = (this.props.startDateIndex || 0) + 5; + private renderPagination() { + const prevStartDateIndex = (this.props.startDateIndex || 0) - 5; + const nextStartDateIndex = (this.props.startDateIndex || 0) + 5; - return ( -
- Previous - {this.props.isLoading && Loading...} - Next -
- ); - } + return ( +
+ Previous + {this.props.isLoading && Loading...} + Next +
+ ); + } } export default connect( - (state: ApplicationState) => state.weatherForecasts, // Selects which state properties are merged into the component's props - WeatherForecastsStore.actionCreators // Selects which action creators are merged into the component's props + (state: ApplicationState) => state.weatherForecasts, // Selects which state properties are merged into the component's props + WeatherForecastsStore.actionCreators // Selects which action creators are merged into the component's props )(FetchData as any); diff --git a/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets b/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets index 5b419146af..df2b4e5e1b 100644 --- a/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets +++ b/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets @@ -6,7 +6,7 @@ Condition="$(DesignTimeBuild) != true"> - RestoreSources=$([MSBuild]::Escape("$(RestoreSources);$(ArtifactsShippingPackagesDir);$(ArtifactsNonShippingPackagesDir)")); + RestoreAdditionalProjectSources=$([MSBuild]::Escape("$(RestoreAdditionalProjectSources);$(ArtifactsShippingPackagesDir);$(ArtifactsNonShippingPackagesDir)")); MicrosoftNetCompilersToolsetPackageVersion=$(MicrosoftNetCompilersToolsetPackageVersion); MicrosoftNETCoreAppRuntimeVersion=$(MicrosoftNETCoreAppRuntimeVersion); MicrosoftNETCoreAppRefPackageVersion=$(MicrosoftNETCoreAppRefPackageVersion); diff --git a/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in b/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in index 6fab74db7b..b10d74b52b 100644 --- a/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in +++ b/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in @@ -1,7 +1,8 @@ true - ${RestoreSources} + ${RestoreAdditionalProjectSources} + $(MSBuildThisFileDirectory)runtimeconfig.norollforward.json @@ -46,5 +47,4 @@ -->
- diff --git a/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs b/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs index f85b5c20b3..75a3e0fe10 100644 --- a/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs +++ b/src/ProjectTemplates/test/SpaTemplateTest/SpaTemplateTestBase.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -44,7 +45,7 @@ namespace Templates.Test.SpaTemplateTest { Project = await ProjectFactory.GetOrCreateProject(key, Output); - var createResult = await Project.RunDotNetNewAsync(template, auth: usesAuth ? "Individual" : null, language: null, useLocalDb); + using var createResult = await Project.RunDotNetNewAsync(template, auth: usesAuth ? "Individual" : null, language: null, useLocalDb); Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult)); // We shouldn't have to do the NPM restore in tests because it should happen @@ -60,23 +61,23 @@ namespace Templates.Test.SpaTemplateTest Assert.Contains(".db", projectFileContents); } - var npmRestoreResult = await Project.RestoreWithRetryAsync(Output, clientAppSubdirPath); + using var npmRestoreResult = await Project.RestoreWithRetryAsync(Output, clientAppSubdirPath); Assert.True(0 == npmRestoreResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm restore", Project, npmRestoreResult)); - var lintResult = await ProcessEx.RunViaShellAsync(Output, clientAppSubdirPath, "npm run lint"); + using var lintResult = await ProcessEx.RunViaShellAsync(Output, clientAppSubdirPath, "npm run lint"); Assert.True(0 == lintResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm run lint", Project, lintResult)); var testResult = await ProcessEx.RunViaShellAsync(Output, clientAppSubdirPath, "npm run test"); Assert.True(0 == testResult.ExitCode, ErrorMessages.GetFailedProcessMessage("npm run test", Project, testResult)); - var publishResult = await Project.RunDotNetPublishAsync(); + using var publishResult = await Project.RunDotNetPublishAsync(); Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult)); // Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release // The output from publish will go into bin/Release/netcoreapp3.0/publish and won't be affected by calling build // later, while the opposite is not true. - var buildResult = await Project.RunDotNetBuildAsync(); + using var buildResult = await Project.RunDotNetBuildAsync(); Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, buildResult)); // localdb is not installed on the CI machines, so skip it. @@ -84,13 +85,13 @@ namespace Templates.Test.SpaTemplateTest if (usesAuth) { - var migrationsResult = await Project.RunDotNetEfCreateMigrationAsync(template); + using var migrationsResult = await Project.RunDotNetEfCreateMigrationAsync(template); Assert.True(0 == migrationsResult.ExitCode, ErrorMessages.GetFailedProcessMessage("run EF migrations", Project, migrationsResult)); Project.AssertEmptyMigration(template); if (shouldVisitFetchData) { - var dbUpdateResult = await Project.RunDotNetEfUpdateDatabaseAsync(); + using var dbUpdateResult = await Project.RunDotNetEfUpdateDatabaseAsync(); Assert.True(0 == dbUpdateResult.ExitCode, ErrorMessages.GetFailedProcessMessage("update database", Project, dbUpdateResult)); } } @@ -172,6 +173,9 @@ namespace Templates.Test.SpaTemplateTest catch (OperationCanceledException) { } + catch (HttpRequestException ex) when (ex.Message.StartsWith("The SSL connection could not be established")) + { + } await Task.Delay(TimeSpan.FromSeconds(5 * attempt)); } while (attempt < maxAttempts); } @@ -240,7 +244,7 @@ namespace Templates.Test.SpaTemplateTest browser.Equal("Weather forecast", () => browser.FindElement(By.TagName("h1")).Text); // Asynchronously loads and displays the table of weather forecasts - browser.Exists(By.CssSelector("table>tbody>tr")); + browser.Exists(By.CssSelector("table>tbody>tr"), TimeSpan.FromSeconds(10)); browser.Equal(5, () => browser.FindElements(By.CssSelector("p+table>tbody>tr")).Count); } diff --git a/src/ProjectTemplates/test/template-baselines.json b/src/ProjectTemplates/test/template-baselines.json index 086b4af195..48b625bc37 100644 --- a/src/ProjectTemplates/test/template-baselines.json +++ b/src/ProjectTemplates/test/template-baselines.json @@ -905,7 +905,7 @@ "_Imports.razor", "Areas/Identity/Pages/Account/LogOut.cshtml", "Areas/Identity/Pages/Shared/_LoginPartial.cshtml", - "Areas/Identity/RevalidatingAuthenticationStateProvider.cs", + "Areas/Identity/RevalidatingIdentityAuthenticationStateProvider.cs", "Data/ApplicationDbContext.cs", "Data/WeatherForecast.cs", "Data/WeatherForecastService.cs", @@ -917,7 +917,6 @@ "Pages/FetchData.razor", "Pages/Index.razor", "Pages/_Host.cshtml", - "Pages/_Imports.razor", "Properties/launchSettings.json", "Shared/LoginDisplay.razor", "Shared/MainLayout.razor", @@ -954,7 +953,6 @@ "Pages/FetchData.razor", "Pages/Index.razor", "Pages/_Host.cshtml", - "Pages/_Imports.razor", "Properties/launchSettings.json", "Shared/LoginDisplay.razor", "Shared/MainLayout.razor", @@ -991,7 +989,6 @@ "Pages/FetchData.razor", "Pages/Index.razor", "Pages/_Host.cshtml", - "Pages/_Imports.razor", "Properties/launchSettings.json", "Shared/LoginDisplay.razor", "Shared/MainLayout.razor", @@ -1028,7 +1025,6 @@ "Pages/FetchData.razor", "Pages/Index.razor", "Pages/_Host.cshtml", - "Pages/_Imports.razor", "Properties/launchSettings.json", "Shared/MainLayout.razor", "Shared/NavMenu.razor", @@ -1064,7 +1060,6 @@ "Pages/FetchData.razor", "Pages/Index.razor", "Pages/_Host.cshtml", - "Pages/_Imports.razor", "Properties/launchSettings.json", "Shared/LoginDisplay.razor", "Shared/MainLayout.razor", @@ -1101,7 +1096,6 @@ "Pages/FetchData.razor", "Pages/Index.razor", "Pages/_Host.cshtml", - "Pages/_Imports.razor", "Properties/launchSettings.json", "Shared/LoginDisplay.razor", "Shared/MainLayout.razor", diff --git a/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs index 1643abe614..2e62846f8f 100644 --- a/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs +++ b/src/Security/Authentication/Negotiate/samples/NegotiateAuthSample/Startup.cs @@ -20,7 +20,17 @@ namespace NegotiateAuthSample options.FallbackPolicy = options.DefaultPolicy; }); services.AddAuthentication(NegotiateDefaults.AuthenticationScheme) - .AddNegotiate(); + .AddNegotiate(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + // context.SkipHandler(); + return Task.CompletedTask; + } + }; + }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/src/Security/Authentication/Negotiate/src/Internal/INegotiateState.cs b/src/Security/Authentication/Negotiate/src/Internal/INegotiateState.cs index dbc215ef7d..d2eb518c5b 100644 --- a/src/Security/Authentication/Negotiate/src/Internal/INegotiateState.cs +++ b/src/Security/Authentication/Negotiate/src/Internal/INegotiateState.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate // For testing internal interface INegotiateState : IDisposable { - string GetOutgoingBlob(string incomingBlob); + string GetOutgoingBlob(string incomingBlob, out BlobErrorType status, out Exception error); bool IsCompleted { get; } @@ -17,4 +17,12 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate IIdentity GetIdentity(); } + + internal enum BlobErrorType + { + None, + CredentialError, + ClientError, + Other + } } diff --git a/src/Security/Authentication/Negotiate/src/Internal/NegotiateLoggingExtensions.cs b/src/Security/Authentication/Negotiate/src/Internal/NegotiateLoggingExtensions.cs index 11141bb123..b39ec8cf7b 100644 --- a/src/Security/Authentication/Negotiate/src/Internal/NegotiateLoggingExtensions.cs +++ b/src/Security/Authentication/Negotiate/src/Internal/NegotiateLoggingExtensions.cs @@ -12,6 +12,8 @@ namespace Microsoft.Extensions.Logging private static Action _enablingCredentialPersistence; private static Action _disablingCredentialPersistence; private static Action _exceptionProcessingAuth; + private static Action _credentialError; + private static Action _clientError; private static Action _challengeNegotiate; private static Action _reauthenticating; private static Action _deferring; @@ -50,6 +52,14 @@ namespace Microsoft.Extensions.Logging eventId: new EventId(8, "Deferring"), logLevel: LogLevel.Information, formatString: "Deferring to the server's implementation of Windows Authentication."); + _credentialError = LoggerMessage.Define( + eventId: new EventId(9, "CredentialError"), + logLevel: LogLevel.Debug, + formatString: "There was a problem with the users credentials."); + _clientError = LoggerMessage.Define( + eventId: new EventId(10, "ClientError"), + logLevel: LogLevel.Debug, + formatString: "The users authentication request was invalid."); } public static void IncompleteNegotiateChallenge(this ILogger logger) @@ -75,5 +85,11 @@ namespace Microsoft.Extensions.Logging public static void Deferring(this ILogger logger) => _deferring(logger, null); + + public static void CredentialError(this ILogger logger, Exception ex) + => _credentialError(logger, ex); + + public static void ClientError(this ILogger logger, Exception ex) + => _clientError(logger, ex); } } diff --git a/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateState.cs b/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateState.cs index 37a1dff6f0..fb7a6a3a9f 100644 --- a/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateState.cs +++ b/src/Security/Authentication/Negotiate/src/Internal/ReflectedNegotiateState.cs @@ -5,6 +5,8 @@ using System; using System.Linq; using System.Net; using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; using System.Security.Authentication; using System.Security.Principal; @@ -12,21 +14,30 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate { internal class ReflectedNegotiateState : INegotiateState { + // https://www.gnu.org/software/gss/reference/gss.pdf + private const uint GSS_S_NO_CRED = 7 << 16; + private static readonly ConstructorInfo _constructor; private static readonly MethodInfo _getOutgoingBlob; private static readonly MethodInfo _isCompleted; private static readonly MethodInfo _protocol; private static readonly MethodInfo _getIdentity; private static readonly MethodInfo _closeContext; + private static readonly FieldInfo _statusCode; + private static readonly FieldInfo _statusException; + private static readonly MethodInfo _getException; + private static readonly FieldInfo _gssMinorStatus; + private static readonly Type _gssExceptionType; private readonly object _instance; static ReflectedNegotiateState() { - var ntAuthType = typeof(AuthenticationException).Assembly.GetType("System.Net.NTAuthentication"); + var secAssembly = typeof(AuthenticationException).Assembly; + var ntAuthType = secAssembly.GetType("System.Net.NTAuthentication", throwOnError: true); _constructor = ntAuthType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).First(); _getOutgoingBlob = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => - info.Name.Equals("GetOutgoingBlob") && info.GetParameters().Count() == 2).Single(); + info.Name.Equals("GetOutgoingBlob") && info.GetParameters().Count() == 3).Single(); _isCompleted = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => info.Name.Equals("get_IsCompleted")).Single(); _protocol = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => @@ -34,9 +45,23 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate _closeContext = ntAuthType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(info => info.Name.Equals("CloseContext")).Single(); - var negoStreamPalType = typeof(AuthenticationException).Assembly.GetType("System.Net.Security.NegotiateStreamPal"); + var securityStatusType = secAssembly.GetType("System.Net.SecurityStatusPal", throwOnError: true); + _statusCode = securityStatusType.GetField("ErrorCode"); + _statusException = securityStatusType.GetField("Exception"); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var interopType = secAssembly.GetType("Interop", throwOnError: true); + var netNativeType = interopType.GetNestedType("NetSecurityNative", BindingFlags.NonPublic | BindingFlags.Static); + _gssExceptionType = netNativeType.GetNestedType("GssApiException", BindingFlags.NonPublic); + _gssMinorStatus = _gssExceptionType.GetField("_minorStatus", BindingFlags.Instance | BindingFlags.NonPublic); + } + + var negoStreamPalType = secAssembly.GetType("System.Net.Security.NegotiateStreamPal", throwOnError: true); _getIdentity = negoStreamPalType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(info => info.Name.Equals("GetIdentity")).Single(); + _getException = negoStreamPalType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(info => + info.Name.Equals("CreateExceptionFromError")).Single(); } public ReflectedNegotiateState() @@ -50,14 +75,15 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate // The client doesn't need the context once auth is complete, but the server does. // I'm not sure why it auto-closes for the client given that the client closes it just a few lines later. // https://github.com/dotnet/corefx/blob/a3ab91e10045bb298f48c1d1f9bd5b0782a8ac46/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs#L134 - public string GetOutgoingBlob(string incomingBlob) + public string GetOutgoingBlob(string incomingBlob, out BlobErrorType status, out Exception error) { byte[] decodedIncomingBlob = null; if (incomingBlob != null && incomingBlob.Length > 0) { decodedIncomingBlob = Convert.FromBase64String(incomingBlob); } - byte[] decodedOutgoingBlob = GetOutgoingBlob(decodedIncomingBlob, true); + + byte[] decodedOutgoingBlob = GetOutgoingBlob(decodedIncomingBlob, out status, out error); string outgoingBlob = null; if (decodedOutgoingBlob != null && decodedOutgoingBlob.Length > 0) @@ -68,9 +94,65 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate return outgoingBlob; } - private byte[] GetOutgoingBlob(byte[] incomingBlob, bool thrownOnError) + private byte[] GetOutgoingBlob(byte[] incomingBlob, out BlobErrorType status, out Exception error) { - return (byte[])_getOutgoingBlob.Invoke(_instance, new object[] { incomingBlob, thrownOnError }); + try + { + // byte[] GetOutgoingBlob(byte[] incomingBlob, bool throwOnError, out SecurityStatusPal statusCode) + var parameters = new object[] { incomingBlob, false, null }; + var blob = (byte[])_getOutgoingBlob.Invoke(_instance, parameters); + + var securityStatus = parameters[2]; + // TODO: Update after corefx changes + error = (Exception)(_statusException.GetValue(securityStatus) + ?? _getException.Invoke(null, new[] { securityStatus })); + var errorCode = (SecurityStatusPalErrorCode)_statusCode.GetValue(securityStatus); + + // TODO: Remove after corefx changes + // The linux implementation always uses InternalError; + if (errorCode == SecurityStatusPalErrorCode.InternalError + && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + && _gssExceptionType.IsInstanceOfType(error)) + { + var majorStatus = (uint)error.HResult; + var minorStatus = (uint)_gssMinorStatus.GetValue(error); + + // Remap specific errors + if (majorStatus == GSS_S_NO_CRED && minorStatus == 0) + { + errorCode = SecurityStatusPalErrorCode.UnknownCredentials; + } + + error = new Exception($"An authentication exception occured (0x{majorStatus:X}/0x{minorStatus:X}).", error); + } + + if (errorCode == SecurityStatusPalErrorCode.OK + || errorCode == SecurityStatusPalErrorCode.ContinueNeeded + || errorCode == SecurityStatusPalErrorCode.CompleteNeeded) + { + status = BlobErrorType.None; + } + else if (IsCredentialError(errorCode)) + { + status = BlobErrorType.CredentialError; + } + else if (IsClientError(errorCode)) + { + status = BlobErrorType.ClientError; + } + else + { + status = BlobErrorType.Other; + } + + return blob; + } + catch (TargetInvocationException tex) + { + // Unwrap + ExceptionDispatchInfo.Capture(tex.InnerException).Throw(); + throw; + } } public bool IsCompleted @@ -92,5 +174,36 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate { _closeContext.Invoke(_instance, Array.Empty()); } + + private bool IsCredentialError(SecurityStatusPalErrorCode error) + { + return error == SecurityStatusPalErrorCode.LogonDenied || + error == SecurityStatusPalErrorCode.UnknownCredentials || + error == SecurityStatusPalErrorCode.NoImpersonation || + error == SecurityStatusPalErrorCode.NoAuthenticatingAuthority || + error == SecurityStatusPalErrorCode.UntrustedRoot || + error == SecurityStatusPalErrorCode.CertExpired || + error == SecurityStatusPalErrorCode.SmartcardLogonRequired || + error == SecurityStatusPalErrorCode.BadBinding; + } + + private bool IsClientError(SecurityStatusPalErrorCode error) + { + return error == SecurityStatusPalErrorCode.InvalidToken || + error == SecurityStatusPalErrorCode.CannotPack || + error == SecurityStatusPalErrorCode.QopNotSupported || + error == SecurityStatusPalErrorCode.NoCredentials || + error == SecurityStatusPalErrorCode.MessageAltered || + error == SecurityStatusPalErrorCode.OutOfSequence || + error == SecurityStatusPalErrorCode.IncompleteMessage || + error == SecurityStatusPalErrorCode.IncompleteCredentials || + error == SecurityStatusPalErrorCode.WrongPrincipal || + error == SecurityStatusPalErrorCode.TimeSkew || + error == SecurityStatusPalErrorCode.IllegalMessage || + error == SecurityStatusPalErrorCode.CertUnknown || + error == SecurityStatusPalErrorCode.AlgorithmMismatch || + error == SecurityStatusPalErrorCode.SecurityQosFailed || + error == SecurityStatusPalErrorCode.UnsupportedPreauth; + } } } diff --git a/src/Security/Authentication/Negotiate/src/Internal/SecurityStatusPalErrorCode.cs b/src/Security/Authentication/Negotiate/src/Internal/SecurityStatusPalErrorCode.cs new file mode 100644 index 0000000000..f89a996e7e --- /dev/null +++ b/src/Security/Authentication/Negotiate/src/Internal/SecurityStatusPalErrorCode.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.Negotiate +{ + internal enum SecurityStatusPalErrorCode + { + NotSet = 0, + OK, + ContinueNeeded, + CompleteNeeded, + CompAndContinue, + ContextExpired, + CredentialsNeeded, + Renegotiate, + + // Errors + OutOfMemory, + InvalidHandle, + Unsupported, + TargetUnknown, + InternalError, + PackageNotFound, + NotOwner, + CannotInstall, + InvalidToken, + CannotPack, + QopNotSupported, + NoImpersonation, + LogonDenied, + UnknownCredentials, + NoCredentials, + MessageAltered, + OutOfSequence, + NoAuthenticatingAuthority, + IncompleteMessage, + IncompleteCredentials, + BufferNotEnough, + WrongPrincipal, + TimeSkew, + UntrustedRoot, + IllegalMessage, + CertUnknown, + CertExpired, + DecryptFailure, + AlgorithmMismatch, + SecurityQosFailed, + SmartcardLogonRequired, + UnsupportedPreauth, + BadBinding, + DowngradeDetected, + ApplicationProtocolMismatch + } +} diff --git a/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs b/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs index e49cd5cd97..c880fb1564 100644 --- a/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs +++ b/src/Security/Authentication/Negotiate/src/NegotiateHandler.cs @@ -65,6 +65,8 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate /// True if a response was generated, false otherwise. public async Task HandleRequestAsync() { + AuthPersistence persistence = null; + bool authFailedEventCalled = false; try { if (_requestProcessed || Options.DeferToServer) @@ -86,7 +88,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate } var connectionItems = GetConnectionItems(); - var persistence = (AuthPersistence)connectionItems[AuthPersistenceKey]; + persistence = (AuthPersistence)connectionItems[AuthPersistenceKey]; _negotiateState = persistence?.State; var authorizationHeader = Request.Headers[HeaderNames.Authorization]; @@ -126,7 +128,40 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate _negotiateState ??= Options.StateFactory.CreateInstance(); - var outgoing = _negotiateState.GetOutgoingBlob(token); + var outgoing = _negotiateState.GetOutgoingBlob(token, out var errorType, out var exception); + Logger.LogInformation(errorType.ToString()); + if (errorType != BlobErrorType.None) + { + _negotiateState.Dispose(); + _negotiateState = null; + if (persistence?.State != null) + { + persistence.State.Dispose(); + persistence.State = null; + } + + if (errorType == BlobErrorType.CredentialError) + { + Logger.CredentialError(exception); + authFailedEventCalled = true; // Could throw, and we don't want to double trigger the event. + var result = await InvokeAuthenticateFailedEvent(exception); + return result ?? false; // Default to skipping the handler, let AuthZ generate a new 401 + } + else if (errorType == BlobErrorType.ClientError) + { + Logger.ClientError(exception); + authFailedEventCalled = true; // Could throw, and we don't want to double trigger the event. + var result = await InvokeAuthenticateFailedEvent(exception); + if (result.HasValue) + { + return result.Value; + } + Context.Response.StatusCode = StatusCodes.Status400BadRequest; + return true; // Default to terminating request + } + + throw exception; + } if (!_negotiateState.IsCompleted) { @@ -193,24 +228,26 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate } catch (Exception ex) { - Logger.ExceptionProcessingAuth(ex); - var errorContext = new AuthenticationFailedContext(Context, Scheme, Options) { Exception = ex }; - await Events.AuthenticationFailed(errorContext); - - if (errorContext.Result != null) + if (authFailedEventCalled) { - if (errorContext.Result.Handled) - { - return true; - } - else if (errorContext.Result.Skipped) - { - return false; - } - else if (errorContext.Result.Failure != null) - { - throw new Exception("An error was returned from the AuthenticationFailed event.", errorContext.Result.Failure); - } + throw; + } + + Logger.ExceptionProcessingAuth(ex); + + // Clear state so it's possible to retry on the same connection. + _negotiateState?.Dispose(); + _negotiateState = null; + if (persistence?.State != null) + { + persistence.State.Dispose(); + persistence.State = null; + } + + var result = await InvokeAuthenticateFailedEvent(ex); + if (result.HasValue) + { + return result.Value; } throw; @@ -219,6 +256,30 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate return false; } + private async Task InvokeAuthenticateFailedEvent(Exception ex) + { + var errorContext = new AuthenticationFailedContext(Context, Scheme, Options) { Exception = ex }; + await Events.AuthenticationFailed(errorContext); + + if (errorContext.Result != null) + { + if (errorContext.Result.Handled) + { + return true; + } + else if (errorContext.Result.Skipped) + { + return false; + } + else if (errorContext.Result.Failure != null) + { + throw new Exception("An error was returned from the AuthenticationFailed event.", errorContext.Result.Failure); + } + } + + return null; + } + /// /// Checks if the current request is authenticated and returns the user. /// diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs b/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs index dc76aacb6e..6c3baef320 100644 --- a/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs +++ b/src/Security/Authentication/Negotiate/test/Negotiate.Test/EventTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Reflection.Metadata; using System.Security.Principal; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -71,16 +72,16 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate } [Fact] - public async Task OnAuthenticationFailed_Fires() + public async Task OnAuthenticationFailed_FromException_Fires() { - var eventInvoked = false; + var eventInvoked = 0; using var host = await CreateHostAsync(options => { options.Events = new NegotiateEvents() { OnAuthenticationFailed = context => { - eventInvoked = true; + eventInvoked++; Assert.IsType(context.Exception); Assert.Equal("InvalidBlob", context.Exception.Message); return Task.CompletedTask; @@ -92,11 +93,11 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate var ex = await Assert.ThrowsAsync(() => SendAsync(server, "/404", new TestConnection(), "Negotiate InvalidBlob")); Assert.Equal("InvalidBlob", ex.Message); - Assert.True(eventInvoked); + Assert.Equal(1, eventInvoked); } [Fact] - public async Task OnAuthenticationFailed_Handled() + public async Task OnAuthenticationFailed_FromException_Handled() { using var host = await CreateHostAsync(options => { @@ -104,7 +105,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate { OnAuthenticationFailed = context => { - context.Response.StatusCode = StatusCodes.Status418ImATeapot; ; + context.Response.StatusCode = StatusCodes.Status418ImATeapot; context.Response.Headers[HeaderNames.WWWAuthenticate] = "Teapot"; context.HandleResponse(); return Task.CompletedTask; @@ -118,6 +119,157 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate Assert.Equal("Teapot", result.Response.Headers[HeaderNames.WWWAuthenticate]); } + [Fact] + public async Task OnAuthenticationFailed_FromOtherBlobError_Fires() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + Assert.IsType(context.Exception); + Assert.Equal("A test other error occured", context.Exception.Message); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var ex = await Assert.ThrowsAsync(() => + SendAsync(server, "/404", new TestConnection(), "Negotiate OtherError")); + Assert.Equal("A test other error occured", ex.Message); + Assert.Equal(1, eventInvoked); + } + + [Fact] + public async Task OnAuthenticationFailed_FromOtherBlobError_Handled() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + context.Response.StatusCode = StatusCodes.Status418ImATeapot; + context.Response.Headers[HeaderNames.WWWAuthenticate] = "Teapot"; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var result = await SendAsync(server, "/404", new TestConnection(), "Negotiate OtherError"); + Assert.Equal(StatusCodes.Status418ImATeapot, result.Response.StatusCode); + Assert.Equal("Teapot", result.Response.Headers[HeaderNames.WWWAuthenticate]); + Assert.Equal(1, eventInvoked); + } + + [Fact] + public async Task OnAuthenticationFailed_FromCredentialError_Fires() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + Assert.IsType(context.Exception); + Assert.Equal("A test credential error occured", context.Exception.Message); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var response = await SendAsync(server, "/418", new TestConnection(), "Negotiate CredentialError"); + Assert.Equal(StatusCodes.Status418ImATeapot, response.Response.StatusCode); + Assert.Equal(1, eventInvoked); + } + + [Fact] + public async Task OnAuthenticationFailed_FromCredentialError_Handled() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + context.Response.StatusCode = StatusCodes.Status418ImATeapot; + context.Response.Headers[HeaderNames.WWWAuthenticate] = "Teapot"; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var result = await SendAsync(server, "/404", new TestConnection(), "Negotiate CredentialError"); + Assert.Equal(StatusCodes.Status418ImATeapot, result.Response.StatusCode); + Assert.Equal("Teapot", result.Response.Headers[HeaderNames.WWWAuthenticate]); + Assert.Equal(1, eventInvoked); + } + + [Fact] + public async Task OnAuthenticationFailed_FromClientError_Fires() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + Assert.IsType(context.Exception); + Assert.Equal("A test client error occured", context.Exception.Message); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var response = await SendAsync(server, "/404", new TestConnection(), "Negotiate ClientError"); + Assert.Equal(StatusCodes.Status400BadRequest, response.Response.StatusCode); + Assert.Equal(1, eventInvoked); + } + + [Fact] + public async Task OnAuthenticationFailed_FromClientError_Handled() + { + var eventInvoked = 0; + using var host = await CreateHostAsync(options => + { + options.Events = new NegotiateEvents() + { + OnAuthenticationFailed = context => + { + eventInvoked++; + context.Response.StatusCode = StatusCodes.Status418ImATeapot; + context.Response.Headers[HeaderNames.WWWAuthenticate] = "Teapot"; + context.HandleResponse(); + return Task.CompletedTask; + } + }; + }); + var server = host.GetTestServer(); + + var result = await SendAsync(server, "/404", new TestConnection(), "Negotiate ClientError"); + Assert.Equal(StatusCodes.Status418ImATeapot, result.Response.StatusCode); + Assert.Equal("Teapot", result.Response.Headers[HeaderNames.WWWAuthenticate]); + Assert.Equal(1, eventInvoked); + } + [Fact] public async Task OnAuthenticated_FiresOncePerRequest() { @@ -278,6 +430,12 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate Assert.False(string.IsNullOrEmpty(name), "name"); await context.Response.WriteAsync(name); }); + + builder.Map("/418", context => + { + context.Response.StatusCode = StatusCodes.Status418ImATeapot; + return Task.CompletedTask; + }); } private static Task SendAsync(TestServer server, string path, TestConnection connection, string authorizationHeader = null) @@ -352,7 +510,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate return new GenericIdentity("name", _protocol); } - public string GetOutgoingBlob(string incomingBlob) + public string GetOutgoingBlob(string incomingBlob, out BlobErrorType errorType, out Exception ex) { if (IsDisposed) { @@ -362,6 +520,10 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate { throw new InvalidOperationException("Authentication is already complete."); } + + errorType = BlobErrorType.None; + ex = null; + switch (incomingBlob) { case "ClientNtlmBlob1": @@ -391,8 +553,22 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate Assert.Equal("Kerberos", _protocol); IsCompleted = true; return "ServerKerberosBlob2"; + case "CredentialError": + errorType = BlobErrorType.CredentialError; + ex = new Exception("A test credential error occured"); + return null; + case "ClientError": + errorType = BlobErrorType.ClientError; + ex = new Exception("A test client error occured"); + return null; + case "OtherError": + errorType = BlobErrorType.Other; + ex = new Exception("A test other error occured"); + return null; default: - throw new InvalidOperationException(incomingBlob); + errorType = BlobErrorType.Other; + ex = new InvalidOperationException(incomingBlob); + return null; } } } diff --git a/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs b/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs index b57a8e996d..d696cd0afd 100644 --- a/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs +++ b/src/Security/Authentication/Negotiate/test/Negotiate.Test/NegotiateHandlerTests.cs @@ -271,6 +271,39 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate Assert.False(result.Response.Headers.ContainsKey(HeaderNames.WWWAuthenticate)); } + [Fact] + public async Task CredentialError_401() + { + using var host = await CreateHostAsync(); + var server = host.GetTestServer(); + var testConnection = new TestConnection(); + var result = await SendAsync(server, "/Authenticate", testConnection, "Negotiate CredentialError"); + Assert.Equal(StatusCodes.Status401Unauthorized, result.Response.StatusCode); + Assert.Equal("Negotiate", result.Response.Headers[HeaderNames.WWWAuthenticate]); + } + + [Fact] + public async Task ClientError_400() + { + using var host = await CreateHostAsync(); + var server = host.GetTestServer(); + var testConnection = new TestConnection(); + var result = await SendAsync(server, "/404", testConnection, "Negotiate ClientError"); + Assert.Equal(StatusCodes.Status400BadRequest, result.Response.StatusCode); + Assert.DoesNotContain(HeaderNames.WWWAuthenticate, result.Response.Headers); + } + + [Fact] + public async Task OtherError_Throws() + { + using var host = await CreateHostAsync(); + var server = host.GetTestServer(); + var testConnection = new TestConnection(); + + var ex = await Assert.ThrowsAsync(() => SendAsync(server, "/404", testConnection, "Negotiate OtherError")); + Assert.Equal("A test other error occured", ex.Message); + } + // Single Stage private static async Task KerberosAuth(TestServer server, TestConnection testConnection) { @@ -474,7 +507,7 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate return new GenericIdentity("name", _protocol); } - public string GetOutgoingBlob(string incomingBlob) + public string GetOutgoingBlob(string incomingBlob, out BlobErrorType errorType, out Exception ex) { if (IsDisposed) { @@ -484,6 +517,10 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate { throw new InvalidOperationException("Authentication is already complete."); } + + errorType = BlobErrorType.None; + ex = null; + switch (incomingBlob) { case "ClientNtlmBlob1": @@ -513,8 +550,22 @@ namespace Microsoft.AspNetCore.Authentication.Negotiate Assert.Equal("Kerberos", _protocol); IsCompleted = true; return "ServerKerberosBlob2"; + case "CredentialError": + errorType = BlobErrorType.CredentialError; + ex = new Exception("A test credential error occured"); + return null; + case "ClientError": + errorType = BlobErrorType.ClientError; + ex = new Exception("A test client error occured"); + return null; + case "OtherError": + errorType = BlobErrorType.Other; + ex = new Exception("A test other error occured"); + return null; default: - throw new InvalidOperationException(incomingBlob); + errorType = BlobErrorType.Other; + ex = new InvalidOperationException(incomingBlob); + return null; } } } diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.cpp index b05a4d1a50..545791610b 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.cpp @@ -30,16 +30,17 @@ void HostFxrErrorRedirector::HostFxrErrorRedirectorCallback(const WCHAR* message m_writeFunction->Append(std::wstring(message) + L"\r\n"); } -void HostFxr::Load() -{ - HMODULE hModule; - THROW_LAST_ERROR_IF(!GetModuleHandleEx(0, L"hostfxr.dll", &hModule)); - Load(hModule); -} - void HostFxr::Load(HMODULE moduleHandle) { + // A hostfxr may already be loaded here if we tried to start with an + // invalid configuration. Release hostfxr before loading it again. + if (m_hHostFxrDll != nullptr) + { + m_hHostFxrDll.release(); + } + m_hHostFxrDll = moduleHandle; + try { m_hostfxr_get_native_search_directories_fn = ModuleHelpers::GetKnownProcAddress(moduleHandle, "hostfxr_get_native_search_directories"); @@ -63,9 +64,13 @@ void HostFxr::Load(HMODULE moduleHandle) void HostFxr::Load(const std::wstring& location) { + // Make sure to always load hostfxr via an absolute path. + // If the process fails to start for whatever reason, a mismatched hostfxr + // may be already loaded in the process. try { HMODULE hModule; + LOG_INFOF(L"Loading hostfxr from location %s", location.c_str()); THROW_LAST_ERROR_IF_NULL(hModule = LoadLibraryW(location.c_str())); Load(hModule); } diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h index 526222322e..ced4dd1940 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h @@ -60,7 +60,6 @@ public: { } - void Load(); void Load(HMODULE moduleHandle); void Load(const std::wstring& location); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp index 5707e4cd77..b671e7dba2 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp @@ -210,7 +210,7 @@ IN_PROCESS_APPLICATION::ExecuteApplication() hostFxrResolutionResult->GetArguments(context->m_argc, context->m_argv); THROW_IF_FAILED(SetEnvironmentVariablesOnWorkerProcess()); - context->m_hostFxr.Load(); + context->m_hostFxr.Load(hostFxrResolutionResult->GetHostFxrLocation()); } else { diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/MaxRequestBodySizeTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/MaxRequestBodySizeTests.cs index 3f03d99318..8f5a80a569 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/MaxRequestBodySizeTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/MaxRequestBodySizeTests.cs @@ -78,6 +78,10 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess public async Task SetIISLimitMaxRequestBodyLogsWarning() { var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + + // Logs get tangled up due to ANCM debug logs and managed logs logging at the same time. + // Disable it for this test as we are trying to verify a log. + deploymentParameters.HandlerSettings["debugLevel"] = ""; deploymentParameters.ServerConfigActionList.Add( (config, _) => { config diff --git a/src/Servers/IIS/IntegrationTesting.IIS/src/IISExpressDeployer.cs b/src/Servers/IIS/IntegrationTesting.IIS/src/IISExpressDeployer.cs index b08ba18738..acac067a25 100644 --- a/src/Servers/IIS/IntegrationTesting.IIS/src/IISExpressDeployer.cs +++ b/src/Servers/IIS/IntegrationTesting.IIS/src/IISExpressDeployer.cs @@ -94,10 +94,9 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS RunWebConfigActions(contentRoot); - var testUri = TestUriHelper.BuildTestUri(ServerType.IISExpress, DeploymentParameters.ApplicationBaseUriHint); // Launch the host process. - var (actualUri, hostExitToken) = await StartIISExpressAsync(testUri, contentRoot); + var (actualUri, hostExitToken) = await StartIISExpressAsync(contentRoot); Logger.LogInformation("Application ready at URL: {appUrl}", actualUri); @@ -152,27 +151,28 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS return dllRoot; } - private async Task<(Uri url, CancellationToken hostExitToken)> StartIISExpressAsync(Uri uri, string contentRoot) + private async Task<(Uri url, CancellationToken hostExitToken)> StartIISExpressAsync(string contentRoot) { using (Logger.BeginScope("StartIISExpress")) { - var port = uri.Port; - if (port == 0) - { - port = (uri.Scheme == "https") ? TestPortHelper.GetNextSSLPort() : TestPortHelper.GetNextPort(); - } - - Logger.LogInformation("Attempting to start IIS Express on port: {port}", port); - PrepareConfig(contentRoot, port); - - var parameters = string.IsNullOrEmpty(DeploymentParameters.ServerConfigLocation) ? - string.Format("/port:{0} /path:\"{1}\" /trace:error /systray:false", uri.Port, contentRoot) : - string.Format("/site:{0} /config:{1} /trace:error /systray:false", DeploymentParameters.SiteName, DeploymentParameters.ServerConfigLocation); - var iisExpressPath = GetIISExpressPath(); for (var attempt = 0; attempt < MaximumAttempts; attempt++) { + var uri = TestUriHelper.BuildTestUri(ServerType.IISExpress, DeploymentParameters.ApplicationBaseUriHint); + var port = uri.Port; + if (port == 0) + { + port = (uri.Scheme == "https") ? TestPortHelper.GetNextSSLPort() : TestPortHelper.GetNextPort(); + } + + Logger.LogInformation("Attempting to start IIS Express on port: {port}", port); + PrepareConfig(contentRoot, port); + + var parameters = string.IsNullOrEmpty(DeploymentParameters.ServerConfigLocation) ? + string.Format("/port:{0} /path:\"{1}\" /trace:error /systray:false", uri.Port, contentRoot) : + string.Format("/site:{0} /config:{1} /trace:error /systray:false", DeploymentParameters.SiteName, DeploymentParameters.ServerConfigLocation); + Logger.LogInformation("Executing command : {iisExpress} {parameters}", iisExpressPath, parameters); var startInfo = new ProcessStartInfo @@ -264,8 +264,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS Logger.LogInformation("Started iisexpress successfully. Process Id : {processId}, Port: {port}", _hostProcess.Id, port); return (url: url, hostExitToken: hostExitTokenSource.Token); } - - ChangePort(contentRoot, (uri.Scheme == "https") ? TestPortHelper.GetNextSSLPort() : TestPortHelper.GetNextPort()); } var message = $"Failed to initialize IIS Express after {MaximumAttempts} attempts to select a port"; @@ -315,14 +313,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS File.WriteAllText(DeploymentParameters.ServerConfigLocation, serverConfig); } - private void ChangePort(string contentRoot, int port) - { - var serverConfig = File.ReadAllText(DeploymentParameters.ServerConfigLocation); - XDocument config = XDocument.Parse(serverConfig); - ConfigureModuleAndBinding(config.Root, contentRoot, port); - File.WriteAllText(DeploymentParameters.ServerConfigLocation, serverConfig); - } - private void AddAspNetCoreElement(XElement config) { var aspNetCore = config diff --git a/src/Servers/IIS/build/assets.props b/src/Servers/IIS/build/assets.props index e6f289883b..e97ecec6a9 100644 --- a/src/Servers/IIS/build/assets.props +++ b/src/Servers/IIS/build/assets.props @@ -4,8 +4,8 @@ true true false - x64 - $(Platform) + x64 + $(TargetArchitecture) Win32 $(NativePlatform) diff --git a/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs b/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs index 4aff039e72..02ff378b17 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/UnixDomainSocketsTests.cs @@ -84,20 +84,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var host = hostBuilder.Build()) { - await host.StartAsync(); + await host.StartAsync().DefaultTimeout(); using (var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified)) { - await socket.ConnectAsync(new UnixDomainSocketEndPoint(path)); + await socket.ConnectAsync(new UnixDomainSocketEndPoint(path)).DefaultTimeout(); var data = Encoding.ASCII.GetBytes("Hello World"); - await socket.SendAsync(data, SocketFlags.None); + await socket.SendAsync(data, SocketFlags.None).DefaultTimeout(); var buffer = new byte[data.Length]; var read = 0; while (read < data.Length) { - read += await socket.ReceiveAsync(buffer.AsMemory(read, buffer.Length - read), SocketFlags.None); + read += await socket.ReceiveAsync(buffer.AsMemory(read, buffer.Length - read), SocketFlags.None).DefaultTimeout(); } Assert.Equal(data, buffer); @@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // Wait for the server to complete the loop because of the FIN await serverConnectionCompletedTcs.Task.DefaultTimeout(); - await host.StopAsync(); + await host.StopAsync().DefaultTimeout(); } } finally diff --git a/src/Shared/E2ETesting/E2ETesting.targets b/src/Shared/E2ETesting/E2ETesting.targets index fb3c80a3a3..48d8dad7a6 100644 --- a/src/Shared/E2ETesting/E2ETesting.targets +++ b/src/Shared/E2ETesting/E2ETesting.targets @@ -11,7 +11,7 @@ Importance="High" Text="Prerequisites were not enforced at build time. Running Yarn or the E2E tests might fail as a result. Check /src/Shared/E2ETesting/Readme.md for instructions." /> - + WaitAssertCore(driver, () => Assert.Single(actualValues())); public static void Exists(this IWebDriver driver, By finder) - => WaitAssertCore(driver, () => Assert.NotEmpty(driver.FindElements(finder))); + => Exists(driver, finder, default); + + public static void Exists(this IWebDriver driver, By finder, TimeSpan timeout) + => WaitAssertCore(driver, () => Assert.NotEmpty(driver.FindElements(finder)), timeout); private static void WaitAssertCore(IWebDriver driver, Action assertion, TimeSpan timeout = default) { diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs index 142063ab05..1c2280e42d 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.ConnectionLifecycle.cs @@ -427,6 +427,89 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests } } + [Fact] + public async Task CancellationTokenFromStartPassedToTransport() + { + using (StartVerifiableLog()) + { + var cts = new CancellationTokenSource(); + var httpHandler = new TestHttpMessageHandler(); + + await WithConnectionAsync( + CreateConnection(httpHandler, + transport: new TestTransport(onTransportStart: () => { + // Cancel the token when the transport is starting which will fail the startTask. + cts.Cancel(); + return Task.CompletedTask; + })), + async (connection) => + { + // We aggregate failures that happen when we start the transport. The operation cancelled exception will + // be an inner exception. + var ex = await Assert.ThrowsAsync(async () => await connection.StartAsync(cts.Token)).OrTimeout(); + Assert.Equal(3, ex.InnerExceptions.Count); + var innerEx = ex.InnerExceptions[2]; + var innerInnerEx = innerEx.InnerException; + Assert.IsType(innerInnerEx); + }); + } + } + + [Fact] + public async Task SSECanBeCanceled() + { + bool ExpectedErrors(WriteContext writeContext) + { + return writeContext.LoggerName == typeof(HttpConnection).FullName && + writeContext.EventId.Name == "ErrorStartingTransport"; + } + + using (StartVerifiableLog(expectedErrorsFilter: ExpectedErrors)) + { + var httpHandler = new TestHttpMessageHandler(); + httpHandler.OnGet("/?id=00000000-0000-0000-0000-000000000000", (_, __) => + { + // Simulating a cancellationToken canceling this request. + throw new OperationCanceledException("Cancel SSE Start."); + }); + + var sse = new ServerSentEventsTransport(new HttpClient(httpHandler), LoggerFactory); + + await WithConnectionAsync( + CreateConnection(httpHandler, loggerFactory: LoggerFactory, transport: sse, transportType: HttpTransportType.ServerSentEvents), + async (connection) => + { + var ex = await Assert.ThrowsAsync(async () => await connection.StartAsync()).OrTimeout(); + }); + } + } + + [Fact] + public async Task LongPollingTransportCanBeCanceled() + { + using (StartVerifiableLog()) + { + var cts = new CancellationTokenSource(); + + var httpHandler = new TestHttpMessageHandler(autoNegotiate: false); + httpHandler.OnNegotiate((request, cancellationToken) => + { + // Cancel token so that the first request poll will throw + cts.Cancel(); + return ResponseUtils.CreateResponse(HttpStatusCode.OK, ResponseUtils.CreateNegotiationContent()); + }); + + var lp = new LongPollingTransport(new HttpClient(httpHandler)); + + await WithConnectionAsync( + CreateConnection(httpHandler, transport: lp, transportType: HttpTransportType.LongPolling), + async (connection) => + { + var ex = await Assert.ThrowsAsync(async () => await connection.StartAsync(cts.Token).OrTimeout()); + }); + } + } + private static async Task AssertDisposedAsync(HttpConnection connection) { var exception = diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/TestHttpMessageHandler.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/TestHttpMessageHandler.cs index 75472a4970..06d05da7f5 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/TestHttpMessageHandler.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/TestHttpMessageHandler.cs @@ -46,6 +46,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var firstPoll = true; OnRequest(async (request, next, cancellationToken) => { + cancellationToken.ThrowIfCancellationRequested(); if (ResponseUtils.IsLongPollRequest(request) && firstPoll) { firstPoll = false; @@ -156,6 +157,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests { OnRequest((request, next, cancellationToken) => { + cancellationToken.ThrowIfCancellationRequested(); if (request.Method.Equals(method) && string.Equals(request.RequestUri.PathAndQuery, pathAndQuery)) { return handler(request, cancellationToken); diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransport.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransport.cs index ae1249dba7..35847771ae 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransport.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/TestTransport.cs @@ -44,6 +44,9 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests Application = pair.Application; await _startHandler(); + // To test canceling the token from the onTransportStart Func. + cancellationToken.ThrowIfCancellationRequested(); + // Start a loop to read from the pipe Receiving = ReceiveLoop(); async Task ReceiveLoop() diff --git a/src/SignalR/clients/ts/FunctionalTests/SignalR.Npm.FunctionalTests.npmproj b/src/SignalR/clients/ts/FunctionalTests/SignalR.Npm.FunctionalTests.npmproj index f75f288f9a..f80aec5ee8 100644 --- a/src/SignalR/clients/ts/FunctionalTests/SignalR.Npm.FunctionalTests.npmproj +++ b/src/SignalR/clients/ts/FunctionalTests/SignalR.Npm.FunctionalTests.npmproj @@ -8,6 +8,7 @@ <_TestSauceArgs>--verbose --no-color --configuration $(Configuration) --sauce-user "$(SauceUser)" --sauce-key "$(SauceKey)" <_TestSauceArgs Condition="'$(BrowserTestHostName)' != ''">$(_TestSauceArgs) --use-hostname "$(BrowserTestHostName)" run test:inner --no-color --configuration $(Configuration) + run build:inner diff --git a/src/SignalR/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj b/src/SignalR/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj deleted file mode 100644 index 4bcf55a1a2..0000000000 --- a/src/SignalR/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - netcoreapp3.0 - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Directory.Build.props b/src/SignalR/perf/benchmarkapps/BenchmarkServer/Directory.Build.props deleted file mode 100644 index 8c119d5413..0000000000 --- a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Directory.Build.props +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Directory.Build.targets b/src/SignalR/perf/benchmarkapps/BenchmarkServer/Directory.Build.targets deleted file mode 100644 index 2e3fb2fa71..0000000000 --- a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Directory.Build.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Hubs/EchoHub.cs b/src/SignalR/perf/benchmarkapps/BenchmarkServer/Hubs/EchoHub.cs deleted file mode 100644 index f6336fab84..0000000000 --- a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Hubs/EchoHub.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.SignalR; - -namespace BenchmarkServer.Hubs -{ - public class EchoHub : Hub - { - public async Task Broadcast(int duration) - { - var sent = 0; - try - { - var t = new CancellationTokenSource(); - t.CancelAfter(TimeSpan.FromSeconds(duration)); - while (!t.IsCancellationRequested && !Context.ConnectionAborted.IsCancellationRequested) - { - await Clients.All.SendAsync("send", DateTime.UtcNow); - sent++; - } - } - catch (Exception e) - { - Console.WriteLine(e); - } - Console.WriteLine("Broadcast exited: Sent {0} messages", sent); - } - - public DateTime Echo(DateTime time) - { - return time; - } - - public Task EchoAll(DateTime time) - { - return Clients.All.SendAsync("send", time); - } - - public void SendPayload(string payload) - { - // Dump the payload, we don't care - } - - public DateTime GetCurrentTime() - { - return DateTime.UtcNow; - } - } -} diff --git a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Program.cs b/src/SignalR/perf/benchmarkapps/BenchmarkServer/Program.cs deleted file mode 100644 index 0232643b58..0000000000 --- a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Program.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Diagnostics; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace BenchmarkServer -{ - public class Program - { - public static void Main(string[] args) - { - Console.WriteLine($"Process ID: {Process.GetCurrentProcess().Id}"); - - var config = new ConfigurationBuilder() - .AddEnvironmentVariables(prefix: "ASPNETCORE_") - .AddCommandLine(args) - .Build(); - - var host = new WebHostBuilder() - .UseConfiguration(config) - .ConfigureLogging(loggerFactory => - { - if (Enum.TryParse(config["LogLevel"], out LogLevel logLevel)) - { - loggerFactory.AddConsole().SetMinimumLevel(logLevel); - } - }) - .UseKestrel() - .UseStartup(); - - host.Build().Run(); - } - } -} diff --git a/src/SignalR/perf/benchmarkapps/BenchmarkServer/README.md b/src/SignalR/perf/benchmarkapps/BenchmarkServer/README.md deleted file mode 100644 index e4f8ceb7a3..0000000000 --- a/src/SignalR/perf/benchmarkapps/BenchmarkServer/README.md +++ /dev/null @@ -1,17 +0,0 @@ -## Purpose - -This project is to assist in Benchmarking SignalR. -It makes it easier to test local changes than having the App in the Benchmarks repo by letting us make changes in signalr branches and using the example commandline below to run the benchmarks against our branches. - -The SignalRWorker that runs against this server is located at https://github.com/aspnet/benchmarks/blob/master/src/BenchmarksClient/Workers/SignalRWorker.cs. - -## Usage - -1. Push changes you would like to test to a branch on GitHub -2. Clone aspnet/benchmarks repo to your machine or install the global BenchmarksDriver tool https://www.nuget.org/packages/BenchmarksDriver/ -3. If cloned go to the BenchmarksDriver project -4. Use the following command as a guideline for running a test using your changes - -`benchmarks --server --client -p TransportType=WebSockets -p HubProtocol=messagepack -j https://raw.githubusercontent.com/aspnet/SignalR/dev/benchmarks/BenchmarkServer/signalr.json` - -5. For more info/commands see https://github.com/aspnet/benchmarks/blob/master/src/BenchmarksDriver/README.md diff --git a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Startup.cs b/src/SignalR/perf/benchmarkapps/BenchmarkServer/Startup.cs deleted file mode 100644 index 139ec09d47..0000000000 --- a/src/SignalR/perf/benchmarkapps/BenchmarkServer/Startup.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using BenchmarkServer.Hubs; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace BenchmarkServer -{ - public class Startup - { - private readonly IConfiguration _config; - public Startup(IConfiguration configuration) - { - _config = configuration; - } - - public void ConfigureServices(IServiceCollection services) - { - var signalrBuilder = services.AddSignalR(o => - { - o.EnableDetailedErrors = true; - }) - // TODO: Json vs NewtonsoftJson option - .AddMessagePackProtocol(); - - var redisConnectionString = _config["SignalRRedis"]; - if (!string.IsNullOrEmpty(redisConnectionString)) - { - signalrBuilder.AddStackExchangeRedis(redisConnectionString); - } - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - app.UseSignalR(routes => - { - routes.MapHub("/echo", o => - { - // Remove backpressure for benchmarking - o.TransportMaxBufferSize = 0; - o.ApplicationMaxBufferSize = 0; - }); - }); - } - } -} diff --git a/src/SignalR/perf/benchmarkapps/BenchmarkServer/signalr.json b/src/SignalR/perf/benchmarkapps/BenchmarkServer/signalr.json deleted file mode 100644 index 9fc37041b3..0000000000 --- a/src/SignalR/perf/benchmarkapps/BenchmarkServer/signalr.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "Default": { - "Client": "SignalR", - "Source": { - "Repository": "https://github.com/aspnet/aspnetcore.git", - "BranchOrCommit": "master", - "Project": "src/SignalR/perf/benchmarkapps/BenchmarkServer/BenchmarkServer.csproj" - }, - "Connections": 10, - "Duration": 20, - "Warmup": 2, - "Path": "/echo", - "ClientProperties": { - "CollectLatency": "true" - } - }, - "SignalRBroadcast": { - "ClientProperties": { "Scenario": "broadcast" } - }, - "SignalREcho": { - "ClientProperties": { "Scenario": "echo" } - }, - "SignalREchoAll": { - "ClientProperties": { "Scenario": "echoAll" }, - "Warmup": 0 - }, - "SignalREchoIdle": { - "ClientProperties": { - "Scenario": "echoIdle", - "CollectLatency": "false" - } - } -} diff --git a/src/SignalR/perf/benchmarkapps/NuGet.Config b/src/SignalR/perf/benchmarkapps/NuGet.Config deleted file mode 100644 index 298193b812..0000000000 --- a/src/SignalR/perf/benchmarkapps/NuGet.Config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs index 89c7f77498..d8c1292d5a 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/Hubs.cs @@ -91,6 +91,16 @@ namespace Microsoft.AspNetCore.SignalR.Tests return 43; } + public ValueTask ValueTaskMethod() + { + return new ValueTask(Task.CompletedTask); + } + + public ValueTask ValueTaskValueMethod() + { + return new ValueTask(43); + } + [HubMethodName("RenamedMethod")] public int ATestMethodThatIsRenamedByTheAttribute() { @@ -1050,4 +1060,32 @@ namespace Microsoft.AspNetCore.SignalR.Tests public bool TokenStateInDisconnected { get; set; } } + + public class CallerServiceHub : Hub + { + private readonly CallerService _service; + + public CallerServiceHub(CallerService service) + { + _service = service; + } + + public override Task OnConnectedAsync() + { + _service.SetCaller(Clients.Caller); + var tcs = (TaskCompletionSource)Context.Items["ConnectedTask"]; + tcs?.TrySetResult(true); + return base.OnConnectedAsync(); + } + } + + public class CallerService + { + public IClientProxy Caller { get; private set; } + + public void SetCaller(IClientProxy caller) + { + Caller = caller; + } + } } diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs index f2eb574c9d..389e760f6c 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs @@ -814,6 +814,57 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Fact] + public async Task HubMethodCanReturnValueFromValueTask() + { + using (StartVerifiableLog()) + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(null, LoggerFactory); + + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler); + + var result = (await client.InvokeAsync(nameof(MethodHub.ValueTaskValueMethod)).OrTimeout()).Result; + + // json serializer makes this a long + Assert.Equal(43L, result); + + // kill the connection + client.Dispose(); + + await connectionHandlerTask.OrTimeout(); + } + } + } + + [Fact] + public async Task HubMethodCanReturnValueTask() + { + using (StartVerifiableLog()) + { + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(null, LoggerFactory); + + var connectionHandler = serviceProvider.GetService>(); + + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler); + + var result = (await client.InvokeAsync(nameof(MethodHub.ValueTaskMethod)).OrTimeout()).Result; + + Assert.Null(result); + + // kill the connection + client.Dispose(); + + await connectionHandlerTask.OrTimeout(); + } + } + } + [Theory] [MemberData(nameof(HubTypes))] public async Task HubMethodsAreCaseInsensitive(Type hubType) @@ -3625,6 +3676,35 @@ namespace Microsoft.AspNetCore.SignalR.Tests } } + [Fact] + public async Task ClientsCallerPropertyCanBeUsedOutsideOfHub() + { + CallerService callerService = new CallerService(); + var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => + { + services.AddSingleton(callerService); + }); + var connectionHandler = serviceProvider.GetService>(); + + using (StartVerifiableLog()) + { + using (var client = new TestClient()) + { + var connectionHandlerTask = await client.ConnectAsync(connectionHandler); + + // Wait for a connection, or for the endpoint to fail. + await client.Connected.OrThrowIfOtherFails(connectionHandlerTask).OrTimeout(); + + await callerService.Caller.SendAsync("Echo", "message").OrTimeout(); + + var message = Assert.IsType(await client.ReadAsync().OrTimeout()); + + Assert.Equal("Echo", message.Target); + Assert.Equal("message", message.Arguments[0]); + } + } + } + private class CustomHubActivator : IHubActivator where THub : Hub { public int ReleaseCount; diff --git a/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj b/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj index 347eca2062..7b880e889b 100644 --- a/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj +++ b/src/SiteExtensions/LoggingAggregate/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj @@ -14,11 +14,10 @@ content true true - - $(RestoreSources); - $(ArtifactsNonShippingPackagesDir) - true + + + $(RestoreAdditionalProjectSources);$(ArtifactsNonShippingPackagesDir) @@ -39,15 +38,13 @@ - - - - + - + + + - diff --git a/src/SiteExtensions/LoggingBranch/LB.csproj b/src/SiteExtensions/LoggingBranch/LB.csproj index a2de284cf1..ec6512acc9 100644 --- a/src/SiteExtensions/LoggingBranch/LB.csproj +++ b/src/SiteExtensions/LoggingBranch/LB.csproj @@ -31,4 +31,4 @@ - \ No newline at end of file + diff --git a/src/SiteExtensions/Sdk/SiteExtension.targets b/src/SiteExtensions/Sdk/SiteExtension.targets index 677e122221..320ea9fb3d 100644 --- a/src/SiteExtensions/Sdk/SiteExtension.targets +++ b/src/SiteExtensions/Sdk/SiteExtension.targets @@ -15,10 +15,11 @@ <_RuntimeStoreManifestFile>$(_DepsOutputDirectory)\rs.csproj <_RuntimeStoreOutput>$(_DepsOutputDirectory)\rs\ <_RsRestoreSources> - $(RestoreSources); + $(RestoreAdditionalProjectSources); $(ArtifactsShippingPackagesDir); $(ArtifactsNonShippingPackagesDir) + @@ -66,7 +67,7 @@ ComposeDir=$(_RuntimeStoreOutput)\%(HostingStartupRuntimeStoreTargets.Runtime); SkipOptimization=true; DisablePackageReferenceRestrictions=true; - RestoreSources=$(_RsRestoreSources)" /> + RestoreAdditionalProjectSources=$(_RsRestoreSources)" /> @@ -82,7 +83,7 @@ MicrosoftAspNetCoreAppPackageVersion=$(MicrosoftAspNetCoreAppPackageVersion); UseAppHost=false; NoBuild=false; - RestoreSources=$(_RsRestoreSources)" /> + RestoreAdditionalProjectSources=$(_RsRestoreSources)" /> "); - - var restoreSources = GetMetadata("TestSettings:RestoreSources"); - - var dbTargets = new XDocument( - new XElement("Project", - new XElement("PropertyGroup", - new XElement("RestoreSources", restoreSources)))); - dbTargets.Save(Path.Combine(WorkFolder, "Directory.Build.targets")); + File.WriteAllText(Path.Combine(WorkFolder, "Directory.Build.targets"), ""); } private string GetMetadata(string key) diff --git a/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj b/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj index 49aebd1482..913332b68d 100644 --- a/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj +++ b/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj @@ -17,13 +17,6 @@ - - - <_Parameter1>TestSettings:RestoreSources - <_Parameter2>$(RestoreSources) - - -