diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7594b269a8..9fba304d8e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,6 +3,7 @@ /build/ @natemcmaster /eng/ @natemcmaster +/src/Components/ @SteveSandersonMS /src/DefaultBuilder/ @tratcher /src/Hosting/ @tratcher /src/Http/ @tratcher diff --git a/build.ps1 b/build.ps1 index fc4830731a..2d08643422 100644 --- a/build.ps1 +++ b/build.ps1 @@ -102,6 +102,11 @@ param( [Parameter(ParameterSetName = 'Groups')] [switch]$Installers, + # By default, Windows builds will use MSBuild.exe. Passing this will force the build to run on + # dotnet.exe instead, which may cause issues if you invoke build on a project unsupported by + # MSBuild for .NET Core + [switch]$ForceCoreMsbuild, + # Other lifecycle targets [switch]$Help, # Show help @@ -269,6 +274,9 @@ Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') try { Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $RepoRoot -ConfigFile $ConfigFile -CI:$CI + if ($ForceCoreMsbuild) { + $global:KoreBuildSettings.MSBuildType = 'core' + } Invoke-KoreBuildCommand 'default-build' @MSBuildArguments } finally { diff --git a/build/Publish.targets b/build/Publish.targets index 54d9b39641..fa3e8c9d77 100644 --- a/build/Publish.targets +++ b/build/Publish.targets @@ -1,130 +1,13 @@ - - - GetFilesToPublish; - PublishToAzureFeed; - PublishToMyGet; - - + + - - - - - - <_FilesToPublish Include="$(InstallersOutputPath)*.txt"> - text/plain - - - <_FilesToPublish Include="$(InstallersOutputPath)*.version"> - text/plain - no-cache, no-store, must-revalidate - - - <_FilesToPublish Include="$(InstallersOutputPath)*.svg"> - no-cache, no-store, must-revalidate - image/svg+xml - - - - <_FilesToPublish Include="$(InstallersOutputPath)*" Exclude="@(_FilesToPublish)" /> - - - <_FilesToPublish Include="$(ProductPackageOutputPath)*.jar;$(ProductPackageOutputPath)*.pom"> - aspnetcore/jar/$(PackageVersion)/ - - - - - %(_FilesToPublish.BlobBasePath)%(_FilesToPublish.FileName)%(_FilesToPublish.Extension) - aspnetcore/Runtime/$(PackageVersion)/%(_FilesToPublish.FileName)%(_FilesToPublish.Extension) - - <_FilesToPublish Remove="@(_FilesToPublish)" /> - - - - - - - - - - - - NonShipping=true - - - NonShipping=true - - - - - - - - - - - - - - - - - - - $(PublishMyGetNpmRegistryUrl.Replace("https:", "")):_authToken - - - - - - - - - - - - - - - - - - - + diff --git a/build/SharedFrameworkOnly.props b/build/SharedFrameworkOnly.props index 4c3969a169..73b030b5f7 100644 --- a/build/SharedFrameworkOnly.props +++ b/build/SharedFrameworkOnly.props @@ -11,6 +11,10 @@ + + + + diff --git a/build/external-dependencies.props b/build/external-dependencies.props index 494ba861db..e9b08f2897 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -28,6 +28,7 @@ + diff --git a/build/publish/Publish.csproj b/build/publish/Publish.csproj new file mode 100644 index 0000000000..a136fe7910 --- /dev/null +++ b/build/publish/Publish.csproj @@ -0,0 +1,182 @@ + + + + + + + netcoreapp3.0 + true + $(ArtifactsDir)manifests\ + https://maestro-prod.westus2.cloudapp.azure.com + + + + + + + + + + + + GetFilesToPublish; + GenerateBuildAssetManifest; + PublishToAzureFeed; + PublishToMyGet; + PublishToBuildAssetRegistry; + + + + + + + + + <_FilesToPublish Include="$(InstallersOutputPath)*.txt"> + text/plain + + + <_FilesToPublish Include="$(InstallersOutputPath)*.version"> + text/plain + no-cache, no-store, must-revalidate + + + <_FilesToPublish Include="$(InstallersOutputPath)*.svg"> + no-cache, no-store, must-revalidate + image/svg+xml + + + + <_FilesToPublish Include="$(InstallersOutputPath)*" Exclude="@(_FilesToPublish)" /> + + + <_FilesToPublish Include="$(ProductPackageOutputPath)*.jar;$(ProductPackageOutputPath)*.pom"> + aspnetcore/jar/$(PackageVersion)/ + + + + + aspnetcore/npm/$(PackageVersion)/ + + <_FilesToPublish Include="@(NpmPackageToPublish)" /> + + + + %(_FilesToPublish.BlobBasePath)%(_FilesToPublish.FileName)%(_FilesToPublish.Extension) + aspnetcore/Runtime/$(PackageVersion)/%(_FilesToPublish.FileName)%(_FilesToPublish.Extension) + + <_FilesToPublish Remove="@(_FilesToPublish)" /> + + + + + + + + + NonShipping=true + + + NonShipping=true + + + + + + + + + + + + + + + + + + + + + + + $(PublishMyGetNpmRegistryUrl.Replace("https:", "")):_authToken + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/tasks/PublishToAzureBlob.cs b/build/tasks/PublishToAzureBlob.cs index 86150d0be3..71f1b1c29a 100644 --- a/build/tasks/PublishToAzureBlob.cs +++ b/build/tasks/PublishToAzureBlob.cs @@ -1,6 +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. +#if BUILD_AZ_TASKS using System; using System.Collections.Generic; using System.IO; @@ -142,3 +143,4 @@ namespace RepoTasks } } } +#endif diff --git a/build/tasks/RepoTasks.csproj b/build/tasks/RepoTasks.csproj index 82c90e29a2..06c08355a3 100644 --- a/build/tasks/RepoTasks.csproj +++ b/build/tasks/RepoTasks.csproj @@ -5,13 +5,14 @@ netcoreapp2.2 net461 $(DefineConstants);BUILD_MSI_TASKS + $(DefineConstants);BUILD_AZ_TASKS - + diff --git a/build/tasks/RepoTasks.tasks b/build/tasks/RepoTasks.tasks index 43323cf2e2..e073e49173 100644 --- a/build/tasks/RepoTasks.tasks +++ b/build/tasks/RepoTasks.tasks @@ -9,6 +9,6 @@ - + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 810775dee9..c11c8325a8 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -37,227 +37,231 @@ https://github.com/aspnet/EntityFrameworkCore 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 - + + https://github.com/aspnet/Extensions + 000000 + + https://github.com/aspnet/Extensions 000000 @@ -355,11 +359,11 @@ - + https://github.com/aspnet/Extensions 000000 - + https://github.com/aspnet/Extensions 000000 diff --git a/eng/Versions.props b/eng/Versions.props index d0e0fa9d3e..fd7bd4c55c 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -35,64 +35,65 @@ 4.6.0-preview.18619.1 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 - 3.0.0-preview.19059.4 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 + 3.0.0-preview.19059.5 3.0.0-preview.18604.3 @@ -105,7 +106,7 @@ 3.0.0-preview.18604.3 - 3.0.0-preview-19057-06 + 3.0.0-preview-19064-09 diff --git a/src/Azure/Azure.sln b/src/Azure/Azure.sln index b26a534edd..f3724c7bc7 100644 --- a/src/Azure/Azure.sln +++ b/src/Azure/Azure.sln @@ -73,6 +73,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.TestHo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization", "..\Security\Authorization\Core\src\Microsoft.AspNetCore.Authorization.csproj", "{C57DFBC2-A887-44B4-A149-7ABFA6D98F7E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.OpenIdConnect", "..\Security\Authentication\OpenIdConnect\src\Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj", "{F44054A2-DAC9-467F-B899-F35F9DCDAE9C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.OAuth", "..\Security\Authentication\OAuth\src\Microsoft.AspNetCore.Authentication.OAuth.csproj", "{406DF28A-0B58-408E-96B0-2D373EE36352}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication", "..\Security\Authentication\Core\src\Microsoft.AspNetCore.Authentication.csproj", "{A5E7BA46-B76B-467A-88FA-38E04D0A42FC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -383,6 +389,42 @@ Global {C57DFBC2-A887-44B4-A149-7ABFA6D98F7E}.Release|x64.Build.0 = Release|Any CPU {C57DFBC2-A887-44B4-A149-7ABFA6D98F7E}.Release|x86.ActiveCfg = Release|Any CPU {C57DFBC2-A887-44B4-A149-7ABFA6D98F7E}.Release|x86.Build.0 = Release|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Debug|x64.Build.0 = Debug|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Debug|x86.Build.0 = Debug|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Release|Any CPU.Build.0 = Release|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Release|x64.ActiveCfg = Release|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Release|x64.Build.0 = Release|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Release|x86.ActiveCfg = Release|Any CPU + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C}.Release|x86.Build.0 = Release|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Debug|Any CPU.Build.0 = Debug|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Debug|x64.ActiveCfg = Debug|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Debug|x64.Build.0 = Debug|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Debug|x86.ActiveCfg = Debug|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Debug|x86.Build.0 = Debug|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Release|Any CPU.ActiveCfg = Release|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Release|Any CPU.Build.0 = Release|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Release|x64.ActiveCfg = Release|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Release|x64.Build.0 = Release|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Release|x86.ActiveCfg = Release|Any CPU + {406DF28A-0B58-408E-96B0-2D373EE36352}.Release|x86.Build.0 = Release|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Debug|x64.ActiveCfg = Debug|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Debug|x64.Build.0 = Debug|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Debug|x86.ActiveCfg = Debug|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Debug|x86.Build.0 = Debug|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Release|Any CPU.Build.0 = Release|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Release|x64.ActiveCfg = Release|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Release|x64.Build.0 = Release|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Release|x86.ActiveCfg = Release|Any CPU + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -418,6 +460,9 @@ Global {38027842-48A7-4A64-A44F-004BAF0AB450} = {84622717-F98A-4DE2-806E-1EF89C45C0EB} {C520D48E-87A0-463D-B4CF-3E6B68F8F4D0} = {84622717-F98A-4DE2-806E-1EF89C45C0EB} {C57DFBC2-A887-44B4-A149-7ABFA6D98F7E} = {84622717-F98A-4DE2-806E-1EF89C45C0EB} + {F44054A2-DAC9-467F-B899-F35F9DCDAE9C} = {84622717-F98A-4DE2-806E-1EF89C45C0EB} + {406DF28A-0B58-408E-96B0-2D373EE36352} = {84622717-F98A-4DE2-806E-1EF89C45C0EB} + {A5E7BA46-B76B-467A-88FA-38E04D0A42FC} = {84622717-F98A-4DE2-806E-1EF89C45C0EB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {81AADD49-473B-43ED-8A08-F6B7A058AA39} diff --git a/src/Azure/AzureAD/test/FunctionalTests/WebAuthenticationTests.cs b/src/Azure/AzureAD/test/FunctionalTests/WebAuthenticationTests.cs index dc8e5f8271..d724ee2561 100644 --- a/src/Azure/AzureAD/test/FunctionalTests/WebAuthenticationTests.cs +++ b/src/Azure/AzureAD/test/FunctionalTests/WebAuthenticationTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.using Microsoft.AspNetCore.Authorization; using System.Net; @@ -11,7 +11,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Xunit; @@ -158,5 +160,60 @@ namespace Microsoft.AspNetCore.Authentication.AzureAD.FunctionalTests // Assert Assert.Equal(expectedStatusCode, response.StatusCode); } + + [Fact] + public async Task ADB2C_EndToEnd_PasswordReset() + { + var client = Factory.WithWebHostBuilder(builder => builder.ConfigureTestServices( + services => + { + services + .AddAuthentication(AzureADB2CDefaults.AuthenticationScheme) + .AddAzureADB2C(o => + { + o.Instance = "https://login.microsoftonline.com/tfp/"; + o.ClientId = "ClientId"; + o.CallbackPath = "/signin-oidc"; + o.Domain = "test.onmicrosoft.com"; + o.SignUpSignInPolicyId = "B2C_1_SiUpIn"; + o.ResetPasswordPolicyId = "B2C_1_SSPR"; + o.EditProfilePolicyId = "B2C_1_SiPe"; + }); + + services.Configure(AzureADB2CDefaults.OpenIdScheme, o => + { + o.Configuration = new OpenIdConnectConfiguration() + { + Issuer = "https://www.example.com", + TokenEndpoint = "https://www.example.com/token", + AuthorizationEndpoint = "https://www.example.com/authorize", + EndSessionEndpoint = "https://www.example.com/logout" + }; + // CookieContainer doesn't allow cookies from other paths + o.CorrelationCookie.Path = "/"; + o.NonceCookie.Path = "/"; + }); + + services.AddMvc(o => o.Filters.Add( + new AuthorizeFilter(new AuthorizationPolicyBuilder(new[] { AzureADB2CDefaults.AuthenticationScheme }) + .RequireAuthenticatedUser().Build()))); + })).CreateClient(new WebApplicationFactoryClientOptions() { AllowAutoRedirect = false }); + + var response = await client.GetAsync("/api/get"); + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + + var location = response.Headers.Location; + Assert.StartsWith("https://www.example.com/authorize", location.AbsoluteUri); + var queryString = location.Query; + var query = QueryHelpers.ParseQuery(queryString); + var state = query["state"]; + Assert.False(StringValues.IsNullOrEmpty(state)); + + // Mock Authorization response + response = await client.GetAsync($"/signin-oidc?error=access_denied&error_description=AADB2C90118&state={state}"); + + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal("/AzureADB2C/Account/ResetPassword/AzureADB2C", response.Headers.Location.OriginalString); + } } } diff --git a/src/Azure/AzureAD/test/testassets/AzureAD.WebSite/AzureAD.WebSite.csproj b/src/Azure/AzureAD/test/testassets/AzureAD.WebSite/AzureAD.WebSite.csproj index 73b832bf22..25e2626cfe 100644 --- a/src/Azure/AzureAD/test/testassets/AzureAD.WebSite/AzureAD.WebSite.csproj +++ b/src/Azure/AzureAD/test/testassets/AzureAD.WebSite/AzureAD.WebSite.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -9,16 +9,8 @@ - + - - - - - - - - diff --git a/src/Azure/AzureAD/test/testassets/AzureAD.WebSite/Program.cs b/src/Azure/AzureAD/test/testassets/AzureAD.WebSite/Program.cs index 01046c73a0..998a9ab324 100644 --- a/src/Azure/AzureAD/test/testassets/AzureAD.WebSite/Program.cs +++ b/src/Azure/AzureAD/test/testassets/AzureAD.WebSite/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -23,55 +23,8 @@ namespace AzureAD.WebSite public static IWebHostBuilder CreateWebHostBuilder(string[] args) { - var builder = new WebHostBuilder() - .UseKestrel((builderContext, options) => - { - options.Configure(builderContext.Configuration.GetSection("Kestrel")); - }) - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - var env = hostingContext.HostingEnvironment; - - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - - if (env.IsDevelopment()) - { - var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); - if (appAssembly != null) - { - config.AddUserSecrets(appAssembly, optional: true); - } - } - - config.AddEnvironmentVariables(); - - if (args != null) - { - config.AddCommandLine(args); - } - }) - .ConfigureLogging((hostingContext, logging) => - { - logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); - logging.AddConsole(); - logging.AddDebug(); - }) - .UseIISIntegration() - .UseDefaultServiceProvider((context, options) => - { - options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); - }); - - if (args != null) - { - builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build()); - } - - builder.UseStartup(); - - return builder; + return WebHost.CreateDefaultBuilder() + .UseStartup(); } } } diff --git a/src/Components/.editorconfig b/src/Components/.editorconfig index 0d238f8e41..f9d02415ce 100644 --- a/src/Components/.editorconfig +++ b/src/Components/.editorconfig @@ -25,3 +25,31 @@ indent_size = 2 [*.{xml,csproj,config,*proj,targets,props}] indent_size = 2 + +# Dotnet code style settings: +[*.cs] +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true + +# Don't use this. qualifier +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion + +# use int x = .. over Int32 +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +# use int.MaxValue over Int32.MaxValue +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Require var all the time. +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true diff --git a/src/Components/blazor/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj b/src/Components/blazor/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj index c68831682f..76cb6f8495 100644 --- a/src/Components/blazor/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj +++ b/src/Components/blazor/src/Microsoft.AspNetCore.Blazor.Build/Microsoft.AspNetCore.Blazor.Build.csproj @@ -22,6 +22,9 @@ + + + @@ -37,7 +40,6 @@ - diff --git a/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Microsoft.AspNetCore.Blazor.csproj b/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Microsoft.AspNetCore.Blazor.csproj index b01c0d97d0..1aa8b3eba5 100644 --- a/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Microsoft.AspNetCore.Blazor.csproj +++ b/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Microsoft.AspNetCore.Blazor.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Rendering/WebAssemblyRenderer.cs b/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Rendering/WebAssemblyRenderer.cs index e6b4de5dc6..93941e4d34 100644 --- a/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/blazor/src/Microsoft.AspNetCore.Blazor/Rendering/WebAssemblyRenderer.cs @@ -15,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, IDisposable + public class WebAssemblyRenderer : Renderer { private readonly int _webAssemblyRendererId; @@ -71,11 +71,10 @@ namespace Microsoft.AspNetCore.Blazor.Rendering RenderRootComponent(componentId); } - /// - /// Disposes the instance. - /// - public void Dispose() + /// + protected override void Dispose(bool disposing) { + base.Dispose(disposing); RendererRegistry.Current.TryRemove(_webAssemblyRendererId); } diff --git a/src/Components/build/dependencies.props b/src/Components/build/dependencies.props index 9e3ba7f2cf..13fac9d3ab 100644 --- a/src/Components/build/dependencies.props +++ b/src/Components/build/dependencies.props @@ -12,7 +12,7 @@ 3.0.0-alpha1-10605 3.0.0-alpha1-10605 3.0.0-alpha1-10605 - 3.0.0-preview-19057-06 + 3.0.0-preview-19064-09 0.10.1 3.0.0-alpha1-10605 0.8.0-preview-19064-0339 @@ -20,7 +20,8 @@ 2.1.2 0.8.0-preview1-20181126.1 - - 0.8.0-preview1-20181126.4 + 3.0.0-preview.19059.5 + + 0.8.0-preview1-20181126.4 diff --git a/src/Components/src/Microsoft.AspNetCore.Components.Browser/Microsoft.AspNetCore.Components.Browser.csproj b/src/Components/src/Microsoft.AspNetCore.Components.Browser/Microsoft.AspNetCore.Components.Browser.csproj index d56001821d..1d49c29df7 100644 --- a/src/Components/src/Microsoft.AspNetCore.Components.Browser/Microsoft.AspNetCore.Components.Browser.csproj +++ b/src/Components/src/Microsoft.AspNetCore.Components.Browser/Microsoft.AspNetCore.Components.Browser.csproj @@ -2,7 +2,7 @@ netstandard2.0 - Support for rendering ASP.NET Core components in browsers. + Support for rendering ASP.NET Core components for browsers. true diff --git a/src/Components/src/Microsoft.AspNetCore.Components.Browser/Properties/AssemblyInfo.cs b/src/Components/src/Microsoft.AspNetCore.Components.Browser/Properties/AssemblyInfo.cs index b513292596..a311581a18 100644 --- a/src/Components/src/Microsoft.AspNetCore.Components.Browser/Properties/AssemblyInfo.cs +++ b/src/Components/src/Microsoft.AspNetCore.Components.Browser/Properties/AssemblyInfo.cs @@ -2,3 +2,5 @@ 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.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Components/src/Microsoft.AspNetCore.Components.Build/targets/RazorCompilation.targets b/src/Components/src/Microsoft.AspNetCore.Components.Build/targets/RazorCompilation.targets index f3c62f1e56..2dbfe3ce78 100644 --- a/src/Components/src/Microsoft.AspNetCore.Components.Build/targets/RazorCompilation.targets +++ b/src/Components/src/Microsoft.AspNetCore.Components.Build/targets/RazorCompilation.targets @@ -12,6 +12,7 @@ false false false + <_RazorComponentInclude>**\*.cshtml @@ -24,8 +25,4 @@ - - - - diff --git a/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/CircuitHost.cs b/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/CircuitHost.cs index 8c483daf8b..ea62df438c 100644 --- a/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/CircuitHost.cs +++ b/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/CircuitHost.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits { internal class CircuitHost : IDisposable { - private static AsyncLocal _current = new AsyncLocal(); + private static readonly AsyncLocal _current = new AsyncLocal(); /// /// Gets the current , if any. @@ -24,23 +24,18 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public static CircuitHost Current => _current.Value; /// - /// Sets the current . + /// Sets the current . /// - /// The . + /// The . /// /// Calling will store the circuit /// and other related values such as the and /// in the local execution context. Application code should not need to call this method, - /// it is primarily used by the Server-Side Blazor infrastructure. + /// it is primarily used by the Server-Side Components infrastructure. /// public static void SetCurrentCircuitHost(CircuitHost circuitHost) { - if (circuitHost == null) - { - throw new ArgumentNullException(nameof(circuitHost)); - } - - _current.Value = circuitHost; + _current.Value = circuitHost ?? throw new ArgumentNullException(nameof(circuitHost)); Microsoft.JSInterop.JSRuntime.SetCurrentJSRuntime(circuitHost.JSRuntime); RendererRegistry.SetCurrentRendererRegistry(circuitHost.RendererRegistry); @@ -134,6 +129,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits public void Dispose() { Scope.Dispose(); + Renderer.Dispose(); } private void AssertInitialized() diff --git a/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/RemoteRenderer.cs b/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/RemoteRenderer.cs index e60b9643e1..b81673f8ca 100644 --- a/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/RemoteRenderer.cs +++ b/src/Components/src/Microsoft.AspNetCore.Components.Server/Circuits/RemoteRenderer.cs @@ -120,11 +120,10 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering } } - /// - /// Disposes the instance. - /// - public void Dispose() + /// + protected override void Dispose(bool disposing) { + base.Dispose(true); _rendererRegistry.TryRemove(_id); } diff --git a/src/Components/src/Microsoft.AspNetCore.Components/Microsoft.AspNetCore.Components.csproj b/src/Components/src/Microsoft.AspNetCore.Components/Microsoft.AspNetCore.Components.csproj index 58295c79da..038d8d67ef 100644 --- a/src/Components/src/Microsoft.AspNetCore.Components/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/src/Microsoft.AspNetCore.Components/Microsoft.AspNetCore.Components.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs b/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs index ff8e2b0f8e..3dde251101 100644 --- a/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs +++ b/src/Components/src/Microsoft.AspNetCore.Components/Rendering/Renderer.cs @@ -12,19 +12,17 @@ namespace Microsoft.AspNetCore.Components.Rendering /// Provides mechanisms for rendering hierarchies of instances, /// dispatching events to them, and notifying when the user interface is being updated. /// - public abstract class Renderer + public abstract class Renderer : IDisposable { private readonly ComponentFactory _componentFactory; - private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it - private readonly Dictionary _componentStateById - = new Dictionary(); - + private readonly Dictionary _componentStateById = new Dictionary(); private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder(); - private bool _isBatchInProgress; - - private int _lastEventHandlerId = 0; private readonly Dictionary _eventBindings = new Dictionary(); + private int _nextComponentId = 0; // TODO: change to 'long' when Mono .NET->JS interop supports it + private bool _isBatchInProgress; + private int _lastEventHandlerId = 0; + /// /// Constructs an instance of . /// @@ -175,7 +173,7 @@ namespace Microsoft.AspNetCore.Components.Rendering if (frame.AttributeValue is MulticastDelegate @delegate) { - _eventBindings.Add(id, new EventHandlerInvoker(@delegate)); + _eventBindings.Add(id, new EventHandlerInvoker(@delegate)); } frame = frame.WithAttributeEventHandlerId(id); @@ -295,5 +293,44 @@ namespace Microsoft.AspNetCore.Components.Rendering RemoveEventHandlerIds(eventHandlerIdsClone, Task.CompletedTask)); } } + + /// + /// Releases all resources currently used by this instance. + /// + /// if this method is being invoked by , otherwise . + protected virtual void Dispose(bool disposing) + { + List exceptions = null; + + foreach (var componentState in _componentStateById.Values) + { + if (componentState.Component is IDisposable disposable) + { + try + { + disposable.Dispose(); + } + catch (Exception exception) + { + // Capture exceptions thrown by individual components and rethrow as an aggregate. + exceptions = exceptions ?? new List(); + exceptions.Add(exception); + } + } + } + + if (exceptions != null) + { + throw new AggregateException(exceptions); + } + } + + /// + /// Releases all resources currently used by this instance. + /// + public void Dispose() + { + Dispose(disposing: true); + } } } diff --git a/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Circuits/CircuitHostTest.cs b/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Circuits/CircuitHostTest.cs new file mode 100644 index 0000000000..27a35bfa73 --- /dev/null +++ b/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Circuits/CircuitHostTest.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using Microsoft.AspNetCore.Components.Browser; +using Microsoft.AspNetCore.Components.Browser.Rendering; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.JSInterop; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Server.Circuits +{ + public class CircuitHostTest + { + [Fact] + public void Dispose_DisposesResources() + { + // Arrange + var serviceScope = new Mock(); + var clientProxy = Mock.Of(); + var renderRegistry = new RendererRegistry(); + var jsRuntime = Mock.Of(); + var syncContext = new CircuitSynchronizationContext(); + + var remoteRenderer = new TestRemoteRenderer( + Mock.Of(), + renderRegistry, + jsRuntime, + clientProxy, + syncContext); + + var circuitHost = new CircuitHost(serviceScope.Object, clientProxy, renderRegistry, remoteRenderer, configure: _ => { }, jsRuntime: jsRuntime, synchronizationContext: syncContext); + + // Act + circuitHost.Dispose(); + + // Assert + serviceScope.Verify(s => s.Dispose(), Times.Once()); + Assert.True(remoteRenderer.Disposed); + } + + private class TestRemoteRenderer : RemoteRenderer + { + public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client, SynchronizationContext syncContext) + : base(serviceProvider, rendererRegistry, jsRuntime, client, syncContext) + { + } + + public bool Disposed { get; set; } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + Disposed = true; + } + } + } +} diff --git a/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Microsoft.AspNetCore.Components.Server.Test.csproj b/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Microsoft.AspNetCore.Components.Server.Test.csproj index 152cb7df88..204aecef37 100644 --- a/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Microsoft.AspNetCore.Components.Server.Test.csproj +++ b/src/Components/test/Microsoft.AspNetCore.Components.Server.Test/Microsoft.AspNetCore.Components.Server.Test.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -7,6 +7,7 @@ + diff --git a/src/Components/test/Microsoft.AspNetCore.Components.Test/RendererTest.cs b/src/Components/test/Microsoft.AspNetCore.Components.Test/RendererTest.cs index 5fe8b79097..57eb16707d 100644 --- a/src/Components/test/Microsoft.AspNetCore.Components.Test/RendererTest.cs +++ b/src/Components/test/Microsoft.AspNetCore.Components.Test/RendererTest.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Test.Helpers; @@ -1139,6 +1138,78 @@ namespace Microsoft.AspNetCore.Components.Test Assert.Equal(2, numEventsFired); } + [Fact] + public void DisposingRenderer_DisposesTopLevelComponents() + { + // Arrange + var renderer = new TestRenderer(); + var component = new DisposableComponent(); + renderer.AssignRootComponentId(component); + + // Act + renderer.Dispose(); + + // Assert + Assert.True(component.Disposed); + } + + [Fact] + public void DisposingRenderer_DisposesNestedComponents() + { + // Arrange + var renderer = new TestRenderer(); + var component = new TestComponent(builder => + { + builder.AddContent(0, "Hello"); + builder.OpenComponent(1); + builder.CloseComponent(); + }); + var componentId = renderer.AssignRootComponentId(component); + component.TriggerRender(); + var batch = renderer.Batches.Single(); + var componentFrame = batch.ReferenceFrames + .Single(frame => frame.FrameType == RenderTreeFrameType.Component); + var nestedComponent = Assert.IsType(componentFrame.Component); + + // Act + renderer.Dispose(); + + // Assert + Assert.True(component.Disposed); + Assert.True(nestedComponent.Disposed); + } + + [Fact] + public void DisposingRenderer_CapturesExceptionsFromAllRegisteredComponents() + { + // Arrange + var renderer = new TestRenderer(); + var exception1 = new Exception(); + var exception2 = new Exception(); + var component = new TestComponent(builder => + { + builder.AddContent(0, "Hello"); + builder.OpenComponent(1); + builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception1)); + builder.CloseComponent(); + + builder.OpenComponent(2); + builder.AddAttribute(1, nameof(DisposableComponent.DisposeAction), (Action)(() => throw exception2)); + builder.CloseComponent(); + }); + var componentId = renderer.AssignRootComponentId(component); + component.TriggerRender(); + + // Act &A Assert + var aggregate = Assert.Throws(renderer.Dispose); + + // All components must be disposed even if some throw as part of being diposed. + Assert.True(component.Disposed); + Assert.Equal(2, aggregate.InnerExceptions.Count); + Assert.Contains(exception1, aggregate.InnerExceptions); + Assert.Contains(exception2, aggregate.InnerExceptions); + } + private class NoOpRenderer : Renderer { public NoOpRenderer() : base(new TestServiceProvider()) @@ -1152,7 +1223,7 @@ namespace Microsoft.AspNetCore.Components.Test => Task.CompletedTask; } - private class TestComponent : IComponent + private class TestComponent : IComponent, IDisposable { private RenderHandle _renderHandle; private RenderFragment _renderFragment; @@ -1172,6 +1243,10 @@ namespace Microsoft.AspNetCore.Components.Test public void TriggerRender() => _renderHandle.Render(_renderFragment); + + public bool Disposed { get; private set; } + + void IDisposable.Dispose() => Disposed = true; } private class MessageComponent : AutoRenderComponent @@ -1398,6 +1473,24 @@ namespace Microsoft.AspNetCore.Components.Test } } + private class DisposableComponent : AutoRenderComponent, IDisposable + { + public bool Disposed { get; private set; } + + [Parameter] + public Action DisposeAction { get; private set; } + + public void Dispose() + { + Disposed = true; + DisposeAction?.Invoke(); + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + } + } + class TestAsyncRenderer : TestRenderer { public Task NextUpdateDisplayReturnTask { get; set; } diff --git a/src/Framework/Microsoft.AspNetCore.App.props b/src/Framework/Microsoft.AspNetCore.App.props index 3be5f12ffe..d2dc4ccd41 100644 --- a/src/Framework/Microsoft.AspNetCore.App.props +++ b/src/Framework/Microsoft.AspNetCore.App.props @@ -19,6 +19,8 @@ + + @@ -127,6 +129,7 @@ + diff --git a/src/Hosting/TestHost/src/AsyncStreamWrapper.cs b/src/Hosting/TestHost/src/AsyncStreamWrapper.cs new file mode 100644 index 0000000000..32f1548d35 --- /dev/null +++ b/src/Hosting/TestHost/src/AsyncStreamWrapper.cs @@ -0,0 +1,128 @@ +// 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; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.TestHost +{ + internal class AsyncStreamWrapper : Stream + { + private Stream _inner; + private Func _allowSynchronousIO; + + internal AsyncStreamWrapper(Stream inner, Func allowSynchronousIO) + { + _inner = inner; + _allowSynchronousIO = allowSynchronousIO; + } + + public override bool CanRead => _inner.CanRead; + + public override bool CanSeek => _inner.CanSeek; + + public override bool CanWrite => _inner.CanWrite; + + public override long Length => _inner.Length; + + public override long Position { get => _inner.Position; set => _inner.Position = value; } + + public override void Flush() + { + // Not blocking Flush because things like StreamWriter.Dispose() always call it. + _inner.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!_allowSynchronousIO()) + { + throw new InvalidOperationException("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true."); + } + + return _inner.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + return _inner.ReadAsync(buffer, cancellationToken); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _inner.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return _inner.EndRead(asyncResult); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _inner.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _inner.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (!_allowSynchronousIO()) + { + throw new InvalidOperationException("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true."); + } + + _inner.Write(buffer, offset, count); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return _inner.BeginWrite(buffer, offset, count, callback, state); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + _inner.EndWrite(asyncResult); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + return _inner.WriteAsync(buffer, cancellationToken); + } + + public override void Close() + { + _inner.Close(); + } + + protected override void Dispose(bool disposing) + { + _inner.Dispose(); + } + + public override ValueTask DisposeAsync() + { + return _inner.DisposeAsync(); + } + } +} diff --git a/src/Hosting/TestHost/src/ClientHandler.cs b/src/Hosting/TestHost/src/ClientHandler.cs index 5471e23d19..c947623220 100644 --- a/src/Hosting/TestHost/src/ClientHandler.cs +++ b/src/Hosting/TestHost/src/ClientHandler.cs @@ -43,6 +43,8 @@ namespace Microsoft.AspNetCore.TestHost _pathBase = pathBase; } + internal bool AllowSynchronousIO { get; set; } + /// /// This adapts HttpRequestMessages to ASP.NET Core requests, dispatches them through the pipeline, and returns the /// associated HttpResponseMessage. @@ -59,7 +61,7 @@ namespace Microsoft.AspNetCore.TestHost throw new ArgumentNullException(nameof(request)); } - var contextBuilder = new HttpContextBuilder(_application); + var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO); Stream responseBody = null; var requestContent = request.Content ?? new StreamContent(Stream.Null); @@ -110,7 +112,7 @@ namespace Microsoft.AspNetCore.TestHost // This body may have been consumed before, rewind it. body.Seek(0, SeekOrigin.Begin); } - req.Body = body; + req.Body = new AsyncStreamWrapper(body, () => contextBuilder.AllowSynchronousIO); responseBody = context.Response.Body; }); diff --git a/src/Hosting/TestHost/src/HttpContextBuilder.cs b/src/Hosting/TestHost/src/HttpContextBuilder.cs index c576628b65..69acf27591 100644 --- a/src/Hosting/TestHost/src/HttpContextBuilder.cs +++ b/src/Hosting/TestHost/src/HttpContextBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -11,7 +11,7 @@ using static Microsoft.AspNetCore.Hosting.Internal.HostingApplication; namespace Microsoft.AspNetCore.TestHost { - internal class HttpContextBuilder + internal class HttpContextBuilder : IHttpBodyControlFeature { private readonly IHttpApplication _application; private readonly HttpContext _httpContext; @@ -23,24 +23,28 @@ namespace Microsoft.AspNetCore.TestHost private bool _pipelineFinished; private Context _testContext; - internal HttpContextBuilder(IHttpApplication application) + internal HttpContextBuilder(IHttpApplication application, bool allowSynchronousIO) { _application = application ?? throw new ArgumentNullException(nameof(application)); + AllowSynchronousIO = allowSynchronousIO; _httpContext = new DefaultHttpContext(); var request = _httpContext.Request; request.Protocol = "HTTP/1.1"; request.Method = HttpMethods.Get; + _httpContext.Features.Set(this); _httpContext.Features.Set(_responseFeature); var requestLifetimeFeature = new HttpRequestLifetimeFeature(); requestLifetimeFeature.RequestAborted = _requestAbortedSource.Token; _httpContext.Features.Set(requestLifetimeFeature); - _responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest); + _responseStream = new ResponseStream(ReturnResponseMessageAsync, AbortRequest, () => AllowSynchronousIO); _responseFeature.Body = _responseStream; } + public bool AllowSynchronousIO { get; set; } + internal void Configure(Action configureContext) { if (configureContext == null) @@ -136,4 +140,4 @@ namespace Microsoft.AspNetCore.TestHost _responseTcs.TrySetException(exception); } } -} \ No newline at end of file +} diff --git a/src/Hosting/TestHost/src/ResponseStream.cs b/src/Hosting/TestHost/src/ResponseStream.cs index b2ababa182..7563beb4c3 100644 --- a/src/Hosting/TestHost/src/ResponseStream.cs +++ b/src/Hosting/TestHost/src/ResponseStream.cs @@ -24,13 +24,15 @@ namespace Microsoft.AspNetCore.TestHost private Func _onFirstWriteAsync; private bool _firstWrite; private Action _abortRequest; + private Func _allowSynchronousIO; private Pipe _pipe = new Pipe(); - internal ResponseStream(Func onFirstWriteAsync, Action abortRequest) + internal ResponseStream(Func onFirstWriteAsync, Action abortRequest, Func allowSynchronousIO) { _onFirstWriteAsync = onFirstWriteAsync ?? throw new ArgumentNullException(nameof(onFirstWriteAsync)); _abortRequest = abortRequest ?? throw new ArgumentNullException(nameof(abortRequest)); + _allowSynchronousIO = allowSynchronousIO ?? throw new ArgumentNullException(nameof(allowSynchronousIO)); _firstWrite = true; _writeLock = new SemaphoreSlim(1, 1); } @@ -144,6 +146,11 @@ namespace Microsoft.AspNetCore.TestHost // Write with count 0 will still trigger OnFirstWrite public override void Write(byte[] buffer, int offset, int count) { + if (!_allowSynchronousIO()) + { + throw new InvalidOperationException("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true."); + } + // The Pipe Write method requires calling FlushAsync to notify the reader. Call WriteAsync instead. WriteAsync(buffer, offset, count).GetAwaiter().GetResult(); } diff --git a/src/Hosting/TestHost/src/TestServer.cs b/src/Hosting/TestHost/src/TestServer.cs index 6c45c0b21b..c5668185ac 100644 --- a/src/Hosting/TestHost/src/TestServer.cs +++ b/src/Hosting/TestHost/src/TestServer.cs @@ -77,6 +77,14 @@ namespace Microsoft.AspNetCore.TestHost public IFeatureCollection Features { get; } + /// + /// Gets or sets a value that controls whether synchronous IO is allowed for the and + /// + /// + /// Defaults to true. + /// + public bool AllowSynchronousIO { get; set; } = true; + private IHttpApplication Application { get => _application ?? throw new InvalidOperationException("The server has not been started or no web application was configured."); @@ -85,7 +93,7 @@ namespace Microsoft.AspNetCore.TestHost public HttpMessageHandler CreateHandler() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); - return new ClientHandler(pathBase, Application); + return new ClientHandler(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO }; } public HttpClient CreateClient() @@ -96,7 +104,7 @@ namespace Microsoft.AspNetCore.TestHost public WebSocketClient CreateWebSocketClient() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); - return new WebSocketClient(pathBase, Application); + return new WebSocketClient(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO }; } /// @@ -120,7 +128,7 @@ namespace Microsoft.AspNetCore.TestHost throw new ArgumentNullException(nameof(configureContext)); } - var builder = new HttpContextBuilder(Application); + var builder = new HttpContextBuilder(Application, AllowSynchronousIO); builder.Configure(context => { var request = context.Request; @@ -138,6 +146,7 @@ namespace Microsoft.AspNetCore.TestHost request.PathBase = pathBase; }); builder.Configure(configureContext); + // TODO: Wrap the request body if any? return await builder.SendAsync(cancellationToken).ConfigureAwait(false); } diff --git a/src/Hosting/TestHost/src/WebSocketClient.cs b/src/Hosting/TestHost/src/WebSocketClient.cs index e3deb670a5..cec96ab4ce 100644 --- a/src/Hosting/TestHost/src/WebSocketClient.cs +++ b/src/Hosting/TestHost/src/WebSocketClient.cs @@ -46,10 +46,12 @@ namespace Microsoft.AspNetCore.TestHost set; } + internal bool AllowSynchronousIO { get; set; } + public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken) { WebSocketFeature webSocketFeature = null; - var contextBuilder = new HttpContextBuilder(_application); + var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO); contextBuilder.Configure(context => { var request = context.Request; @@ -131,4 +133,4 @@ namespace Microsoft.AspNetCore.TestHost } } } -} \ No newline at end of file +} diff --git a/src/Hosting/TestHost/test/ClientHandlerTests.cs b/src/Hosting/TestHost/test/ClientHandlerTests.cs index f287ebd053..012867dd28 100644 --- a/src/Hosting/TestHost/test/ClientHandlerTests.cs +++ b/src/Hosting/TestHost/test/ClientHandlerTests.cs @@ -92,13 +92,12 @@ namespace Microsoft.AspNetCore.TestHost public async Task ResubmitRequestWorks() { int requestCount = 1; - var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => + var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context => { - int read = context.Request.Body.Read(new byte[100], 0, 100); + int read = await context.Request.Body.ReadAsync(new byte[100], 0, 100); Assert.Equal(11, read); context.Response.Headers["TestHeader"] = "TestValue:" + requestCount++; - return Task.FromResult(0); })); HttpMessageInvoker invoker = new HttpMessageInvoker(handler); diff --git a/src/Hosting/TestHost/test/HttpContextBuilderTests.cs b/src/Hosting/TestHost/test/HttpContextBuilderTests.cs index f04a2f16f9..88126de9e8 100644 --- a/src/Hosting/TestHost/test/HttpContextBuilderTests.cs +++ b/src/Hosting/TestHost/test/HttpContextBuilderTests.cs @@ -109,6 +109,7 @@ namespace Microsoft.AspNetCore.TestHost { c.Response.Headers["TestHeader"] = "TestValue"; var bytes = Encoding.UTF8.GetBytes("BodyStarted" + Environment.NewLine); + c.Features.Get().AllowSynchronousIO = true; c.Response.Body.Write(bytes, 0, bytes.Length); await block.Task; bytes = Encoding.UTF8.GetBytes("BodyFinished"); diff --git a/src/Hosting/TestHost/test/TestClientTests.cs b/src/Hosting/TestHost/test/TestClientTests.cs index 3101c2965f..7b86c18978 100644 --- a/src/Hosting/TestHost/test/TestClientTests.cs +++ b/src/Hosting/TestHost/test/TestClientTests.cs @@ -87,8 +87,8 @@ namespace Microsoft.AspNetCore.TestHost public async Task PutAsyncWorks() { // Arrange - RequestDelegate appDelegate = ctx => - ctx.Response.WriteAsync(new StreamReader(ctx.Request.Body).ReadToEnd() + " PUT Response"); + RequestDelegate appDelegate = async ctx => + await ctx.Response.WriteAsync(await new StreamReader(ctx.Request.Body).ReadToEndAsync() + " PUT Response"); var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var server = new TestServer(builder); var client = server.CreateClient(); @@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.TestHost { // Arrange RequestDelegate appDelegate = async ctx => - await ctx.Response.WriteAsync(new StreamReader(ctx.Request.Body).ReadToEnd() + " POST Response"); + await ctx.Response.WriteAsync(await new StreamReader(ctx.Request.Body).ReadToEndAsync() + " POST Response"); var builder = new WebHostBuilder().Configure(app => app.Run(appDelegate)); var server = new TestServer(builder); var client = server.CreateClient(); @@ -132,16 +132,15 @@ namespace Microsoft.AspNetCore.TestHost } var builder = new WebHostBuilder(); - RequestDelegate app = (ctx) => + RequestDelegate app = async ctx => { var disposable = new TestDisposable(); ctx.Response.RegisterForDispose(disposable); - ctx.Response.Body.Write(data, 0, 1024); + await ctx.Response.Body.WriteAsync(data, 0, 1024); Assert.False(disposable.IsDisposed); - ctx.Response.Body.Write(data, 1024, 1024); - return Task.FromResult(0); + await ctx.Response.Body.WriteAsync(data, 1024, 1024); }; builder.Configure(appBuilder => appBuilder.Run(app)); diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index 380a4bd959..2f17e52019 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -22,7 +22,6 @@ Microsoft.AspNetCore.Http.HttpResponse - diff --git a/src/Http/Http.Features/src/IRequestBodyPipeFeature.cs b/src/Http/Http.Features/src/IRequestBodyPipeFeature.cs index 0c996ff691..503c205276 100644 --- a/src/Http/Http.Features/src/IRequestBodyPipeFeature.cs +++ b/src/Http/Http.Features/src/IRequestBodyPipeFeature.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Http.Features public interface IRequestBodyPipeFeature { /// - /// A representing the request body, if any. + /// A representing the request body, if any. /// PipeReader RequestBodyPipe { get; set; } } diff --git a/src/Http/Http.Features/test/Microsoft.AspNetCore.Http.Features.Tests.csproj b/src/Http/Http.Features/test/Microsoft.AspNetCore.Http.Features.Tests.csproj index c83d286a00..6a80fe588a 100644 --- a/src/Http/Http.Features/test/Microsoft.AspNetCore.Http.Features.Tests.csproj +++ b/src/Http/Http.Features/test/Microsoft.AspNetCore.Http.Features.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp3.0;net461 + netcoreapp3.0 diff --git a/src/Http/Http/src/BufferSegment.cs b/src/Http/Http/src/BufferSegment.cs index f0dcdc5077..735a4a39e0 100644 --- a/src/Http/Http/src/BufferSegment.cs +++ b/src/Http/Http/src/BufferSegment.cs @@ -7,7 +7,7 @@ using System.Runtime.CompilerServices; namespace System.IO.Pipelines { - public sealed class BufferSegment : ReadOnlySequenceSegment + internal sealed class BufferSegment : ReadOnlySequenceSegment { private IMemoryOwner _memoryOwner; private BufferSegment _next; diff --git a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj index 1575488b80..d091f7d690 100644 --- a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj +++ b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core default HTTP feature implementations. @@ -19,7 +19,6 @@ - diff --git a/src/Http/Http/src/Properties/AssemblyInfo.cs b/src/Http/Http/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..2b8d94f4a5 --- /dev/null +++ b/src/Http/Http/src/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Http/Http/src/ReadOnlyPipeStream.cs b/src/Http/Http/src/ReadOnlyPipeStream.cs new file mode 100644 index 0000000000..7585947d2c --- /dev/null +++ b/src/Http/Http/src/ReadOnlyPipeStream.cs @@ -0,0 +1,240 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + /// + /// Represents a read-only Stream backed by a PipeReader + /// + public class ReadOnlyPipeStream : Stream + { + private readonly PipeReader _pipeReader; + private bool _allowSynchronousIO = true; + + /// + /// Creates a new ReadOnlyPipeStream + /// + /// The PipeReader to read from. + public ReadOnlyPipeStream(PipeReader pipeReader) : + this(pipeReader, allowSynchronousIO: true) + { + } + + /// + /// Creates a new ReadOnlyPipeStream + /// + /// The PipeReader to read from. + /// Whether synchronous IO is allowed. + public ReadOnlyPipeStream(PipeReader pipeReader, bool allowSynchronousIO) + { + _allowSynchronousIO = allowSynchronousIO; + _pipeReader = pipeReader; + } + + /// + public override bool CanSeek => false; + + /// + public override bool CanRead => true; + + /// + public override bool CanWrite => false; + + /// + public override long Length => throw new NotSupportedException(); + + /// + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// + public override int WriteTimeout + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => throw new NotSupportedException(); + + /// + public override void Flush() + { + throw new NotSupportedException(); + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + if (!_allowSynchronousIO) + { + ThrowHelper.ThrowInvalidOperationException_SynchronousReadsDisallowed(); + } + return ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); + } + + /// + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + var task = ReadAsync(buffer, offset, count, default, state); + if (callback != null) + { + task.ContinueWith(t => callback.Invoke(t)); + } + return task; + } + + /// + public override int EndRead(IAsyncResult asyncResult) + { + return ((Task)asyncResult).GetAwaiter().GetResult(); + } + + private Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + var tcs = new TaskCompletionSource(state); + var task = ReadAsync(buffer, offset, count, cancellationToken); + task.ContinueWith((task2, state2) => + { + var tcs2 = (TaskCompletionSource)state2; + if (task2.IsCanceled) + { + tcs2.SetCanceled(); + } + else if (task2.IsFaulted) + { + tcs2.SetException(task2.Exception); + } + else + { + tcs2.SetResult(task2.Result); + } + }, tcs, cancellationToken); + return tcs.Task; + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsyncInternal(new Memory(buffer, offset, count), cancellationToken).AsTask(); + } + + /// + public override ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) + { + return ReadAsyncInternal(destination, cancellationToken); + } + + private async ValueTask ReadAsyncInternal(Memory buffer, CancellationToken cancellationToken) + { + while (true) + { + var result = await _pipeReader.ReadAsync(cancellationToken); + var readableBuffer = result.Buffer; + var readableBufferLength = readableBuffer.Length; + + var consumed = readableBuffer.End; + var actual = 0; + try + { + if (readableBufferLength != 0) + { + actual = (int)Math.Min(readableBufferLength, buffer.Length); + + var slice = actual == readableBufferLength ? readableBuffer : readableBuffer.Slice(0, actual); + consumed = slice.End; + slice.CopyTo(buffer.Span); + + return actual; + } + + if (result.IsCompleted) + { + return 0; + } + } + finally + { + _pipeReader.AdvanceTo(consumed); + } + } + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + + return CopyToAsyncInternal(destination, cancellationToken); + } + + private async Task CopyToAsyncInternal(Stream destination, CancellationToken cancellationToken) + { + while (true) + { + var result = await _pipeReader.ReadAsync(cancellationToken); + var readableBuffer = result.Buffer; + var readableBufferLength = readableBuffer.Length; + + try + { + if (readableBufferLength != 0) + { + foreach (var memory in readableBuffer) + { + await destination.WriteAsync(memory, cancellationToken); + } + } + + if (result.IsCompleted) + { + return; + } + } + finally + { + _pipeReader.AdvanceTo(readableBuffer.End); + } + } + } + } +} diff --git a/src/Http/Http/src/StreamPipeReader.cs b/src/Http/Http/src/StreamPipeReader.cs index 9d9e64caca..09295f2807 100644 --- a/src/Http/Http/src/StreamPipeReader.cs +++ b/src/Http/Http/src/StreamPipeReader.cs @@ -25,7 +25,8 @@ namespace System.IO.Pipelines private readonly MemoryPool _pool; private CancellationTokenSource _internalTokenSource; - private bool _isCompleted; + private bool _isReaderCompleted; + private bool _isWriterCompleted; private ExceptionDispatchInfo _exceptionInfo; private BufferSegment _readHead; @@ -182,12 +183,12 @@ namespace System.IO.Pipelines /// public override void Complete(Exception exception = null) { - if (_isCompleted) + if (_isReaderCompleted) { return; } - _isCompleted = true; + _isReaderCompleted = true; if (exception != null) { _exceptionInfo = ExceptionDispatchInfo.Capture(exception); @@ -248,6 +249,11 @@ namespace System.IO.Pipelines _readTail.End += length; _bufferedBytes += length; + + if (length == 0) + { + _isWriterCompleted = true; + } } catch (OperationCanceledException) { @@ -275,7 +281,7 @@ namespace System.IO.Pipelines private void ThrowIfCompleted() { - if (_isCompleted) + if (_isReaderCompleted) { ThrowHelper.ThrowInvalidOperationException_NoReadingAllowed(); } @@ -357,7 +363,7 @@ namespace System.IO.Pipelines [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsCompletedOrThrow() { - if (!_isCompleted) + if (!_isWriterCompleted) { return false; } diff --git a/src/Http/Http/src/StreamPipeWriter.cs b/src/Http/Http/src/StreamPipeWriter.cs index 6926f1e9b9..adfafe4455 100644 --- a/src/Http/Http/src/StreamPipeWriter.cs +++ b/src/Http/Http/src/StreamPipeWriter.cs @@ -65,7 +65,7 @@ namespace System.IO.Pipelines } /// - /// Gets the inner stream that is being read from. + /// Gets the inner stream that is being written to. /// public Stream InnerStream => _writingStream; diff --git a/src/Http/Http/src/ThrowHelper.cs b/src/Http/Http/src/ThrowHelper.cs index e671d9f6ee..1ae116b646 100644 --- a/src/Http/Http/src/ThrowHelper.cs +++ b/src/Http/Http/src/ThrowHelper.cs @@ -19,5 +19,17 @@ namespace System.IO.Pipelines public static void ThrowInvalidOperationException_NoDataRead() => throw CreateInvalidOperationException_NoDataRead(); [MethodImpl(MethodImplOptions.NoInlining)] public static Exception CreateInvalidOperationException_NoDataRead() => new InvalidOperationException("No data has been read into the StreamPipeReader."); + + public static void ThrowInvalidOperationException_SynchronousReadsDisallowed() => throw CreateInvalidOperationException_SynchronousReadsDisallowed(); + [MethodImpl(MethodImplOptions.NoInlining)] + public static Exception CreateInvalidOperationException_SynchronousReadsDisallowed() => new InvalidOperationException("Synchronous operations are disallowed. Call ReadAsync or set allowSynchronousIO to true instead."); + + public static void ThrowInvalidOperationException_SynchronousWritesDisallowed() => throw CreateInvalidOperationException_SynchronousWritesDisallowed(); + [MethodImpl(MethodImplOptions.NoInlining)] + public static Exception CreateInvalidOperationException_SynchronousWritesDisallowed() => new InvalidOperationException("Synchronous operations are disallowed. Call WriteAsync or set allowSynchronousIO to true instead."); + + public static void ThrowInvalidOperationException_SynchronousFlushesDisallowed() => throw CreateInvalidOperationException_SynchronousFlushesDisallowed(); + [MethodImpl(MethodImplOptions.NoInlining)] + public static Exception CreateInvalidOperationException_SynchronousFlushesDisallowed() => new InvalidOperationException("Synchronous operations are disallowed. Call FlushAsync or set allowSynchronousIO to true instead."); } } diff --git a/src/Http/Http/src/WriteOnlyPipeStream.cs b/src/Http/Http/src/WriteOnlyPipeStream.cs new file mode 100644 index 0000000000..0f5121cc2f --- /dev/null +++ b/src/Http/Http/src/WriteOnlyPipeStream.cs @@ -0,0 +1,162 @@ +// 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; +using System.Threading.Tasks; + +namespace System.IO.Pipelines +{ + /// + /// Represents a WriteOnlyStream backed by a PipeWriter + /// + public class WriteOnlyPipeStream : Stream + { + private PipeWriter _pipeWriter; + private bool _allowSynchronousIO = true; + + /// + /// Creates a new WriteOnlyStream + /// + /// The PipeWriter to write to. + public WriteOnlyPipeStream(PipeWriter pipeWriter) : + this(pipeWriter, allowSynchronousIO: true) + { + } + + /// + /// Creates a new WriteOnlyStream + /// + /// The PipeWriter to write to. + /// Whether synchronous IO is allowed. + public WriteOnlyPipeStream(PipeWriter pipeWriter, bool allowSynchronousIO) + { + _pipeWriter = pipeWriter; + _allowSynchronousIO = allowSynchronousIO; + } + + /// + public override bool CanSeek => false; + + /// + public override bool CanRead => false; + + /// + public override bool CanWrite => true; + + /// + public override long Length => throw new NotSupportedException(); + + /// + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// + public override int ReadTimeout + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => throw new NotSupportedException(); + + /// + public override void Flush() + { + if (!_allowSynchronousIO) + { + ThrowHelper.ThrowInvalidOperationException_SynchronousFlushesDisallowed(); + } + + FlushAsync(default).GetAwaiter().GetResult(); + } + + /// + public override async Task FlushAsync(CancellationToken cancellationToken) + { + await _pipeWriter.FlushAsync(cancellationToken); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + if (!_allowSynchronousIO) + { + ThrowHelper.ThrowInvalidOperationException_SynchronousWritesDisallowed(); + } + WriteAsync(buffer, offset, count, default).GetAwaiter().GetResult(); + } + + /// + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + var task = WriteAsync(buffer, offset, count, default, state); + if (callback != null) + { + task.ContinueWith(t => callback.Invoke(t)); + } + return task; + } + + /// + public override void EndWrite(IAsyncResult asyncResult) + { + ((Task)asyncResult).GetAwaiter().GetResult(); + } + + private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) + { + var tcs = new TaskCompletionSource(state); + var task = WriteAsync(buffer, offset, count, cancellationToken); + task.ContinueWith((task2, state2) => + { + var tcs2 = (TaskCompletionSource)state2; + if (task2.IsCanceled) + { + tcs2.SetCanceled(); + } + else if (task2.IsFaulted) + { + tcs2.SetException(task2.Exception); + } + else + { + tcs2.SetResult(null); + } + }, tcs, cancellationToken); + return tcs.Task; + } + + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + } + + /// + public override async ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) + { + await _pipeWriter.WriteAsync(source, cancellationToken); + } + } +} diff --git a/src/Http/Http/test/FlushResultCancellationTests.cs b/src/Http/Http/test/FlushResultCancellationTests.cs index e1f2476682..86dfac17c2 100644 --- a/src/Http/Http/test/FlushResultCancellationTests.cs +++ b/src/Http/Http/test/FlushResultCancellationTests.cs @@ -10,7 +10,7 @@ using Xunit; namespace System.IO.Pipelines.Tests { - public class FlushResultCancellationTests : PipeTest + public class FlushResultCancellationTests : StreamPipeTest { [Fact] public async Task FlushAsyncWithNewCancellationTokenNotAffectedByPrevious() diff --git a/src/Http/Http/test/PipeStreamTest.cs b/src/Http/Http/test/PipeStreamTest.cs new file mode 100644 index 0000000000..94b3ded7be --- /dev/null +++ b/src/Http/Http/test/PipeStreamTest.cs @@ -0,0 +1,85 @@ +// 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.Buffers; +using System.Text; +using System.Threading.Tasks; + +namespace System.IO.Pipelines.Tests +{ + public class PipeStreamTest : IDisposable + { + public Stream ReadingStream { get; set; } + public Stream WritingStream { get; set; } + + public Pipe Pipe { get; set; } + + public PipeReader Reader => Pipe.Reader; + + public PipeWriter Writer => Pipe.Writer; + + public PipeStreamTest() + { + Pipe = new Pipe(); + ReadingStream = new ReadOnlyPipeStream(Reader); + WritingStream = new WriteOnlyPipeStream(Writer); + } + + public void Dispose() + { + Writer.Complete(); + Reader.Complete(); + } + + public async Task WriteStringToStreamAsync(string input) + { + await WritingStream.WriteAsync(Encoding.ASCII.GetBytes(input)); + } + + public async Task WriteStringToPipeAsync(string input) + { + await Writer.WriteAsync(Encoding.ASCII.GetBytes(input)); + } + + public async Task WriteByteArrayToPipeAsync(byte[] input) + { + await Writer.WriteAsync(input); + } + + public async Task ReadFromPipeAsStringAsync() + { + var readResult = await Reader.ReadAsync(); + var result = Encoding.ASCII.GetString(readResult.Buffer.ToArray()); + Reader.AdvanceTo(readResult.Buffer.End); + return result; + } + + public async Task ReadFromStreamAsStringAsync() + { + var memory = new Memory(new byte[4096]); + var readLength = await ReadingStream.ReadAsync(memory); + var result = Encoding.ASCII.GetString(memory.ToArray(), 0, readLength); + return result; + } + + public async Task ReadFromPipeAsByteArrayAsync() + { + var readResult = await Reader.ReadAsync(); + var result = readResult.Buffer.ToArray(); + Reader.AdvanceTo(readResult.Buffer.End); + return result; + } + + public Task ReadFromStreamAsByteArrayAsync(int size) + { + return ReadFromStreamAsByteArrayAsync(size, ReadingStream); + } + + public async Task ReadFromStreamAsByteArrayAsync(int size, Stream stream) + { + var memory = new Memory(new byte[size]); + var readLength = await stream.ReadAsync(memory); + return memory.Slice(0, readLength).ToArray(); + } + } +} diff --git a/src/Http/Http/test/PipeWriterTests.cs b/src/Http/Http/test/PipeWriterTests.cs index 7edffabf2b..3343abd6f3 100644 --- a/src/Http/Http/test/PipeWriterTests.cs +++ b/src/Http/Http/test/PipeWriterTests.cs @@ -12,7 +12,7 @@ using Xunit; namespace System.IO.Pipelines.Tests { - public class PipeWriterTests : PipeTest + public class PipeWriterTests : StreamPipeTest { [Theory] diff --git a/src/Http/Http/test/ReadAsyncCancellationTests.cs b/src/Http/Http/test/ReadAsyncCancellationTests.cs index 72c80a7058..84d183c512 100644 --- a/src/Http/Http/test/ReadAsyncCancellationTests.cs +++ b/src/Http/Http/test/ReadAsyncCancellationTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -12,7 +12,7 @@ using Xunit; namespace System.IO.Pipelines.Tests { - public class ReadAsyncCancellationTests : PipeTest + public class ReadAsyncCancellationTests : StreamPipeTest { [Fact] public async Task AdvanceShouldResetStateIfReadCanceled() diff --git a/src/Http/Http/test/ReadOnlyPipeStreamTests.cs b/src/Http/Http/test/ReadOnlyPipeStreamTests.cs new file mode 100644 index 0000000000..427c30696a --- /dev/null +++ b/src/Http/Http/test/ReadOnlyPipeStreamTests.cs @@ -0,0 +1,164 @@ +// 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.Buffers; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class ReadOnlyPipeStreamTests : PipeStreamTest + { + [Fact] + public void CanSeekFalse() + { + Assert.False(ReadingStream.CanSeek); + } + + [Fact] + public void CanReadTrue() + { + Assert.True(ReadingStream.CanRead); + } + + [Fact] + public void CanWriteFalse() + { + Assert.False(ReadingStream.CanWrite); + } + + [Fact] + public void LengthThrows() + { + Assert.Throws(() => ReadingStream.Length); + } + + [Fact] + public void PositionThrows() + { + Assert.Throws(() => ReadingStream.Position); + Assert.Throws(() => ReadingStream.Position = 1); + } + + [Fact] + public void SeekThrows() + { + Assert.Throws(() => ReadingStream.Seek(0, SeekOrigin.Begin)); + } + + [Fact] + public void SetLengthThrows() + { + Assert.Throws(() => ReadingStream.SetLength(1)); + } + + [Fact] + public void WriteThrows() + { + Assert.Throws(() => ReadingStream.Write(new byte[1], 0, 1)); + } + + [Fact] + public async Task WriteAsyncThrows() + { + await Assert.ThrowsAsync(async () => await ReadingStream.WriteAsync(new byte[1], 0, 1)); + } + + [Fact] + public void ReadTimeoutThrows() + { + Assert.Throws(() => ReadingStream.WriteTimeout = 1); + Assert.Throws(() => ReadingStream.WriteTimeout); + } + + [Fact] + public async Task ReadAsyncWorks() + { + var expected = "Hello World!"; + + await WriteStringToPipeAsync(expected); + + Assert.Equal(expected, await ReadFromStreamAsStringAsync()); + } + + [Fact] + public async Task BasicLargeRead() + { + var expected = new byte[8000]; + + await WriteByteArrayToPipeAsync(expected); + + Assert.Equal(expected, await ReadFromStreamAsByteArrayAsync(8000)); + } + + [Fact] + public async Task ReadAsyncIsCalledFromCallingRead() + { + var pipeReader = await SetupMockPipeReader(); + var stream = new ReadOnlyPipeStream(pipeReader.Object); + + stream.Read(new byte[1]); + + pipeReader.Verify(m => m.ReadAsync(It.IsAny())); + } + + [Fact] + public async Task ReadAsyncIsCalledFromCallingReadAsync() + { + var pipeReader = await SetupMockPipeReader(); + var stream = new ReadOnlyPipeStream(pipeReader.Object); + + await stream.ReadAsync(new byte[1]); + + pipeReader.Verify(m => m.ReadAsync(It.IsAny())); + } + + [Fact] + public async Task ReadAsyncCancellationTokenIsPassedIntoReadAsync() + { + var pipeReader = await SetupMockPipeReader(); + var stream = new ReadOnlyPipeStream(pipeReader.Object); + var token = new CancellationToken(); + + await stream.ReadAsync(new byte[1], token); + + pipeReader.Verify(m => m.ReadAsync(token)); + } + + [Fact] + public async Task CopyToAsyncWorks() + { + const int expectedSize = 8000; + var expected = new byte[expectedSize]; + + await WriteByteArrayToPipeAsync(expected); + + Writer.Complete(); + var destStream = new MemoryStream(); + + await ReadingStream.CopyToAsync(destStream); + + Assert.Equal(expectedSize, destStream.Length); + } + + [Fact] + public void BlockSyncIOThrows() + { + var readOnlyPipeStream = new ReadOnlyPipeStream(Reader, allowSynchronousIO: false); + Assert.Throws(() => readOnlyPipeStream.Read(new byte[0], 0, 0)); + } + + private async Task> SetupMockPipeReader() + { + await WriteByteArrayToPipeAsync(new byte[1]); + + var pipeReader = new Mock(); + pipeReader + .Setup(m => m.ReadAsync(It.IsAny())) + .Returns(new ValueTask(new ReadResult(new ReadOnlySequence(new byte[1]), false, false))); + return pipeReader; + } + } +} diff --git a/src/Http/Http/test/ReadingAdaptersInteropTests.cs b/src/Http/Http/test/ReadingAdaptersInteropTests.cs new file mode 100644 index 0000000000..fad1909b13 --- /dev/null +++ b/src/Http/Http/test/ReadingAdaptersInteropTests.cs @@ -0,0 +1,147 @@ +// 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.Buffers; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class ReadingAdaptersInteropTests + { + [Fact] + public async Task CheckBasicReadPipeApi() + { + var pipe = new Pipe(); + var readStream = new ReadOnlyPipeStream(pipe.Reader); + var pipeReader = new StreamPipeReader(readStream); + + await pipe.Writer.WriteAsync(new byte[10]); + var res = await pipeReader.ReadAsync(); + Assert.Equal(new byte[10], res.Buffer.ToArray()); + } + + [Fact] + public async Task CheckNestedPipeApi() + { + var pipe = new Pipe(); + var reader = pipe.Reader; + for (var i = 0; i < 3; i++) + { + var readStream = new ReadOnlyPipeStream(reader); + reader = new StreamPipeReader(readStream); + } + + await pipe.Writer.WriteAsync(new byte[10]); + var res = await reader.ReadAsync(); + Assert.Equal(new byte[10], res.Buffer.ToArray()); + } + + [Fact] + public async Task CheckBasicReadStreamApi() + { + var stream = new MemoryStream(); + await stream.WriteAsync(new byte[10]); + stream.Position = 0; + + var pipeReader = new StreamPipeReader(stream); + var readOnlyStream = new ReadOnlyPipeStream(pipeReader); + + var resSize = await readOnlyStream.ReadAsync(new byte[10]); + + Assert.Equal(10, resSize); + } + + [Fact] + public async Task CheckNestedStreamApi() + { + var stream = new MemoryStream(); + await stream.WriteAsync(new byte[10]); + stream.Position = 0; + + Stream readOnlyStream = stream; + for (var i = 0; i < 3; i++) + { + var pipeReader = new StreamPipeReader(readOnlyStream); + readOnlyStream = new ReadOnlyPipeStream(pipeReader); + } + + var resSize = await readOnlyStream.ReadAsync(new byte[10]); + + Assert.Equal(10, resSize); + } + + [Fact] + public async Task ReadsCanBeCanceledViaProvidedCancellationToken() + { + var readOnlyStream = new ReadOnlyPipeStream(new HangingPipeReader()); + var pipeReader = new StreamPipeReader(readOnlyStream); + + var cts = new CancellationTokenSource(1); + await Task.Delay(1); + await Assert.ThrowsAsync(async () => await pipeReader.ReadAsync(cts.Token)); + } + + [Fact] + public async Task ReadCanBeCancelledViaCancelPendingReadWhenReadIsAsync() + { + var readOnlyStream = new ReadOnlyPipeStream(new HangingPipeReader()); + var pipeReader = new StreamPipeReader(readOnlyStream); + + var result = new ReadResult(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var task = Task.Run(async () => + { + var readingTask = pipeReader.ReadAsync(); + tcs.SetResult(0); + result = await readingTask; + }); + await tcs.Task; + pipeReader.CancelPendingRead(); + await task; + + Assert.True(result.IsCanceled); + } + + private class HangingPipeReader : PipeReader + { + public override void AdvanceTo(SequencePosition consumed) + { + throw new NotImplementedException(); + } + + public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) + { + throw new NotImplementedException(); + } + + public override void CancelPendingRead() + { + throw new NotImplementedException(); + } + + public override void Complete(Exception exception = null) + { + throw new NotImplementedException(); + } + + public override void OnWriterCompleted(Action callback, object state) + { + throw new NotImplementedException(); + } + + public override async ValueTask ReadAsync(CancellationToken cancellationToken = default) + { + await Task.Delay(30000, cancellationToken); + return new ReadResult(); + } + + public override bool TryRead(out ReadResult result) + { + result = new ReadResult(); + return false; + } + } + } +} diff --git a/src/Http/Http/test/StreamPipeReaderTests.cs b/src/Http/Http/test/StreamPipeReaderTests.cs index 4c94bb6ce3..b85a602c6d 100644 --- a/src/Http/Http/test/StreamPipeReaderTests.cs +++ b/src/Http/Http/test/StreamPipeReaderTests.cs @@ -11,7 +11,7 @@ using Xunit; namespace System.IO.Pipelines.Tests { - public partial class StreamPipeReaderTests : PipeTest + public partial class StreamPipeReaderTests : StreamPipeTest { [Fact] public async Task CanRead() @@ -198,6 +198,8 @@ namespace System.IO.Pipelines.Tests [Fact] public async Task ReadAsyncReturnsCanceledInterleaved() { + Write(new byte[10000]); + // Cancel and Read interleaved to confirm cancellations are independent for (var i = 0; i < 3; i++) { @@ -501,22 +503,6 @@ namespace System.IO.Pipelines.Tests Assert.False(readResult.Buffer.IsSingleSegment); } - [Fact] - public async Task SetMinimumReadThresholdToMiminumSegmentSizeOnlyGetNewBlockWhenDataIsWritten() - { - CreateReader(minimumReadThreshold: 16); - WriteByteArray(0); - - var readResult = await Reader.ReadAsync(); - Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); - - WriteByteArray(16); - readResult = await Reader.ReadAsync(); - - Assert.Equal(16, readResult.Buffer.Length); - Assert.True(readResult.Buffer.IsSingleSegment); - } - [Fact] public void SetMinimumReadThresholdOfZeroThrows() { @@ -595,6 +581,24 @@ namespace System.IO.Pipelines.Tests Assert.Equal("c", ReadFromStreamAsString(buffer)); } + [Fact] + public async Task ReadAsyncWithNoDataCompletesReader() + { + var readResult = await Reader.ReadAsync(); + + Assert.True(readResult.IsCompleted); + } + + [Fact] + public async Task ReadAsyncWithEmptyDataCompletesStream() + { + WriteByteArray(0); + + var readResult = await Reader.ReadAsync(); + + Assert.True(readResult.IsCompleted); + } + private async Task ReadFromPipeAsString() { var readResult = await Reader.ReadAsync(); diff --git a/src/Http/Http/test/PipeTest.cs b/src/Http/Http/test/StreamPipeTest.cs similarity index 93% rename from src/Http/Http/test/PipeTest.cs rename to src/Http/Http/test/StreamPipeTest.cs index 01801b9518..2d7f406c33 100644 --- a/src/Http/Http/test/PipeTest.cs +++ b/src/Http/Http/test/StreamPipeTest.cs @@ -1,14 +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.IO; -using System.IO.Pipelines; using System.Text; namespace System.IO.Pipelines.Tests { - public abstract class PipeTest : IDisposable + public abstract class StreamPipeTest : IDisposable { protected const int MaximumSizeHigh = 65; @@ -20,7 +17,7 @@ namespace System.IO.Pipelines.Tests public PipeReader Reader { get; set; } - protected PipeTest() + protected StreamPipeTest() { MemoryStream = new MemoryStream(); Writer = new StreamPipeWriter(MemoryStream, MinimumSegmentSize, new TestMemoryPool()); diff --git a/src/Http/Http/test/StreamPipeWriterTests.cs b/src/Http/Http/test/StreamPipeWriterTests.cs index d51bca9772..4810fedcda 100644 --- a/src/Http/Http/test/StreamPipeWriterTests.cs +++ b/src/Http/Http/test/StreamPipeWriterTests.cs @@ -12,7 +12,7 @@ using Xunit; namespace System.IO.Pipelines.Tests { - public class StreamPipeWriterTests : PipeTest + public class StreamPipeWriterTests : StreamPipeTest { [Fact] public async Task CanWriteAsyncMultipleTimesIntoSameBlock() diff --git a/src/Http/Http/test/WriteOnlyPipeStreamTests.cs b/src/Http/Http/test/WriteOnlyPipeStreamTests.cs new file mode 100644 index 0000000000..c6ee97660c --- /dev/null +++ b/src/Http/Http/test/WriteOnlyPipeStreamTests.cs @@ -0,0 +1,199 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class WriteOnlyPipeStreamTests : PipeStreamTest + { + [Fact] + public void CanSeekFalse() + { + Assert.False(WritingStream.CanSeek); + } + + [Fact] + public void CanReadFalse() + { + Assert.False(WritingStream.CanRead); + } + + [Fact] + public void CanWriteTrue() + { + Assert.True(WritingStream.CanWrite); + } + + [Fact] + public void LengthThrows() + { + Assert.Throws(() => WritingStream.Length); + } + + [Fact] + public void PositionThrows() + { + Assert.Throws(() => WritingStream.Position); + Assert.Throws(() => WritingStream.Position = 1); + } + + [Fact] + public void SeekThrows() + { + Assert.Throws(() => WritingStream.Seek(0, SeekOrigin.Begin)); + } + + [Fact] + public void SetLengthThrows() + { + Assert.Throws(() => WritingStream.SetLength(1)); + } + + [Fact] + public void ReadThrows() + { + Assert.Throws(() => WritingStream.Read(new byte[1], 0, 1)); + } + + [Fact] + public async Task ReadAsyncThrows() + { + await Assert.ThrowsAsync(async () => await WritingStream.ReadAsync(new byte[1], 0, 1)); + } + + [Fact] + public void ReadTimeoutThrows() + { + Assert.Throws(() => WritingStream.ReadTimeout = 1); + Assert.Throws(() => WritingStream.ReadTimeout); + } + + [Fact] + public async Task WriteAsyncWithReadOnlyMemoryWorks() + { + var expected = "Hello World!"; + + await WriteStringToStreamAsync(expected); + + Assert.Equal(expected, await ReadFromPipeAsStringAsync()); + } + + [Fact] + public async Task WriteAsyncWithArrayWorks() + { + var expected = new byte[1]; + + await WritingStream.WriteAsync(expected, 0, expected.Length); + + Assert.Equal(expected, await ReadFromPipeAsByteArrayAsync()); + } + + [Fact] + public async Task BasicLargeWrite() + { + var expected = new byte[8000]; + + await WritingStream.WriteAsync(expected); + + Assert.Equal(expected, await ReadFromPipeAsByteArrayAsync()); + } + + [Fact] + public void FlushAsyncIsCalledFromCallingFlush() + { + var pipeWriter = new Mock(); + var stream = new WriteOnlyPipeStream(pipeWriter.Object); + + stream.Flush(); + + pipeWriter.Verify(m => m.FlushAsync(default)); + } + + [Fact] + public async Task FlushAsyncIsCalledFromCallingFlushAsync() + { + var pipeWriter = new Mock(); + var stream = new WriteOnlyPipeStream(pipeWriter.Object); + + await stream.FlushAsync(); + + pipeWriter.Verify(m => m.FlushAsync(default)); + } + + [Fact] + public async Task FlushAsyncCancellationTokenIsPassedIntoFlushAsync() + { + var pipeWriter = new Mock(); + var stream = new WriteOnlyPipeStream(pipeWriter.Object); + var token = new CancellationToken(); + + await stream.FlushAsync(token); + + pipeWriter.Verify(m => m.FlushAsync(token)); + } + + [Fact] + public void WriteAsyncIsCalledFromCallingWrite() + { + var pipeWriter = new Mock(); + var stream = new WriteOnlyPipeStream(pipeWriter.Object); + + stream.Write(new byte[1]); + + pipeWriter.Verify(m => m.WriteAsync(It.IsAny>(), It.IsAny())); + } + + [Fact] + public async Task WriteAsyncIsCalledFromCallingWriteAsync() + { + var pipeWriter = new Mock(); + var stream = new WriteOnlyPipeStream(pipeWriter.Object); + + await stream.WriteAsync(new byte[1]); + + pipeWriter.Verify(m => m.WriteAsync(It.IsAny>(), It.IsAny())); + } + + [Fact] + public async Task WriteAsyncCancellationTokenIsPassedIntoWriteAsync() + { + var pipeWriter = new Mock(); + var stream = new WriteOnlyPipeStream(pipeWriter.Object); + var token = new CancellationToken(); + + await stream.WriteAsync(new byte[1], token); + + pipeWriter.Verify(m => m.WriteAsync(It.IsAny>(), token)); + } + + [Fact] + public void WriteAsyncIsCalledFromBeginWrite() + { + var pipeWriter = new Mock(); + var stream = new WriteOnlyPipeStream(pipeWriter.Object); + stream.BeginWrite(new byte[1], 0, 1, null, this); + pipeWriter.Verify(m => m.WriteAsync(It.IsAny>(), It.IsAny())); + } + + [Fact] + public async Task BeginAndEndWriteWork() + { + var expected = new byte[1]; + var asyncResult = WritingStream.BeginWrite(expected, 0, 1, null, this); + WritingStream.EndWrite(asyncResult); + Assert.Equal(expected, await ReadFromPipeAsByteArrayAsync()); + } + + [Fact] + public void BlockSyncIOThrows() + { + var writeOnlyPipeStream = new WriteOnlyPipeStream(Writer, allowSynchronousIO: false); + Assert.Throws(() => writeOnlyPipeStream.Write(new byte[0], 0, 0)); + Assert.Throws(() => writeOnlyPipeStream.Flush()); + } + } +} diff --git a/src/Http/Http/test/WritingAdaptersInteropTests.cs b/src/Http/Http/test/WritingAdaptersInteropTests.cs new file mode 100644 index 0000000000..44280961b2 --- /dev/null +++ b/src/Http/Http/test/WritingAdaptersInteropTests.cs @@ -0,0 +1,155 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Buffers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class WritingAdaptersInteropTests : PipeStreamTest + { + [Fact] + public async Task CheckBasicWritePipeApi() + { + var pipe = new Pipe(); + var writeOnlyStream = new WriteOnlyPipeStream(pipe.Writer); + var pipeWriter = new StreamPipeWriter(writeOnlyStream); + await pipeWriter.WriteAsync(new byte[10]); + + var res = await pipe.Reader.ReadAsync(); + Assert.Equal(new byte[10], res.Buffer.ToArray()); + } + + [Fact] + public async Task CheckNestedPipeApi() + { + var pipe = new Pipe(); + var writer = pipe.Writer; + for (var i = 0; i < 3; i++) + { + var writeOnlyStream = new WriteOnlyPipeStream(writer); + writer = new StreamPipeWriter(writeOnlyStream); + } + + await writer.WriteAsync(new byte[10]); + + var res = await pipe.Reader.ReadAsync(); + Assert.Equal(new byte[10], res.Buffer.ToArray()); + } + + [Fact] + public async Task CheckBasicWriteStreamApi() + { + var stream = new MemoryStream(); + var pipeWriter = new StreamPipeWriter(stream); + var writeOnlyStream = new WriteOnlyPipeStream(pipeWriter); + + await writeOnlyStream.WriteAsync(new byte[10]); + + stream.Position = 0; + var res = await ReadFromStreamAsByteArrayAsync(10, stream); + Assert.Equal(new byte[10], res); + } + + [Fact] + public async Task CheckNestedStreamApi() + { + var stream = new MemoryStream(); + Stream writeOnlyStream = stream; + for (var i = 0; i < 3; i++) + { + var pipeWriter = new StreamPipeWriter(writeOnlyStream); + writeOnlyStream = new WriteOnlyPipeStream(pipeWriter); + } + + await writeOnlyStream.WriteAsync(new byte[10]); + + stream.Position = 0; + var res = await ReadFromStreamAsByteArrayAsync(10, stream); + Assert.Equal(new byte[10], res); + } + + [Fact] + public async Task WritesCanBeCanceledViaProvidedCancellationToken() + { + var writeOnlyStream = new WriteOnlyPipeStream(new HangingPipeWriter()); + var pipeWriter = new StreamPipeWriter(writeOnlyStream); + var cts = new CancellationTokenSource(1); + await Assert.ThrowsAsync(async () => await pipeWriter.WriteAsync(new byte[1], cts.Token)); + } + + [Fact] + public async Task WriteCanBeCanceledViaCancelPendingFlushWhenFlushIsAsync() + { + var writeOnlyStream = new WriteOnlyPipeStream(new HangingPipeWriter()); + var pipeWriter = new StreamPipeWriter(writeOnlyStream); + + FlushResult flushResult = new FlushResult(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var task = Task.Run(async () => + { + try + { + var writingTask = pipeWriter.WriteAsync(new byte[1]); + tcs.SetResult(0); + flushResult = await writingTask; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + throw ex; + } + }); + + await tcs.Task; + + pipeWriter.CancelPendingFlush(); + + await task; + + Assert.True(flushResult.IsCanceled); + } + + private class HangingPipeWriter : PipeWriter + { + public override void Advance(int bytes) + { + } + + public override void CancelPendingFlush() + { + throw new NotImplementedException(); + } + + public override void Complete(Exception exception = null) + { + throw new NotImplementedException(); + } + + public override async ValueTask FlushAsync(CancellationToken cancellationToken = default) + { + await Task.Delay(30000, cancellationToken); + return new FlushResult(); + } + + public override Memory GetMemory(int sizeHint = 0) + { + return new Memory(new byte[4096]); + } + + public override Span GetSpan(int sizeHint = 0) + { + return new Span(new byte[4096]); + } + + public override void OnReaderCompleted(Action callback, object state) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Identity/test/InMemory.Test/FunctionalTest.cs b/src/Identity/test/InMemory.Test/FunctionalTest.cs index 7d3ab77494..b193bc16cd 100644 --- a/src/Identity/test/InMemory.Test/FunctionalTest.cs +++ b/src/Identity/test/InMemory.Test/FunctionalTest.cs @@ -352,12 +352,12 @@ namespace Microsoft.AspNetCore.Identity.InMemory } else if (req.Path == new PathString("/me")) { - Describe(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, null, "Application"))); + await DescribeAsync(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, null, "Application"))); } else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder)) { var auth = await context.AuthenticateAsync(remainder.Value.Substring(1)); - Describe(res, auth); + await DescribeAsync(res, auth); } else if (req.Path == new PathString("/testpath") && testpath != null) { @@ -393,7 +393,7 @@ namespace Microsoft.AspNetCore.Identity.InMemory return server; } - private static void Describe(HttpResponse res, AuthenticateResult result) + private static async Task DescribeAsync(HttpResponse res, AuthenticateResult result) { res.StatusCode = 200; res.ContentType = "text/xml"; @@ -412,7 +412,7 @@ namespace Microsoft.AspNetCore.Identity.InMemory { xml.WriteTo(writer); } - res.Body.Write(memory.ToArray(), 0, memory.ToArray().Length); + await res.Body.WriteAsync(memory.ToArray(), 0, memory.ToArray().Length); } } diff --git a/src/Middleware/CORS/samples/SampleDestination/SampleMiddleware.cs b/src/Middleware/CORS/samples/SampleDestination/SampleMiddleware.cs index ac53eb5908..4400bd46d3 100644 --- a/src/Middleware/CORS/samples/SampleDestination/SampleMiddleware.cs +++ b/src/Middleware/CORS/samples/SampleDestination/SampleMiddleware.cs @@ -25,9 +25,7 @@ namespace SampleDestination context.Response.ContentType = "text/plain; charset=utf-8"; context.Response.ContentLength = content.Length; - context.Response.Body.Write(content, 0, content.Length); - - return Task.CompletedTask; + return context.Response.Body.WriteAsync(content, 0, content.Length); } } } diff --git a/src/Middleware/CORS/samples/SampleDestination/Startup.cs b/src/Middleware/CORS/samples/SampleDestination/Startup.cs index cf306c6eeb..b17c504ad8 100644 --- a/src/Middleware/CORS/samples/SampleDestination/Startup.cs +++ b/src/Middleware/CORS/samples/SampleDestination/Startup.cs @@ -86,9 +86,7 @@ namespace SampleDestination context.Response.ContentType = "text/plain; charset=utf-8"; context.Response.ContentLength = content.Length; - context.Response.Body.Write(content, 0, content.Length); - - return Task.CompletedTask; + return context.Response.Body.WriteAsync(content, 0, content.Length); } } } diff --git a/src/Middleware/ResponseCaching/test/TestUtils.cs b/src/Middleware/ResponseCaching/test/TestUtils.cs index 09f21a8878..319c1d590f 100644 --- a/src/Middleware/ResponseCaching/test/TestUtils.cs +++ b/src/Middleware/ResponseCaching/test/TestUtils.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -81,6 +81,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var uniqueId = Guid.NewGuid().ToString(); if (TestRequestDelegate(context, uniqueId)) { + var feature = context.Features.Get(); + if (feature != null) + { + feature.AllowSynchronousIO = true; + } context.Response.Write(uniqueId); } return Task.CompletedTask; diff --git a/src/Middleware/ResponseCompression/test/ResponseCompressionMiddlewareTest.cs b/src/Middleware/ResponseCompression/test/ResponseCompressionMiddlewareTest.cs index 43cf908475..4f5c6bf073 100644 --- a/src/Middleware/ResponseCompression/test/ResponseCompressionMiddlewareTest.cs +++ b/src/Middleware/ResponseCompression/test/ResponseCompressionMiddlewareTest.cs @@ -548,6 +548,12 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests app.UseResponseCompression(); app.Run(context => { + var feature = context.Features.Get(); + if (feature != null) + { + feature.AllowSynchronousIO = true; + } + context.Response.Headers[HeaderNames.ContentMD5] = "MD5"; context.Response.ContentType = TextPlain; context.Response.Body.Write(new byte[10], 0, 10); @@ -652,6 +658,12 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests context.Response.ContentType = TextPlain; context.Features.Get()?.DisableResponseBuffering(); + var feature = context.Features.Get(); + if (feature != null) + { + feature.AllowSynchronousIO = true; + } + foreach (var signal in responseReceived) { context.Response.Body.Write(new byte[1], 0, 1); diff --git a/src/Middleware/Rewrite/src/Internal/UrlActions/CustomResponseAction.cs b/src/Middleware/Rewrite/src/Internal/UrlActions/CustomResponseAction.cs index b7cc36eeda..4fc7d1615c 100644 --- a/src/Middleware/Rewrite/src/Internal/UrlActions/CustomResponseAction.cs +++ b/src/Middleware/Rewrite/src/Internal/UrlActions/CustomResponseAction.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Text; @@ -31,6 +31,11 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlActions if (!string.IsNullOrEmpty(StatusDescription)) { + var feature = context.HttpContext.Features.Get(); + if (feature != null) + { + feature.AllowSynchronousIO = true; + } var content = Encoding.UTF8.GetBytes(StatusDescription); response.ContentLength = content.Length; response.ContentType = "text/plain; charset=utf-8"; @@ -42,4 +47,4 @@ namespace Microsoft.AspNetCore.Rewrite.Internal.UrlActions context.Logger?.CustomResponse(context.HttpContext.Request.GetEncodedUrl()); } } -} \ No newline at end of file +} diff --git a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs index 50a237234d..ca40dc6ea8 100644 --- a/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs +++ b/src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RoutingTestsBase.cs @@ -26,6 +26,28 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public HttpClient Client { get; } + [Theory] + [InlineData("http://localhost/Login/Index", "Login", "Index", "http://localhost/Login")] + [InlineData("http://localhost/Login/Sso", "Login", "Sso", "http://localhost/Login/Sso")] + [InlineData("http://localhost/Contact/Index", "Contact", "Index", "http://localhost/Contact")] + [InlineData("http://localhost/Contact/Sso", "Contact", "Sso", "http://localhost/Contact/Sso")] + public async Task ConventionalRoutedAction_RouteUrl_AmbientValues(string requestUrl, string controller, string action, string expectedUrl) + { + // Arrange & Act + var response = await Client.GetAsync(requestUrl); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal(controller, result.Controller); + Assert.Equal(action, result.Action); + + Assert.Equal(expectedUrl, Assert.Single(result.ExpectedUrls)); + } + [Fact] public async Task ConventionalRoutedAction_RouteContainsPage_RouteNotMatched() { diff --git a/src/Mvc/test/WebSites/RoutingWebSite/Controllers/ContactController.cs b/src/Mvc/test/WebSites/RoutingWebSite/Controllers/ContactController.cs new file mode 100644 index 0000000000..3421f6580b --- /dev/null +++ b/src/Mvc/test/WebSites/RoutingWebSite/Controllers/ContactController.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc; + +namespace RoutingWebSite +{ + // This controller is reachable via traditional routing. + public class ContactController : Controller + { + private readonly TestResponseGenerator _generator; + + public ContactController(TestResponseGenerator generator) + { + _generator = generator; + } + + public IActionResult Index() + { + return _generator.Generate(Url.RouteUrl("ActionAsMethod", null, Url.ActionContext.HttpContext.Request.Scheme)); + } + + public IActionResult Sso() + { + return _generator.Generate(Url.RouteUrl("ActionAsMethod", null, Url.ActionContext.HttpContext.Request.Scheme)); + } + } +} \ No newline at end of file diff --git a/src/Mvc/test/WebSites/RoutingWebSite/Controllers/LoginController.cs b/src/Mvc/test/WebSites/RoutingWebSite/Controllers/LoginController.cs new file mode 100644 index 0000000000..724481f5b7 --- /dev/null +++ b/src/Mvc/test/WebSites/RoutingWebSite/Controllers/LoginController.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc; + +namespace RoutingWebSite +{ + // This controller is reachable via traditional routing. + public class LoginController : Controller + { + private readonly TestResponseGenerator _generator; + + public LoginController(TestResponseGenerator generator) + { + _generator = generator; + } + + public IActionResult Index() + { + return _generator.Generate(Url.RouteUrl("ActionAsMethod", null, Url.ActionContext.HttpContext.Request.Scheme)); + } + + public IActionResult Sso() + { + return _generator.Generate(Url.RouteUrl("ActionAsMethod", null, Url.ActionContext.HttpContext.Request.Scheme)); + } + } +} \ No newline at end of file diff --git a/src/Security/Authentication/Core/src/HandleRequestResult.cs b/src/Security/Authentication/Core/src/HandleRequestResult.cs index da9b6ea01c..0a9a179db3 100644 --- a/src/Security/Authentication/Core/src/HandleRequestResult.cs +++ b/src/Security/Authentication/Core/src/HandleRequestResult.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -92,5 +92,10 @@ namespace Microsoft.AspNetCore.Authentication { return new HandleRequestResult() { Skipped = true }; } + + public new static HandleRequestResult NoResult() + { + return new HandleRequestResult() { None = true }; + } } } diff --git a/src/Security/Authentication/Core/src/RemoteAuthenticationHandler.cs b/src/Security/Authentication/Core/src/RemoteAuthenticationHandler.cs index 95ededd349..a8975f11a0 100644 --- a/src/Security/Authentication/Core/src/RemoteAuthenticationHandler.cs +++ b/src/Security/Authentication/Core/src/RemoteAuthenticationHandler.cs @@ -283,7 +283,7 @@ namespace Microsoft.AspNetCore.Authentication return HandleRequestResult.Handle(); } - return HandleRequestResult.Fail("Access was denied by the resource owner or by the remote server.", properties); + return HandleRequestResult.NoResult(); } } -} \ No newline at end of file +} diff --git a/src/Security/Authentication/OAuth/src/OAuthHandler.cs b/src/Security/Authentication/OAuth/src/OAuthHandler.cs index 8bbe34f4b0..a84f8a485a 100644 --- a/src/Security/Authentication/OAuth/src/OAuthHandler.cs +++ b/src/Security/Authentication/OAuth/src/OAuthHandler.cs @@ -71,7 +71,9 @@ namespace Microsoft.AspNetCore.Authentication.OAuth // Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information. if (StringValues.Equals(error, "access_denied")) { - return await HandleAccessDeniedErrorAsync(properties); + var result = await HandleAccessDeniedErrorAsync(properties); + return !result.None ? result + : HandleRequestResult.Fail("Access was denied by the resource owner or by the remote server.", properties); } var failureMessage = new StringBuilder(); diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs index 40e0eacbb8..9823763873 100644 --- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs +++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs @@ -560,7 +560,11 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect // Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information. if (string.Equals(authorizationResponse.Error, "access_denied", StringComparison.Ordinal)) { - return await HandleAccessDeniedErrorAsync(properties); + var result = await HandleAccessDeniedErrorAsync(properties); + if (!result.None) + { + return result; + } } return HandleRequestResult.Fail(CreateOpenIdConnectProtocolException(authorizationResponse, response: null), properties); diff --git a/src/Security/Authentication/Twitter/src/TwitterHandler.cs b/src/Security/Authentication/Twitter/src/TwitterHandler.cs index cc8591ed32..01f80dc72f 100644 --- a/src/Security/Authentication/Twitter/src/TwitterHandler.cs +++ b/src/Security/Authentication/Twitter/src/TwitterHandler.cs @@ -62,7 +62,9 @@ namespace Microsoft.AspNetCore.Authentication.Twitter // approve the authorization demand requested by the remote authorization server. // Since it's a frequent scenario (that is not caused by incorrect configuration), // denied errors are handled differently using HandleAccessDeniedErrorAsync(). - return await HandleAccessDeniedErrorAsync(properties); + var result = await HandleAccessDeniedErrorAsync(properties); + return !result.None ? result + : HandleRequestResult.Fail("Access was denied by the resource owner or by the remote server.", properties); } var returnedToken = query["oauth_token"]; @@ -311,4 +313,4 @@ namespace Microsoft.AspNetCore.Authentication.Twitter } } } -} \ No newline at end of file +} diff --git a/src/Security/Authentication/test/CookieTests.cs b/src/Security/Authentication/test/CookieTests.cs index 107fc5db1e..504a264b41 100644 --- a/src/Security/Authentication/test/CookieTests.cs +++ b/src/Security/Authentication/test/CookieTests.cs @@ -1302,7 +1302,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies app.Use(async (context, next) => { var result = await context.AuthenticateAsync("Cookies"); - Describe(context.Response, result); + await DescribeAsync(context.Response, result); }); }) .ConfigureServices(services => services.AddAuthentication().AddCookie("Cookies", o => @@ -1478,12 +1478,12 @@ namespace Microsoft.AspNetCore.Authentication.Cookies } else if (req.Path == new PathString("/me")) { - Describe(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, new AuthenticationProperties(), CookieAuthenticationDefaults.AuthenticationScheme))); + await DescribeAsync(res, AuthenticateResult.Success(new AuthenticationTicket(context.User, new AuthenticationProperties(), CookieAuthenticationDefaults.AuthenticationScheme))); } else if (req.Path.StartsWithSegments(new PathString("/me"), out remainder)) { var ticket = await context.AuthenticateAsync(remainder.Value.Substring(1)); - Describe(res, ticket); + await DescribeAsync(res, ticket); } else if (req.Path == new PathString("/testpath") && testpath != null) { @@ -1510,7 +1510,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies return server; } - private static void Describe(HttpResponse res, AuthenticateResult result) + private static Task DescribeAsync(HttpResponse res, AuthenticateResult result) { res.StatusCode = 200; res.ContentType = "text/xml"; @@ -1524,7 +1524,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies xml.Add(result.Ticket.Properties.Items.Select(extra => new XElement("extra", new XAttribute("type", extra.Key), new XAttribute("value", extra.Value)))); } var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); - res.Body.Write(xmlBytes, 0, xmlBytes.Length); + return res.Body.WriteAsync(xmlBytes, 0, xmlBytes.Length); } private static async Task SendAsync(TestServer server, string uri, string cookieHeader = null) diff --git a/src/Security/Authentication/test/DynamicSchemeTests.cs b/src/Security/Authentication/test/DynamicSchemeTests.cs index d658609b04..6df65f66ad 100644 --- a/src/Security/Authentication/test/DynamicSchemeTests.cs +++ b/src/Security/Authentication/test/DynamicSchemeTests.cs @@ -145,7 +145,7 @@ namespace Microsoft.AspNetCore.Authentication { var name = (remainder.Value.Length > 0) ? remainder.Value.Substring(1) : null; var result = await context.AuthenticateAsync(name); - res.Describe(result?.Ticket?.Principal); + await res.DescribeAsync(result?.Ticket?.Principal); } else if (req.Path.StartsWithSegments(new PathString("/remove"), out remainder)) { diff --git a/src/Security/Authentication/test/GoogleTests.cs b/src/Security/Authentication/test/GoogleTests.cs index ce158eaf20..24b7ef0fab 100644 --- a/src/Security/Authentication/test/GoogleTests.cs +++ b/src/Security/Authentication/test/GoogleTests.cs @@ -1177,26 +1177,26 @@ namespace Microsoft.AspNetCore.Authentication.Google { var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme); var tokens = result.Properties.GetTokens(); - res.Describe(tokens); + await res.DescribeAsync(tokens); } else if (req.Path == new PathString("/me")) { - res.Describe(context.User); + await res.DescribeAsync(context.User); } else if (req.Path == new PathString("/authenticate")) { var result = await context.AuthenticateAsync(TestExtensions.CookieAuthenticationScheme); - res.Describe(result.Principal); + await res.DescribeAsync(result.Principal); } else if (req.Path == new PathString("/authenticateGoogle")) { var result = await context.AuthenticateAsync("Google"); - res.Describe(result?.Principal); + await res.DescribeAsync(result?.Principal); } else if (req.Path == new PathString("/authenticateFacebook")) { var result = await context.AuthenticateAsync("Facebook"); - res.Describe(result?.Principal); + await res.DescribeAsync(result?.Principal); } else if (req.Path == new PathString("/unauthorized")) { diff --git a/src/Security/Authentication/test/MicrosoftAccountTests.cs b/src/Security/Authentication/test/MicrosoftAccountTests.cs index 26a5484c83..4005b50efe 100644 --- a/src/Security/Authentication/test/MicrosoftAccountTests.cs +++ b/src/Security/Authentication/test/MicrosoftAccountTests.cs @@ -270,7 +270,7 @@ namespace Microsoft.AspNetCore.Authentication.Tests.MicrosoftAccount } else if (req.Path == new PathString("/me")) { - res.Describe(context.User); + await res.DescribeAsync(context.User); } else if (req.Path == new PathString("/signIn")) { diff --git a/src/Security/Authentication/test/PolicyTests.cs b/src/Security/Authentication/test/PolicyTests.cs index 368026beb8..60e6c41be6 100644 --- a/src/Security/Authentication/test/PolicyTests.cs +++ b/src/Security/Authentication/test/PolicyTests.cs @@ -469,7 +469,7 @@ namespace Microsoft.AspNetCore.Authentication { var name = (remainder.Value.Length > 0) ? remainder.Value.Substring(1) : null; var result = await context.AuthenticateAsync(name); - res.Describe(result?.Ticket?.Principal); + await res.DescribeAsync(result?.Ticket?.Principal); } else { diff --git a/src/Security/Authentication/test/TestExtensions.cs b/src/Security/Authentication/test/TestExtensions.cs index 87d6d95a2c..98b45a0159 100644 --- a/src/Security/Authentication/test/TestExtensions.cs +++ b/src/Security/Authentication/test/TestExtensions.cs @@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Authentication return transaction; } - public static void Describe(this HttpResponse res, ClaimsPrincipal principal) + public static Task DescribeAsync(this HttpResponse res, ClaimsPrincipal principal) { res.StatusCode = 200; res.ContentType = "text/xml"; @@ -62,10 +62,10 @@ namespace Microsoft.AspNetCore.Authentication } } var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); - res.Body.Write(xmlBytes, 0, xmlBytes.Length); + return res.Body.WriteAsync(xmlBytes, 0, xmlBytes.Length); } - public static void Describe(this HttpResponse res, IEnumerable tokens) + public static Task DescribeAsync(this HttpResponse res, IEnumerable tokens) { res.StatusCode = 200; res.ContentType = "text/xml"; @@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Authentication } } var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); - res.Body.Write(xmlBytes, 0, xmlBytes.Length); + return res.Body.WriteAsync(xmlBytes, 0, xmlBytes.Length); } } diff --git a/src/Security/CookiePolicy/test/TestExtensions.cs b/src/Security/CookiePolicy/test/TestExtensions.cs index 9456094d41..4bbce1c302 100644 --- a/src/Security/CookiePolicy/test/TestExtensions.cs +++ b/src/Security/CookiePolicy/test/TestExtensions.cs @@ -45,24 +45,5 @@ namespace Microsoft.AspNetCore.CookiePolicy } return transaction; } - - public static void Describe(this HttpResponse res, ClaimsPrincipal principal) - { - res.StatusCode = 200; - res.ContentType = "text/xml"; - var xml = new XElement("xml"); - if (principal != null) - { - foreach (var identity in principal.Identities) - { - xml.Add(identity.Claims.Select(claim => - new XElement("claim", new XAttribute("type", claim.Type), - new XAttribute("value", claim.Value), - new XAttribute("issuer", claim.Issuer)))); - } - } - var xmlBytes = Encoding.UTF8.GetBytes(xml.ToString()); - res.Body.Write(xmlBytes, 0, xmlBytes.Length); - } } } diff --git a/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs b/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs index ed3bfde554..af044d8297 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Listener/ResponseBodyTests.cs @@ -353,6 +353,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener context = await server.AcceptAsync(Utilities.DefaultTimeout).Before(responseTask); // First write sends headers + context.AllowSynchronousIO = true; context.Response.Body.Write(new byte[10], 0, 10); var response = await responseTask; diff --git a/src/Servers/HttpSys/test/FunctionalTests/ResponseCachingTests.cs b/src/Servers/HttpSys/test/FunctionalTests/ResponseCachingTests.cs index 7333a1ce71..73cb15f113 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/ResponseCachingTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/ResponseCachingTests.cs @@ -307,7 +307,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests httpContext.Response.Headers["x-request-count"] = (requestCount++).ToString(); httpContext.Response.Headers["Cache-Control"] = "public, max-age=10"; httpContext.Response.ContentLength = 10; - httpContext.Response.Body.Flush(); + httpContext.Response.Body.FlushAsync(); return httpContext.Response.Body.WriteAsync(new byte[10], 0, 10); })) { diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp index 83527fbbdc..8491edc5a4 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp @@ -214,9 +214,7 @@ try DWORD dwBufferSize = s_initialGetNativeSearchDirectoriesBufferSize; DWORD dwRequiredBufferSize = 0; - RETURN_LAST_ERROR_IF_NULL(m_hHostFxrDll = LoadLibraryW(hostfxrOptions.GetHostFxrLocation().c_str())); - - auto const hostFxr = HostFxr::CreateFromLoadedModule(); + m_hHostFxrDll.Load(hostfxrOptions.GetHostFxrLocation()); { auto redirectionOutput = LoggingHelpers::CreateOutputs( @@ -227,7 +225,7 @@ try ); StandardStreamRedirection stdOutRedirection(*redirectionOutput.get(), m_pServer.IsCommandLineLaunch()); - auto hostFxrErrorRedirection = hostFxr.RedirectOutput(redirectionOutput.get()); + auto hostFxrErrorRedirection = m_hHostFxrDll.RedirectOutput(redirectionOutput.get()); struNativeSearchPaths.resize(dwBufferSize); while (TRUE) @@ -237,7 +235,7 @@ try hostfxrOptions.GetArguments(hostfxrArgc, hostfxrArgv); - const auto intHostFxrExitCode = hostFxr.GetNativeSearchDirectories( + const auto intHostFxrExitCode = m_hHostFxrDll.GetNativeSearchDirectories( hostfxrArgc, hostfxrArgv.get(), struNativeSearchPaths.data(), diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.h b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.h index 92efd74390..faa81855c3 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.h @@ -10,6 +10,7 @@ #include "HandleWrapper.h" #include "ApplicationFactory.h" #include "RedirectionOutput.h" +#include "HostFxr.h" class HandlerResolver { @@ -35,7 +36,7 @@ private: SRWLOCK m_requestHandlerLoadLock {}; std::wstring m_loadedApplicationId; APP_HOSTING_MODEL m_loadedApplicationHostingModel; - HandleWrapper m_hHostFxrDll; + HostFxr m_hHostFxrDll; static const PCWSTR s_pwzAspnetcoreInProcessRequestHandlerName; static const PCWSTR s_pwzAspnetcoreOutOfProcessRequestHandlerName; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.cpp index 2959f94f49..f291e227c7 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.cpp @@ -30,6 +30,61 @@ 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) +{ + m_hHostFxrDll = moduleHandle; + try + { + m_hostfxr_main_fn = ModuleHelpers::GetKnownProcAddress(moduleHandle, "hostfxr_main"); + m_hostfxr_get_native_search_directories_fn = ModuleHelpers::GetKnownProcAddress(moduleHandle, "hostfxr_get_native_search_directories"); + m_corehost_set_error_writer_fn = ModuleHelpers::GetKnownProcAddress(moduleHandle, "hostfxr_set_error_writer", /* optional */ true); + } + catch (...) + { + EventLog::Error( + ASPNETCORE_EVENT_GENERAL_ERROR, + ASPNETCORE_EVENT_HOSTFXR_DLL_INVALID_VERSION_MSG, + ModuleHelpers::GetModuleFileNameValue(moduleHandle).c_str() + ); + + throw; + } +} + + +void HostFxr::Load(const std::wstring& location) +{ + try + { + HMODULE hModule; + THROW_LAST_ERROR_IF_NULL(hModule = LoadLibraryW(location.c_str())); + Load(hModule); + } + catch (...) + { + EventLog::Error( + ASPNETCORE_EVENT_GENERAL_ERROR, + ASPNETCORE_EVENT_HOSTFXR_DLL_UNABLE_TO_LOAD_MSG, + location.c_str() + ); + + throw; + } +} + +void HostFxr::SetMain(hostfxr_main_fn hostfxr_main_fn) +{ + m_hostfxr_main_fn = hostfxr_main_fn; +} + int HostFxr::Main(DWORD argc, const PCWSTR* argv) const noexcept(false) { return m_hostfxr_main_fn(argc, argv); @@ -44,27 +99,3 @@ HostFxrErrorRedirector HostFxr::RedirectOutput(RedirectionOutput* writer) const { return HostFxrErrorRedirector(m_corehost_set_error_writer_fn, writer); } - -HostFxr HostFxr::CreateFromLoadedModule() -{ - HMODULE hModule; - THROW_LAST_ERROR_IF_NULL(hModule = GetModuleHandle(L"hostfxr.dll")); - - try - { - return HostFxr( - ModuleHelpers::GetKnownProcAddress(hModule, "hostfxr_main"), - ModuleHelpers::GetKnownProcAddress(hModule, "hostfxr_get_native_search_directories"), - ModuleHelpers::GetKnownProcAddress(hModule, "hostfxr_set_error_writer", /* optional */ true)); - } - catch (...) - { - EventLog::Error( - ASPNETCORE_EVENT_GENERAL_ERROR, - ASPNETCORE_EVENT_HOSTFXR_DLL_INVALID_VERSION_MSG, - ModuleHelpers::GetModuleFileNameValue(hModule).c_str() - ); - - throw; - } -} diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h index e8951741d0..990963b6e7 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h @@ -26,9 +26,13 @@ private: static inline thread_local RedirectionOutput* m_writeFunction; }; -class HostFxr +class HostFxr: NonCopyable { public: + HostFxr() : HostFxr(nullptr, nullptr, nullptr) + { + } + HostFxr( hostfxr_main_fn hostfxr_main_fn, hostfxr_get_native_search_directories_fn hostfxr_get_native_search_directories_fn, @@ -39,18 +43,22 @@ public: { } + void Load(); + void Load(HMODULE moduleHandle); + void Load(const std::wstring& location); + ~HostFxr() = default; + void SetMain(hostfxr_main_fn hostfxr_main_fn); + int Main(DWORD argc, CONST PCWSTR* argv) const noexcept(false); int GetNativeSearchDirectories(INT argc, CONST PCWSTR* argv, PWSTR buffer, DWORD buffer_size, DWORD* required_buffer_size) const noexcept; HostFxrErrorRedirector RedirectOutput(RedirectionOutput* writer) const noexcept; - static - HostFxr CreateFromLoadedModule(); - private: + HandleWrapper m_hHostFxrDll; hostfxr_main_fn m_hostfxr_main_fn; hostfxr_get_native_search_directories_fn m_hostfxr_get_native_search_directories_fn; corehost_set_error_writer_fn m_corehost_set_error_writer_fn; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/resources.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/resources.h index fdee8b9578..991c402262 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/resources.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/resources.h @@ -39,6 +39,7 @@ #define ASPNETCORE_EVENT_RECYCLE_FAILURE_CONFIGURATION_MSG L"Failed to recycle application due to a configuration change at '%s'. Recycling worker process." #define ASPNETCORE_EVENT_MODULE_DISABLED_MSG L"AspNetCore Module is disabled" #define ASPNETCORE_EVENT_HOSTFXR_DLL_INVALID_VERSION_MSG L"Hostfxr version used does not support 'hostfxr_get_native_search_directories', update the version of hostfxr to a higher version. Path to hostfxr: '%s'." +#define ASPNETCORE_EVENT_HOSTFXR_DLL_UNABLE_TO_LOAD_MSG L"Unable to load '%s'. This might be caused by a bitness mismatch between IIS application pool and published application." #define ASPNETCORE_EVENT_HOSTFXR_FAILURE_MSG L"Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. This most likely means the app is misconfigured, please check the versions of Microsoft.NetCore.App and Microsoft.AspNetCore.App that are targeted by the application and are installed on the machine." #define ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION_MSG L"Application '%s' with physical root '%s' hit unexpected managed exception, exception code = '0x%x'. Please check the stderr logs for more information." #define ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION_STDOUT_MSG L"Application '%s' with physical root '%s' hit unexpected managed exception, exception code = '0x%x'. Last 4KB characters of captured stdout and stderr logs:\r\n%s" diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp index 0aee6b887d..1c6f8de401 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp @@ -189,11 +189,11 @@ IN_PROCESS_APPLICATION::ExecuteApplication() hostFxrResolutionResult->GetArguments(context->m_argc, context->m_argv); THROW_IF_FAILED(SetEnvironmentVariablesOnWorkerProcess()); - context->m_hostFxr = HostFxr::CreateFromLoadedModule(); + context->m_hostFxr.Load(); } else { - context->m_hostFxr = HostFxr(s_fMainCallback, nullptr, nullptr); + context->m_hostFxr.SetMain(s_fMainCallback); } // There can only ever be a single instance of .NET Core diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index fbb421d07a..1e20316d27 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -82,6 +82,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core _options = options; _server = server; _logger = logger; + + ((IHttpBodyControlFeature)this).AllowSynchronousIO = _options.AllowSynchronousIO; } public Version HttpVersion { get; set; } diff --git a/src/Servers/IIS/IIS/src/IISServerOptions.cs b/src/Servers/IIS/IIS/src/IISServerOptions.cs index f439080195..47d7619f8a 100644 --- a/src/Servers/IIS/IIS/src/IISServerOptions.cs +++ b/src/Servers/IIS/IIS/src/IISServerOptions.cs @@ -1,10 +1,20 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNetCore.Http; + namespace Microsoft.AspNetCore.Builder { public class IISServerOptions { + /// + /// Gets or sets a value that controls whether synchronous IO is allowed for the and + /// + /// + /// Defaults to true. + /// + public bool AllowSynchronousIO { get; set; } = true; + /// /// If true the server should set HttpContext.User. If false the server will only provide an /// identity when explicitly requested by the AuthenticationScheme. diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/CompressionTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/CompressionTests.cs index ce1c84e609..09b1ecc077 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/CompressionTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/CompressionTests.cs @@ -81,10 +81,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("identity", 0)); client.DefaultRequestHeaders.Add("Response-Content-Type", "text/event-stream"); - var messages = "Message1\r\nMessage2\r\n"; + var messages = "Message1\r\nMessage2\r\n\r\n"; // Send messages with terminator - var response = await client.PostAsync("ReadAndWriteEchoLines", new StringContent(messages + "\r\n")); + var response = await client.PostAsync("ReadAndWriteEchoLines", new StringContent(messages)); Assert.Equal(messages, await response.Content.ReadAsStringAsync()); Assert.True(response.Content.Headers.TryGetValues("Content-Type", out var contentTypes)); Assert.Single(contentTypes, "text/event-stream"); diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs index c76ea789fb..af512044a2 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs @@ -110,6 +110,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests [ConditionalTheory] [InlineData(RuntimeArchitecture.x64)] [InlineData(RuntimeArchitecture.x86)] + [SkipIfNotAdmin] [RequiresNewShim] [RequiresIIS(IISCapability.PoolEnvironmentVariables)] public async Task StartsWithDotnetInstallLocation(RuntimeArchitecture runtimeArchitecture) @@ -256,7 +257,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests } [ConditionalFact] - public async Task RemoveHostfxrFromApp_InProcessHostfxrInvalid() + public async Task RemoveHostfxrFromApp_InProcessHostfxrAPIAbsent() { var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); deploymentParameters.ApplicationType = ApplicationType.Standalone; @@ -271,6 +272,20 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessHostfxrInvalid(deploymentResult), Logger); } + [ConditionalFact] + public async Task RemoveHostfxrFromApp_InProcessHostfxrLoadFailure() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.ApplicationType = ApplicationType.Standalone; + var deploymentResult = await DeployAsync(deploymentParameters); + + // We don't distinguish between load failure types so making dll empty should be enough + File.WriteAllText(Path.Combine(deploymentResult.ContentRoot, "hostfxr.dll"), ""); + await AssertSiteFailsToStartWithInProcessStaticContent(deploymentResult); + + EventLogHelpers.VerifyEventLogEvent(deploymentResult, EventLogHelpers.InProcessHostfxrUnableToLoad(deploymentResult), Logger); + } + [ConditionalFact] public async Task TargedDifferenceSharedFramework_FailedToFindNativeDependencies() { diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs index 9745d412ac..f7bf3b8a43 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs @@ -185,6 +185,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests return $"Hostfxr version used does not support 'hostfxr_get_native_search_directories', update the version of hostfxr to a higher version. Path to hostfxr: '(.*)'."; } + public static string InProcessHostfxrUnableToLoad(IISDeploymentResult deploymentResult) + { + return $"Unable to load '(.*)'. This might be caused by a bitness mismatch between IIS application pool and published application."; + } + public static string InProcessFailedToFindNativeDependencies(IISDeploymentResult deploymentResult) { return "Invoking hostfxr to find the inprocess request handler failed without finding any native dependencies. " + diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs index fb505a14ec..31f69a3b63 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs @@ -331,10 +331,10 @@ namespace TestSite await ctx.Response.Body.FlushAsync(); var reader = new StreamReader(ctx.Request.Body); - while (!reader.EndOfStream) + while (true) { var line = await reader.ReadLineAsync(); - if (line == "") + if (line == null) { return; } @@ -357,10 +357,10 @@ namespace TestSite await ctx.Response.Body.FlushAsync(); var reader = new StreamReader(ctx.Request.Body); - while (!reader.EndOfStream) + while (true) { var line = await reader.ReadLineAsync(); - if (line == "") + if (line == null) { return; } @@ -438,8 +438,8 @@ namespace TestSite private async Task TestReadOffsetWorks(HttpContext ctx) { var buffer = new byte[11]; - ctx.Request.Body.Read(buffer, 0, 6); - ctx.Request.Body.Read(buffer, 6, 5); + await ctx.Request.Body.ReadAsync(buffer, 0, 6); + await ctx.Request.Body.ReadAsync(buffer, 6, 5); await ctx.Response.WriteAsync(Encoding.UTF8.GetString(buffer)); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs index a160d9180e..31d73b2481 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs @@ -68,7 +68,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { - var task = ReadAsync(buffer, offset, count, default(CancellationToken), state); + var task = ReadAsync(buffer, offset, count, default, state); if (callback != null) { task.ContinueWith(t => callback.Invoke(t)); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseStream.cs index 2d50902e31..4ec97117cb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseStream.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public override void Flush() { - FlushAsync(default(CancellationToken)).GetAwaiter().GetResult(); + FlushAsync(default).GetAwaiter().GetResult(); } public override Task FlushAsync(CancellationToken cancellationToken) @@ -64,12 +64,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed); } - WriteAsync(buffer, offset, count, default(CancellationToken)).GetAwaiter().GetResult(); + WriteAsync(buffer, offset, count, default).GetAwaiter().GetResult(); } public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { - var task = WriteAsync(buffer, offset, count, default(CancellationToken), state); + var task = WriteAsync(buffer, offset, count, default, state); if (callback != null) { task.ContinueWith(t => callback.Invoke(t)); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs index 30248cac50..5dfe295497 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http try { - if (!readableBuffer.IsEmpty) + if (readableBufferLength != 0) { // buffer.Length is int actual = (int)Math.Min(readableBufferLength, buffer.Length); @@ -69,8 +69,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // Make sure we don't double-count bytes on the next read. _alreadyTimedBytes = readableBufferLength - actual; - var slice = readableBuffer.Slice(0, actual); - consumed = readableBuffer.GetPosition(actual); + var slice = actual == readableBufferLength ? readableBuffer : readableBuffer.Slice(0, actual); + consumed = slice.End; slice.CopyTo(buffer.Span); return actual; @@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http try { - if (!readableBuffer.IsEmpty) + if (readableBufferLength != 0) { foreach (var memory in readableBuffer) { diff --git a/src/Servers/Kestrel/shared/test/TestApplicationErrorLogger.cs b/src/Servers/Kestrel/shared/test/TestApplicationErrorLogger.cs index 8ff01d719e..ef57c3888c 100644 --- a/src/Servers/Kestrel/shared/test/TestApplicationErrorLogger.cs +++ b/src/Servers/Kestrel/shared/test/TestApplicationErrorLogger.cs @@ -1,10 +1,11 @@ -// 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; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; @@ -16,6 +17,9 @@ namespace Microsoft.AspNetCore.Testing // Application errors are logged using 13 as the eventId. private const int ApplicationErrorEventId = 13; + private Func _messageFilter; + private TaskCompletionSource _messageFilterTcs; + public List IgnoredExceptions { get; } = new List(); public bool ThrowOnCriticalErrors { get; set; } = true; @@ -32,6 +36,19 @@ namespace Microsoft.AspNetCore.Testing public int ApplicationErrorsLogged => Messages.Count(message => message.EventId.Id == ApplicationErrorEventId); + public Task WaitForMessage(Func messageFilter) + { + if (_messageFilterTcs != null) + { + throw new InvalidOperationException($"{nameof(WaitForMessage)} cannot be called concurrently."); + } + + _messageFilterTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _messageFilter = messageFilter; + + return _messageFilterTcs.Task; + } + public IDisposable BeginScope(TState state) { Scopes.Enqueue(state); @@ -77,13 +94,21 @@ namespace Microsoft.AspNetCore.Testing throw new Exception($"Unexpected connection error. {log}"); } - Messages.Enqueue(new LogMessage + var logMessage = new LogMessage { LogLevel = logLevel, EventId = eventId, Exception = exception, Message = formatter(state, exception) - }); + }; + + Messages.Enqueue(logMessage); + + if (_messageFilter?.Invoke(logMessage) == true) + { + _messageFilterTcs.TrySetResult(logMessage); + _messageFilterTcs = null; + } } public class LogMessage diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs index 230931cd77..034b9a058a 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs @@ -134,7 +134,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 Assert.False(requestTask.IsCompleted); await requestStarted.Task.DefaultTimeout(); - await server.StopAsync(new CancellationToken(true)).DefaultTimeout(); + // Wait for the graceful shutdown log before canceling the token passed to StopAsync and triggering an ungraceful shutdown. + // Otherwise, graceful shutdown might be skipped causing there to be no corresponding log. https://github.com/aspnet/AspNetCore/issues/6556 + var closingMessageTask = TestApplicationErrorLogger.WaitForMessage(m => m.Message.Contains("is closing.")).DefaultTimeout(); + + var cts = new CancellationTokenSource(); + var stopServerTask = server.StopAsync(cts.Token).DefaultTimeout(); + + await closingMessageTask; + cts.Cancel(); + await stopServerTask; } Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains("is closing.")); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index 9225795d1c..46ce78701c 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -1933,6 +1933,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await InitializeConnectionAsync(async context => { + var bodyControlFeature = context.Features.Get(); + bodyControlFeature.AllowSynchronousIO = true; // Fill the flow control window to create async back pressure. await context.Response.Body.WriteAsync(new byte[windowSize + 1], 0, windowSize + 1); context.Response.Body.Write(new byte[1], 0, 1); @@ -2084,4 +2086,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); } } -} \ No newline at end of file +} diff --git a/src/Shared/RazorViews/BaseView.cs b/src/Shared/RazorViews/BaseView.cs index a171d8d1f2..3b661d7170 100644 --- a/src/Shared/RazorViews/BaseView.cs +++ b/src/Shared/RazorViews/BaseView.cs @@ -66,9 +66,13 @@ namespace Microsoft.Extensions.RazorViews Context = context; Request = Context.Request; Response = Context.Response; - Output = new StreamWriter(Response.Body, UTF8NoBOM, 4096, leaveOpen: true); + var buffer = new MemoryStream(); + Output = new StreamWriter(buffer, UTF8NoBOM, 4096, leaveOpen: true); await ExecuteAsync(); + await Output.FlushAsync(); Output.Dispose(); + buffer.Seek(0, SeekOrigin.Begin); + await buffer.CopyToAsync(Response.Body); } /// @@ -276,4 +280,4 @@ namespace Microsoft.Extensions.RazorViews .Select(HtmlEncoder.Encode)); } } -} \ No newline at end of file +} diff --git a/src/SignalR/clients/java/signalr/signalr.javaproj b/src/SignalR/clients/java/signalr/signalr.javaproj index bb48253cc2..db43b5522e 100644 --- a/src/SignalR/clients/java/signalr/signalr.javaproj +++ b/src/SignalR/clients/java/signalr/signalr.javaproj @@ -2,9 +2,6 @@ - 1.0.$(AspNetCorePatchVersion) - $(VersionPrefix) - $(VersionPrefix)-$(VersionSuffix) java:signalr diff --git a/src/SignalR/server/Core/src/StreamTracker.cs b/src/SignalR/server/Core/src/StreamTracker.cs index 2d7432ebd6..a507fde54b 100644 --- a/src/SignalR/server/Core/src/StreamTracker.cs +++ b/src/SignalR/server/Core/src/StreamTracker.cs @@ -78,7 +78,9 @@ namespace Microsoft.AspNetCore.SignalR public ChannelConverter() { - _channel = Channel.CreateUnbounded(); + // TODO: Make this configurable or figure out a good limit + // https://github.com/aspnet/AspNetCore/issues/4399 + _channel = Channel.CreateBounded(10); } public Type GetItemType() diff --git a/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorComponentsWeb-CSharp.App.csproj.in b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorComponentsWeb-CSharp.App.csproj.in index fa2964e06a..8766fab7d0 100644 --- a/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorComponentsWeb-CSharp.App.csproj.in +++ b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorComponentsWeb-CSharp.App.csproj.in @@ -10,7 +10,6 @@ - diff --git a/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorComponentsWeb-CSharp.Server.csproj.in b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorComponentsWeb-CSharp.Server.csproj.in index f9b7bf3d1b..970e5e45cc 100644 --- a/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorComponentsWeb-CSharp.Server.csproj.in +++ b/src/Templating/src/Microsoft.DotNet.Web.ProjectTemplates/RazorComponentsWeb-CSharp.Server.csproj.in @@ -12,7 +12,6 @@ -